henchman-cli
Version:
An all-in-one, interactive command-line tool that simplifies creating, setting up, and managing development projects like Flutter and Node.js while automating repetitive tasks.
236 lines (212 loc) • 6.93 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import ini from 'ini';
import {Argument, program} from 'commander';
import child_process from 'child_process';
import util from 'util';
import yaml from 'js-yaml';
import {byeMessage, errorMessage, greetMessage, henchman, logo} from './constants.js';
import {configureCLI} from '../menus/configure.js';
import {cleanupCLI} from '../menus/cleanup.js';
import {setupCLI} from '../menus/setup.js';
import {startCLI} from '../menus/start.js';
import {getCLI} from '../menus/get.js';
import {createCLI} from '../menus/create.js';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const baseDir = path.join(__dirname, '../');
const packageJson = require('../package.json');
// Config directory in user home
export function getConfigDir() {
return path.join(os.homedir(), '.henchman');
}
export function getConfigPath() {
return path.join(getConfigDir(), 'config.ini');
}
// Tool validation
export async function checkToolInstalled(tool) {
const exec = util.promisify(child_process.exec);
try {
const command = process.platform === 'win32' ? `where ${tool}` : `which ${tool}`;
await exec(command);
return true;
} catch {
return false;
}
}
export async function validateRequiredTools(tools) {
const missing = [];
for (const tool of tools) {
const installed = await checkToolInstalled(tool);
if (!installed) {
missing.push(tool);
}
}
if (missing.length > 0) {
console.log(chalk.red(`\n${henchman}: Missing required tools: ${missing.join(', ')}`));
console.log(chalk.yellow(`Please install them before continuing.\n`));
return false;
}
return true;
}
// Path validation
export function validatePath(inputPath) {
// Check for characters that could cause shell issues
const dangerousChars = /[;&|`$(){}[\]<>!]/;
if (dangerousChars.test(inputPath)) {
return {
valid: false,
error: 'Path contains special characters that are not allowed: ; & | ` $ ( ) { } [ ] < > !'
};
}
return { valid: true };
}
export async function initCLI() {
program.name('henchman')
.version(packageJson.version)
.description(
packageJson.description
)
.addHelpText('beforeAll', `${logo}\n${greetMessage}`);
configureCLI();
createCLI();
cleanupCLI();
setupCLI();
startCLI();
getCLI();
await program.parseAsync(process.argv);
}
export async function getPath() {
console.log(`Enter the file path to execute ${henchman}. ${chalk.blue('(Leave empty to run in current folder)')}`);
console.log(chalk.yellow('(If the folder doesn\'t exist Henchman will create one)'));
console.log(`Enter ${chalk.red('q')} to abort.`)
const input = await inquirer.prompt({
type: 'input',
name: 'path',
message: 'Enter path:'
});
const inputPath = input['path'];
if (inputPath === 'q') {
console.log(byeMessage);
process.exit(0);
}
if (inputPath === '') {
return process.cwd();
}
// Validate path
const validation = validatePath(inputPath);
if (!validation.valid) {
console.log(chalk.red(`${henchman}: ${validation.error}`));
console.log(byeMessage);
process.exit(1);
}
return inputPath;
}
export async function execute(command, message) {
console.log(`${command}`);
const spinner = ora(`${henchman} ${message ?? 'running command ...'}\n`).start();
const exec = util.promisify(child_process.exec);
const {stdout} = await exec(command, {maxBuffer: 1024 * 1024 * 10}).catch((err) => errorSpinnerExit(spinner, err));
console.log(stdout);
spinner.succeed('Done');
}
export async function menu(list) {
const choice = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'Please choose from one:',
choices: [
...list,
'Exit',
],
},
]);
const answer = choice['choice'];
if (answer === 'Exit') {
console.log(byeMessage);
process.exit(0);
}
return answer;
}
export function cliArgument(program, command, commandDesc, argChoices, action) {
return program.command(command)
.description(
`${chalk.blue(commandDesc)}\n` +
chalk.yellow(
'' +
'[config] options:\n' +
`${argChoices.join('\n')}`
)
)
.addArgument(
new Argument('[config]',)
.choices(argChoices)
)
.action(action);
}
export function invalidCommandExit() {
console.log(logo);
console.log(greetMessage);
console.log(chalk.red('Invalid Command\n'))
console.log(program.helpInformation());
program.error(byeMessage, {exitCode: 1});
}
export function errorSpinnerExit(spinner = undefined, err) {
console.log(errorMessage);
console.log(err);
console.log(err.stack);
if (spinner !== undefined) {
spinner.fail(chalk.red('Error'));
}
program.error(byeMessage, {exitCode: 1});
}
export function errorExit(message) {
console.log(message);
program.error(byeMessage, {exitCode: 1});
}
export async function getArgumentByMenu(choices, argument, greet) {
if (greet) {
console.log(logo);
console.log(greetMessage);
}
if (argument === undefined) {
argument = await menu(choices);
}
return argument.toLowerCase();
}
export async function getConfig(noError = false) {
const spinner = ora(`${henchman}: Fetching config file...`).start();
try {
const configPath = getConfigPath();
const data = await fs.readFile(configPath, {encoding: 'utf-8'});
const config = ini.parse(data);
spinner.succeed(`${henchman} config found`);
return config;
} catch (err) {
if (err.code === 'ENOENT') {
spinner.fail(chalk.red(`${henchman} configuration not found`));
if (noError) {
return {};
} else {
console.log('Run \`henchman configure\` command')
console.log(byeMessage);
process.exit(1);
}
} else {
errorSpinnerExit(spinner, err);
}
}
}
export async function getFlutterProjectName(directoryPath) {
const pubspecContent = await fs.readFile(path.join(directoryPath, 'pubspec.yaml'), 'utf8');
const pubspec = yaml.load(pubspecContent);
return pubspec.name;
}