UNPKG

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
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; }