cli/src/cli.ts

386 lines
13 KiB
TypeScript

import { logger } from '@squeebot/core/lib/core';
import { IRepository } from '@squeebot/core/lib/plugin/repository';
import { ReadLine } from 'readline';
import { Squeebot } from './core';
declare type Executable = (...args: string[]) => Promise<any>;
class CLICommand {
constructor(
public name: string,
public func: Executable,
public sub: CLICommand[],
public after?: (returned: any, ...args: string[]) => Promise<any>) {}
public getSub(name: string): CLICommand | undefined {
return this.sub.find(c => c.name.startsWith(name));
}
/**
* Executes a sub-command or, if not found, the current one.
* @param args Command arguments
*/
public async execute(args: string[]): Promise<any> {
if (args[0]) {
const subl = this.getSub(args[0]);
if (subl) {
const rv1 = await subl.execute(args.slice(1));
if (this.after) {
return this.after.call(this, rv1, ...args);
}
return rv1;
}
}
const rv2 = await this.func.call(this, ...args);
if (this.after) {
return this.after.call(this, rv2, ...args);
}
return rv2;
}
}
function cmd(name: string, func: Executable, sub: CLICommand[] = []): CLICommand {
return new CLICommand(name, func, sub);
}
export class SqueebotCLI {
private inspector = false;
private cmds: CLICommand[] = [
// Repository management
cmd('repository', async (...args: string[]): Promise<void> => {
logger.log('Usage: repository add <url> | update <name> | remove <name> | plugins <name> | list');
}, [
cmd('install', async (...args: string[]): Promise<void> => {
if (!args.length) {
throw new Error('URL is required');
}
for (const urlp of args) {
const repo = await this.bot.repositoryManager.installRepository(urlp);
logger.log('Installed repository %s!', repo.name);
}
}),
cmd('remove', async (...args: string[]): Promise<void> => {
if (!args.length) {
throw new Error('Name is required');
}
for (const namep of args) {
await this.bot.repositoryManager.uninstallRepository(namep);
logger.log('Installed repository %s.', namep);
}
}),
cmd('update', async (...args: string[]): Promise<void> => {
if (!args.length) {
const repolist = this.bot.repositoryManager.getAll();
for (const repo of repolist) {
await this.checkUpdate(repo);
}
return;
}
for (const namep of args) {
const repo = this.bot.repositoryManager.getRepoByName(namep);
if (!repo) {
throw new Error(`No such repository "${namep}" found.`);
}
await this.checkUpdate(repo);
}
}),
cmd('plugins', async (...args: string[]): Promise<void> => {
if (!args.length) {
throw new Error('Name is required');
}
const repo = this.bot.repositoryManager.getRepoByName(args[0]);
if (!repo) {
throw new Error(`No such repository "${args[0]}" found.`);
}
logger.log('List of plugins in %s:', args[0],
repo.plugins.map(x => `${x.name}@${x.version}`).join(', '));
}),
cmd('list', async (...args: string[]): Promise<void> => {
const repos = this.bot.repositoryManager.getAll();
logger.log('List of installed repositories:');
for (const repo of repos) {
logger.log('%s: (%s) with %d plugins | Date: %s',
repo.name, repo.url, repo.plugins.length, new Date(repo.created * 1000).toDateString());
}
}),
]),
// Plugin management
cmd('plugin', async (...args: string[]): Promise<void> => {
logger.log('Usage: plugin install | remove | enable | disable | load | restart | kill | list | running [<name>]');
}, [
cmd('install', async (...args: string[]): Promise<void> => {
for (const name of args) {
const mf = await this.bot.repositoryManager.installPlugin(name);
logger.log('Installed plugin %s version %s!', mf.name, mf.version);
}
}),
cmd('restart', async (...args: string[]): Promise<void> => {
for (const name of args) {
const plugin = this.bot.pluginManager.getAvailableByName(name);
if (!plugin) {
throw new Error(`"${name}" is not available. Maybe try installing it? plugin install ${name}`);
}
logger.log('Scheduling restart for', name);
await this.bot.pluginManager.restart(plugin);
}
}),
cmd('remove', async (...args: string[]): Promise<void> => {
for (const name of args) {
await this.bot.repositoryManager.uninstallPlugin(name);
logger.log('Uninstalled plugin %s.', name);
}
}),
cmd('running', async (...args: string[]): Promise<void> => {
logger.log('Currently running plugins:',
this.bot.pluginManager.getLoaded().map((p) => p.manifest.name).join(', '));
}),
cmd('load', async (...args: string[]): Promise<void> => {
for (const name of args) {
const plugin = this.bot.pluginManager.getAvailableByName(name);
if (!plugin) {
throw new Error(`"${name}" is not available. Maybe try installing it? plugin install`);
}
await this.bot.pluginManager.load(plugin);
logger.log('Loaded plugin "%s" successfully.', name);
}
}),
cmd('kill', async (...args: string[]): Promise<void> => {
for (const name of args) {
if (!this.bot.pluginManager.getAvailableByName(name)) {
throw new Error('No such plugin is available.');
}
logger.log('Stopping plugin', name);
this.bot.stream.emitTo(name, 'pluginUnload', name);
}
}),
cmd('enable', async (...args: string[]): Promise<void> => {
for (const name of args) {
if (!this.bot.pluginManager.getAvailableByName(name)) {
throw new Error(`No such plugin "${name}" found.`);
}
logger.log('Enabling plugin', name);
if (!this.bot.config.config.enabled) {
this.bot.config.config.enabled = [name];
return;
}
if (this.bot.config.config.enabled.indexOf(name) === -1) {
this.bot.config.config.enabled.push(name);
}
}
await this.bot.config.save();
}),
cmd('disable', async (...args: string[]): Promise<void> => {
for (const name of args) {
if (!this.bot.config.config.enabled) {
return;
}
logger.log('Disabling plugin', name);
const indx = this.bot.config.config.enabled.indexOf(name);
if (indx > -1) {
this.bot.config.config.enabled.splice(indx, 1);
}
}
await this.bot.config.save();
}),
cmd('list', async (...args: string[]): Promise<void> => {
logger.log('Installed plugins:',
this.bot.pluginManager.availablePlugins.map((mf) => mf.name).join(', '));
}),
]),
// Channel management, with an "after" handler
new CLICommand('channel', async (...args: string[]): Promise<void> => {
logger.log('Usage: channel new | del | list | addplugin | delplugin [<name>] [<plugin>]');
}, [
cmd('new', async (...args: string[]): Promise<void> => {
if (this.bot.channelManager.getChannelByName(args[0])) {
throw new Error('A channel by that name already exists!');
}
this.bot.channelManager.addChannel({
name: args[0],
plugins: [],
enabled: true,
});
logger.log('Channel added!');
}),
cmd('remove', async (...args: string[]): Promise<void> => {
for (const name of args) {
const chan = this.bot.channelManager.getChannelByName(name);
if (!chan) {
throw new Error(`No such channel "${name}" found.`);
}
this.bot.channelManager.removeChannel(chan);
logger.log('Channel "%s" removed!', name);
}
}),
cmd('addplugin', async (...args: string[]): Promise<void> => {
const chan1 = this.bot.channelManager.getChannelByName(args[0]);
if (!chan1) {
throw new Error('No such channel exists!');
}
if (!args[1]) {
throw new Error('A plugin name is required.');
}
for (const name of args.slice(1)) {
if (chan1.plugins.indexOf(name) === -1) {
chan1.plugins.push(name);
}
logger.log('Plugin "%s" added to channel!', name);
}
}),
cmd('delplugin', async (...args: string[]): Promise<void> => {
const chan2 = this.bot.channelManager.getChannelByName(args[0]);
if (!chan2) {
throw new Error('No such channel exists!');
}
if (!args[1]) {
throw new Error('A plugin name is required.');
}
for (const name of args.slice(1)) {
const idx = chan2.plugins.indexOf(name);
if (idx !== -1) {
chan2.plugins.splice(idx, 1);
}
logger.log('Plugin "%s" removed from channel!', name);
}
}),
cmd('enable', async (...args: string[]): Promise<void> => {
for (const name of args) {
const chan = this.bot.channelManager.getChannelByName(name);
if (!chan) {
throw new Error(`No such channel "${name}" found.`);
}
chan.enabled = true;
logger.log('Channel "%s" enabled!', name);
}
}),
cmd('disable', async (...args: string[]): Promise<void> => {
for (const name of args) {
const chan = this.bot.channelManager.getChannelByName(name);
if (!chan) {
throw new Error(`No such channel "${name}" found.`);
}
chan.enabled = false;
logger.log('Channel "%s" disabled!', name);
}
}),
cmd('list', async (...args: string[]): Promise<void> => {
logger.log('Channels:\n', this.bot.channelManager.getAll().map((chan) => {
return ` => ${chan.name}: ${chan.plugins.join(', ')} (${chan.enabled ? 'enabled' : 'disabled'})`;
}).join('\n'));
}),
], async (r: any, ...args: string[]): Promise<any> => {
// Don't save when we just printed help or a list
if (!args.length || 'list'.startsWith(args[0])) {
return r;
}
// Save current channel configuration
this.bot.config.config.channels = this.bot.channelManager.getAll();
await this.bot.config.save();
return r;
}),
// Enter inspector mode. Every line entered will be executed as JavaScript.
cmd('inspector', async (...args: string[]): Promise<void> => {
this.inspector = true;
logger.warn('You have entered the JavaScript Inspector!');
console.log('Squeebot is available via "sb" or "this.bot".');
console.log('Type "exit" or "quit" to leave the inspector mode.');
}),
cmd('quit', async (...args: string[]): Promise<void> => this.bot.shutdown()),
];
constructor(private bot: Squeebot) {}
private getCommand(name: string): CLICommand | undefined {
return this.cmds.find(c => c.name.startsWith(name));
}
private async checkUpdate(repo: IRepository): Promise<void> {
const updatable = await this.bot.repositoryManager.checkForUpdates(repo);
if (updatable.length) {
logger.log('[%s] The following plugins can be updated:', repo.name,
updatable.map((u) => u.name).join(', '));
} else {
logger.log('[%s] All plugins are up-to-date!', repo.name);
}
}
private async sequentialExecute(cmds: string[][]): Promise<void> {
for (const argv of cmds) {
if (!argv[0]) {
logger.log('Available CLI Commands:', this.cmds
.map(c => `(${c.name.charAt(0)})${c.name.substring(1)}`)
.join(', '));
continue;
}
const clicmd = this.getCommand(argv[0]);
if (!clicmd) {
continue;
}
await clicmd.execute(argv.slice(1));
}
}
public attach(rl: ReadLine): void {
rl.on('line', (line: string) => {
// Inspector mode
if (this.inspector) {
if (line.startsWith('exit') || line.startsWith('quit')) {
this.inspector = false;
logger.warn('You have exited the JavaScript Inspector!');
return;
}
const sb = this.bot;
try {
// tslint:disable-next-line: no-eval
console.log(eval(line));
} catch (e: any) {
console.error(e.stack);
}
return;
}
// Executing commands in order. If previous command fails, the rest won't execute.
const executeList = line.split('&&');
const toRun = executeList.map(subline => {
return subline
.replace(/\s+/g, ' ')
.replace(/^\s+|\s+$/, '')
.split(' ')
.map(l => l.replace(',', ''));
});
this.sequentialExecute(toRun).catch(e => logger.error(e.message));
});
}
}