diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1a552e4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Copyright © 2020 Evert "Diamond" Prants + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the “Software”), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd5bc07 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Squeebot CLI +This package provides the runtime and tooling for Squeebot 3.x.x! +This package provides two binaries via npm: `squeebot` and `squeebotd`. + +## Running Squeebot +1. Create an environment: `$ squeebot new []` +2. Execute the environment in interactive mode using `squeebotd`: `$ squeebotd -i /.json` +3. Install plugins (documented below) + +`squeebotd` just takes the path to the generated json file and creates all other necessary files and directories by itself. +The primary configuration will be located in `/configs/squeebot.json`. + +## Installing plugins +In order to install plugins, you have to add a repository. Repositories are JSON files served over HTTP. + +For example, installing core plugins (Interactive mode `-i` on `squeebotd`): +1. `repository install https://(TODO)/repository.json` +2. `plugin install control mqtt` +3. `plugin list` + +## Interactive mode commands +The following commands are available when starting in interactive mode: +* `repository help` +* `plugin help` +* `channel help` +* `quit` + +## Creating a repository +In order to create a repository, you need to do the following: +1. Create a new repository: `$ squeebot repository new []` +2. Build your new repository: `$ squeebot repository build ` +3. Your built plugins, both archived and unarchived, are in `/.out`. + +Repositories are created with a TypeScript build environment by default. If you do not wish to use TypeScript (not recommended) for your plugins, +add the `-t` flag to `squeebot repository new` command. + +The build command supports a `-w` argument which will execute the entire build command again when you make changes (watch mode). +**Including deployments, if `-d` is present!** + +### Creating plugins +Within your new repository, each directory you make will be a plugin. The plugin **must** contain the following: +1. `plugin.json` - Manifest for your plugin. +2. `plugin.js` - Has to be a JavaScript file which exports a class inherited from `Plugin` at `@squeebot/core/lib/plugin`. + +**Note:** `plugin.js` only has to exist in the distribution, so `.ts` is fine, but you have to use `$ squeebot repository build ` +to build them into JavaScript files. + +Plugin manifest (`plugin.json`) example: +``` +{ + "name": "plugin-name", // The name of your plugin, must match the name of the directory + "version": "0.0.0", // The version of your plugin, must be semantic versioning! + "description": "", // Optional description for this plugin + "dependencies": [], // List of plugins this plugin depends on + "npmDependencies": [], // List of npm modules this plugin depends on. Supports versions, example 'thing@1.0.0' +} +``` + +### Deploying the repository +You can configure deployment options using a file called `deployment.json` in your repository root. Currently supported deployment methods: +1. TODO! + +In order to activate your configured deployment, use the `-d` flag when building your repository. `-o` flag skips build and deploys immediately. \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index fb4f569..6d71ebf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,18 +6,17 @@ import { Squeebot } from './core'; export class SqueebotCLI { constructor(private bot: Squeebot) {} - private checkUpdate(repo: IRepository): void { - this.bot.repositoryManager.checkForUpdates(repo).then((updatable) => { - 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); - } - }, (e) => logger.error(e.message)); + private async checkUpdate(repo: IRepository): Promise { + 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 repositoryCommand(...args: any[]): void { + private async repositoryCommand(...args: any[]): Promise { const help = 'repository add | update | remove '; if (!args[0] || args[0] === 'help') { logger.log(help); @@ -34,9 +33,10 @@ export class SqueebotCLI { return; } - this.bot.repositoryManager.installRepository(args[1]).then((repo) => { + for (const urlp of args.slice(1)) { + const repo = await this.bot.repositoryManager.installRepository(urlp); logger.log('Installed repository %s!', repo.name); - }, (e) => logger.error(e.message)); + } break; case 'r': case 'rem': @@ -47,9 +47,10 @@ export class SqueebotCLI { return; } - this.bot.repositoryManager.uninstallRepository(args[1]).then(() => { - logger.log('Installed repository %s.', args[1]); - }, (e) => logger.error(e.message)); + for (const namep of args.slice(1)) { + await this.bot.repositoryManager.uninstallRepository(namep); + logger.log('Installed repository %s.', namep); + }; break; case 'u': case 'upd': @@ -57,25 +58,27 @@ export class SqueebotCLI { if (!args[1]) { const repos = this.bot.repositoryManager.getAll(); for (const repo of repos) { - this.checkUpdate(repo); + await this.checkUpdate(repo); } return; } - const repo = this.bot.repositoryManager.getRepoByName(args[1]); - if (!repo) { - logger.error('No such repository found.'); - return; - } + for (const namep of args.slice(1)) { + const repo = this.bot.repositoryManager.getRepoByName(namep); + if (!repo) { + logger.error('No such repository "%s" found.', namep); + return; + } - this.checkUpdate(repo); + await this.checkUpdate(repo); + }; break; default: logger.log(help); } } - private pluginCommand(...args: any[]): void { + private async pluginCommand(...args: any[]): Promise { const help = 'plugin install | update | uninstall | enable | disable | start | stop | list | running []'; if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list' && args[0] !== 'running')) { logger.log(help); @@ -87,17 +90,18 @@ export class SqueebotCLI { case 'u': case 'install': case 'update': - this.bot.repositoryManager.installPlugin(args[1]).then((mf) => { + for (const name of args.slice(1)) { + const mf = await this.bot.repositoryManager.installPlugin(name); logger.log('Installed plugin %s version %s!', mf.name, mf.version); - }, (e) => console.error(e.message)); - + } break; case 'uninst': case 'remove': case 'uninstall': - this.bot.repositoryManager.uninstallPlugin(args[1]).then(() => { - logger.log('Uninstalled plugin %s.', args[1]); - }, (e) => console.error(e.message)); + for (const name of args.slice(1)) { + await this.bot.repositoryManager.uninstallPlugin(name) + logger.log('Uninstalled plugin %s.', name); + } break; case 'list': logger.log('Installed plugins:', @@ -111,73 +115,79 @@ export class SqueebotCLI { case 'run': case 'load': case 'start': - const plugin = this.bot.pluginManager.getAvailableByName(args[1]); - if (!plugin) { - logger.error('No such plugin is available. Maybe try installing it? plugin install', args[1]); - return; + for (const name of args.slice(1)) { + const plugin = this.bot.pluginManager.getAvailableByName(name); + if (!plugin) { + logger.error('"%s" is not available. Maybe try installing it? plugin install', name, name); + return; + } + + await this.bot.pluginManager.load(plugin); + logger.log('Started plugin "%s" successfully.', name); } - - this.bot.pluginManager.load(plugin).then((p) => { - logger.log('Started plugin "%s" successfully.', args[1]); - }, (e) => logger.error(e.stack)); - break; case 'stop': case 'kill': - if (!this.bot.pluginManager.getAvailableByName(args[1])) { - logger.error('No such plugin is available.'); - return; - } + for (const name of args.slice(1)) { + if (!this.bot.pluginManager.getAvailableByName(name)) { + logger.error('No such plugin is available.'); + return; + } - logger.log('Stopping plugin', args[1]); - - this.bot.stream.emitTo(args[1], 'pluginUnload', args[1]); + logger.log('Stopping plugin', name); + + this.bot.stream.emitTo(name, 'pluginUnload', name); + } break; case 'enable': - if (!this.bot.pluginManager.getAvailableByName(args[1])) { - logger.error('No such plugin is available.'); - return; + for (const name of args.slice(1)) { + if (!this.bot.pluginManager.getAvailableByName(name)) { + logger.error('No such plugin "%s" is available.', name); + return; + } + + 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); + } } - logger.log('Enabling plugin', args[1]); - - if (!this.bot.config.config.enabled) { - this.bot.config.config.enabled = [args[1]]; - return; - } - - if (this.bot.config.config.enabled.indexOf(args[1]) === -1) { - this.bot.config.config.enabled.push(args[1]); - } - - this.bot.config.save(); + await this.bot.config.save(); break; case 'disable': - if (!this.bot.pluginManager.getAvailableByName(args[1])) { - logger.error('No such plugin is available.'); - return; + for (const name of args.slice(1)) { + if (!this.bot.pluginManager.getAvailableByName(name)) { + logger.error('No such plugin "%s" is available.', name); + return; + } + + logger.log('Disabling plugin', name); + + if (!this.bot.config.config.enabled) { + return; + } + + const indx = this.bot.config.config.enabled.indexOf(name); + if (indx > -1) { + this.bot.config.config.enabled.splice(indx, 1); + } } - logger.log('Disabling plugin', args[1]); - - if (!this.bot.config.config.enabled) { - return; - } - - const indx = this.bot.config.config.enabled.indexOf(args[1]); - if (indx > -1) { - this.bot.config.config.enabled.splice(indx, 1); - } - - this.bot.config.save(); + await this.bot.config.save(); break; default: logger.log(help); } } - private channelCommand(...args: any[]): void { - const help = 'channel new | del | addplugin | delplugin [] []'; + private async channelCommand(...args: any[]): Promise { + const help = 'channel new | del | list | addplugin | delplugin [] []'; if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list')) { logger.log(help); return; @@ -201,15 +211,17 @@ export class SqueebotCLI { case 'del': case 'delete': case 'remove': - const chan = this.bot.channelManager.getChannelByName(args[1]); - if (!chan) { - logger.error('No such channel exists!'); - return; + for (const name of args.slice(1)) { + const chan = this.bot.channelManager.getChannelByName(name); + if (!chan) { + logger.error('No such channel "%s" exists!', name); + return; + } + + this.bot.channelManager.removeChannel(chan); + + logger.log('Channel "%s" removed!', name); } - - this.bot.channelManager.removeChannel(chan); - - logger.log('Channel removed!'); break; case 'addp': case 'addplugin': @@ -224,11 +236,18 @@ export class SqueebotCLI { return; } - if (chan1.plugins.indexOf(args[2]) !== -1) { - chan1.plugins.push(args[2]); - } + for (const name of args.slice(2)) { + if (chan1.plugins.indexOf(name) === -1) { + chan1.plugins.push(name); + } - logger.log('Plugin added to channel!'); + logger.log('Plugin "%s" added to channel!', name); + } + break; + case 'list': + logger.log('Channels:\n', this.bot.channelManager.getAll().map((chan) => { + return ` => ${chan.name}: ${chan.plugins.join(', ')} (${chan.enabled ? 'enabled' : 'disabled'})`; + }).join('\n')); break; case 'remp': case 'delp': @@ -245,37 +264,43 @@ export class SqueebotCLI { return; } - const idx = chan2.plugins.indexOf(args[2]); - if (idx !== -1) { - chan2.plugins.splice(idx, 1); + for (const name of args.slice(2)) { + const idx = chan2.plugins.indexOf(args[2]); + if (idx !== -1) { + chan2.plugins.splice(idx, 1); + } + logger.log('Plugin "%s" added to channel!', name); } - logger.log('Plugin added to channel!'); break; case 'enable': - const chan3 = this.bot.channelManager.getChannelByName(args[1]); - if (!chan3) { - logger.error('No such channel exists!'); - return; - } + for (const name of args.slice(1)) { + const chan = this.bot.channelManager.getChannelByName(name); + if (!chan) { + logger.error('No such channel "%s" exists!', name); + return; + } - chan3.enabled = true; - logger.log('Channel enabled!'); + chan.enabled = true; + logger.log('Channel "%s" enabled!', name); + } break; case 'disable': - const chan4 = this.bot.channelManager.getChannelByName(args[1]); - if (!chan4) { - logger.error('No such channel exists!'); - return; - } + for (const name of args.slice(1)) { + const chan = this.bot.channelManager.getChannelByName(name); + if (!chan) { + logger.error('No such channel "%s" exists!', name); + return; + } - chan4.enabled = false; - logger.log('Channel disabled!'); + chan.enabled = false; + logger.log('Channel "%s" disabled!', name); + } break; } this.bot.config.config.channels = this.bot.channelManager.getAll(); - this.bot.config.save(); + await this.bot.config.save(); } public attach(rl: ReadLine) { @@ -290,17 +315,20 @@ export class SqueebotCLI { case 'r': case 'repo': case 'repository': - this.repositoryCommand(...split.slice(1)); + this.repositoryCommand(...split.slice(1)).catch( + e => logger.error(e.message)); break; case 'p': case 'pl': case 'plugin': - this.pluginCommand(...split.slice(1)); + this.pluginCommand(...split.slice(1)).catch( + e => logger.error(e.message)); break; case 'c': case 'chan': case 'channel': - this.channelCommand(...split.slice(1)); + this.channelCommand(...split.slice(1)).catch( + e => logger.error(e.message)); break; case 'stop': case 'exit': @@ -309,6 +337,6 @@ export class SqueebotCLI { this.bot.shutdown(); break; } - }) + }); } } \ No newline at end of file diff --git a/src/squeebot.ts b/src/squeebot.ts index cb17689..9af29e5 100644 --- a/src/squeebot.ts +++ b/src/squeebot.ts @@ -26,6 +26,7 @@ const tsConfig = { }; const gitignore = `/node_modules/ +/.out/ deployment.json`; function dummyEnvironment(location: string): IEnvironment { @@ -108,6 +109,7 @@ async function newRepository( await fs.writeJson(path.join(location, 'tsconfig.json'), tsConfig); gitIgnore += '\n*.js'; gitIgnore += '\n*.d.ts'; + gitIgnore += '\n*.tsbuildinfo'; console.log('Adding TypeScript scripts to package.json'); const pkgjson = path.join(location, 'package.json');