// This is a CLI tool, using console is OK /* eslint no-console: 0 */ import {spawn, exec} from 'child_process'; import type {SpawnOptions} from 'child_process'; import {existsSync} from 'fs'; import {isAbsolute, resolve} from 'path'; import {promisify} from 'util'; import args from 'args'; import chalk from 'chalk'; import _columnify from 'columnify'; import got from 'got'; import open from 'open'; import ora from 'ora'; import {version} from '../app/package.json'; import * as api from './api'; let commandPromise: Promise | undefined; const assertPluginName = (pluginName: string) => { if (!pluginName) { console.error(chalk.red('Plugin name is required')); process.exit(1); } }; const checkConfig = () => { if (api.exists()) { return true; } let msg = chalk.red(`Error! Config file not found: ${api.configPath}\n`); msg += 'Please launch Hyper and retry.'; console.error(msg); process.exit(1); }; const columnify = (data: {name: string; description: string}[]) => { const maxNameLength = Math.max(...data.map((entry) => entry.name.length), 0); const descriptionWidth = process.stdout.columns - maxNameLength - 1; return _columnify(data, { showHeaders: false, config: { description: { maxWidth: descriptionWidth }, name: { dataTransform: (nameValue) => chalk.green(nameValue) } } }).replace(/\s+$/gm, ''); // remove padding from the end of all lines }; args.command( 'install', 'Install a plugin', (name, args_) => { checkConfig(); const pluginName = args_[0]; assertPluginName(pluginName); commandPromise = api .install(pluginName) .then(() => console.log(chalk.green(`${pluginName} installed successfully!`))) .catch((err) => console.error(chalk.red(err))); }, ['i'] ); args.command( 'uninstall', 'Uninstall a plugin', (name, args_) => { checkConfig(); const pluginName = args_[0]; assertPluginName(pluginName); commandPromise = api .uninstall(pluginName) .then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`))) .catch((err) => console.error(chalk.red(err))); }, ['u', 'rm', 'remove'] ); args.command( 'list', 'List installed plugins', () => { checkConfig(); const plugins = api.list(); if (plugins) { console.log(plugins); } else { console.log(chalk.red(`No plugins installed yet.`)); } process.exit(0); }, ['ls'] ); const lsRemote = (pattern?: string) => { // note that no errors are catched by this function const URL = `https://api.npms.io/v2/search?q=${ (pattern && `${pattern}+`) || '' }keywords:hyper-plugin,hyper-theme&size=250`; type npmResult = {package: {name: string; description: string}}; return got(URL) .then((response) => JSON.parse(response.body).results as npmResult[]) .then((entries) => entries.map((entry) => entry.package)) .then((entries) => entries.map(({name, description}) => { return {name, description}; }) ); }; args.command( 'search', 'Search for plugins on npm', (name, args_) => { const spinner = ora('Searching').start(); const query = args_[0] ? args_[0].toLowerCase() : ''; commandPromise = lsRemote(query) .then((entries) => { if (entries.length === 0) { spinner.fail(); console.error(chalk.red(`Your search '${query}' did not match any plugins`)); console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`); process.exit(1); } else { const msg = columnify(entries); spinner.succeed(); console.log(msg); } }) .catch((err) => { spinner.fail(); console.error(chalk.red(err)); // TODO }); }, ['s'] ); args.command( 'list-remote', 'List plugins available on npm', () => { const spinner = ora('Searching').start(); commandPromise = lsRemote() .then((entries) => { const msg = columnify(entries); spinner.succeed(); console.log(msg); }) .catch((err) => { spinner.fail(); console.error(chalk.red(err)); // TODO }); }, ['lsr', 'ls-remote'] ); args.command( 'docs', 'Open the npm page of a plugin', (name, args_) => { const pluginName = args_[0]; assertPluginName(pluginName); void open(`http://ghub.io/${pluginName}`, {wait: false}); process.exit(0); }, ['d', 'h', 'home'] ); args.command( 'version', 'Show the version of hyper', () => { console.log(version); process.exit(0); }, [] ); args.command('', 'Launch Hyper'); args.option(['v', 'verbose'], 'Verbose mode', false); const main = (argv: string[]) => { const flags = args.parse(argv, { name: 'hyper', version: false, mri: { boolean: ['v', 'verbose'] }, mainColor: 'yellow', subColor: 'dim' }); if (commandPromise) { return commandPromise; } const env = Object.assign({}, process.env, { // this will signal Hyper that it was spawned from this module HYPER_CLI: '1', ELECTRON_NO_ATTACH_CONSOLE: '1' }); delete env['ELECTRON_RUN_AS_NODE']; if (flags.verbose) { env['ELECTRON_ENABLE_LOGGING'] = '1'; } const options: SpawnOptions = { detached: true, env }; const args_ = args.sub.map((arg) => { const cwd = isAbsolute(arg) ? arg : resolve(process.cwd(), arg); if (!existsSync(cwd)) { console.error(chalk.red(`Error! Directory or file does not exist: ${cwd}`)); process.exit(1); } return cwd; }); if (!flags.verbose) { options['stdio'] = 'ignore'; if (process.platform === 'darwin') { //Use `open` to prevent multiple Hyper process const cmd = `open -b com.quineglobal.hyper ${args_}`; const opts = { env }; return promisify(exec)(cmd, opts); } } const child = spawn(process.execPath, args_, options); if (flags.verbose) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call child.stdout?.on('data', (data) => console.log(data.toString('utf8'))); // eslint-disable-next-line @typescript-eslint/no-unsafe-call child.stderr?.on('data', (data) => console.error(data.toString('utf8'))); } if (flags.verbose) { return new Promise((c) => child.once('exit', () => c(null))); } child.unref(); return Promise.resolve(); }; function eventuallyExit(code: number) { setTimeout(() => process.exit(code), 100); } main(process.argv) .then(() => eventuallyExit(0)) .catch((err) => { console.error(err.stack ? err.stack : err); eventuallyExit(1); });