promisify, multi-argument interactive mode commands, readme, license

This commit is contained in:
Evert Prants 2020-11-28 22:07:20 +02:00
parent 7633dda7a5
commit 037ccb62c4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 224 additions and 112 deletions

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
The MIT License (MIT)
Copyright © 2020 Evert "Diamond" Prants <evert@lunasqu.ee>
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.

63
README.md Normal file
View File

@ -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 <name> [<path>]`
2. Execute the environment in interactive mode using `squeebotd`: `$ squeebotd -i <path>/<name>.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 `<path>/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 <name> [<path>]`
2. Build your new repository: `$ squeebot repository build <path>`
3. Your built plugins, both archived and unarchived, are in `<path>/.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 <path>`
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.

View File

@ -6,18 +6,17 @@ import { Squeebot } from './core';
export class SqueebotCLI { export class SqueebotCLI {
constructor(private bot: Squeebot) {} constructor(private bot: Squeebot) {}
private checkUpdate(repo: IRepository): void { private async checkUpdate(repo: IRepository): Promise<void> {
this.bot.repositoryManager.checkForUpdates(repo).then((updatable) => { const updatable = await this.bot.repositoryManager.checkForUpdates(repo);
if (updatable.length) { if (updatable.length) {
logger.log('[%s] The following plugins can be updated:', repo.name, logger.log('[%s] The following plugins can be updated:', repo.name,
updatable.map((u) => u.name).join(', ')); updatable.map((u) => u.name).join(', '));
} else { } else {
logger.log('[%s] All plugins are up-to-date!', repo.name); logger.log('[%s] All plugins are up-to-date!', repo.name);
} }
}, (e) => logger.error(e.message));
} }
private repositoryCommand(...args: any[]): void { private async repositoryCommand(...args: any[]): Promise<void> {
const help = 'repository add <url> | update <name> | remove <name>'; const help = 'repository add <url> | update <name> | remove <name>';
if (!args[0] || args[0] === 'help') { if (!args[0] || args[0] === 'help') {
logger.log(help); logger.log(help);
@ -34,9 +33,10 @@ export class SqueebotCLI {
return; 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); logger.log('Installed repository %s!', repo.name);
}, (e) => logger.error(e.message)); }
break; break;
case 'r': case 'r':
case 'rem': case 'rem':
@ -47,9 +47,10 @@ export class SqueebotCLI {
return; return;
} }
this.bot.repositoryManager.uninstallRepository(args[1]).then(() => { for (const namep of args.slice(1)) {
logger.log('Installed repository %s.', args[1]); await this.bot.repositoryManager.uninstallRepository(namep);
}, (e) => logger.error(e.message)); logger.log('Installed repository %s.', namep);
};
break; break;
case 'u': case 'u':
case 'upd': case 'upd':
@ -57,25 +58,27 @@ export class SqueebotCLI {
if (!args[1]) { if (!args[1]) {
const repos = this.bot.repositoryManager.getAll(); const repos = this.bot.repositoryManager.getAll();
for (const repo of repos) { for (const repo of repos) {
this.checkUpdate(repo); await this.checkUpdate(repo);
} }
return; return;
} }
const repo = this.bot.repositoryManager.getRepoByName(args[1]); for (const namep of args.slice(1)) {
const repo = this.bot.repositoryManager.getRepoByName(namep);
if (!repo) { if (!repo) {
logger.error('No such repository found.'); logger.error('No such repository "%s" found.', namep);
return; return;
} }
this.checkUpdate(repo); await this.checkUpdate(repo);
};
break; break;
default: default:
logger.log(help); logger.log(help);
} }
} }
private pluginCommand(...args: any[]): void { private async pluginCommand(...args: any[]): Promise<void> {
const help = 'plugin install | update | uninstall | enable | disable | start | stop | list | running [<name>]'; const help = 'plugin install | update | uninstall | enable | disable | start | stop | list | running [<name>]';
if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list' && args[0] !== 'running')) { if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list' && args[0] !== 'running')) {
logger.log(help); logger.log(help);
@ -87,17 +90,18 @@ export class SqueebotCLI {
case 'u': case 'u':
case 'install': case 'install':
case 'update': 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); logger.log('Installed plugin %s version %s!', mf.name, mf.version);
}, (e) => console.error(e.message)); }
break; break;
case 'uninst': case 'uninst':
case 'remove': case 'remove':
case 'uninstall': case 'uninstall':
this.bot.repositoryManager.uninstallPlugin(args[1]).then(() => { for (const name of args.slice(1)) {
logger.log('Uninstalled plugin %s.', args[1]); await this.bot.repositoryManager.uninstallPlugin(name)
}, (e) => console.error(e.message)); logger.log('Uninstalled plugin %s.', name);
}
break; break;
case 'list': case 'list':
logger.log('Installed plugins:', logger.log('Installed plugins:',
@ -111,73 +115,79 @@ export class SqueebotCLI {
case 'run': case 'run':
case 'load': case 'load':
case 'start': case 'start':
const plugin = this.bot.pluginManager.getAvailableByName(args[1]); for (const name of args.slice(1)) {
const plugin = this.bot.pluginManager.getAvailableByName(name);
if (!plugin) { if (!plugin) {
logger.error('No such plugin is available. Maybe try installing it? plugin install', args[1]); logger.error('"%s" is not available. Maybe try installing it? plugin install', name, name);
return; return;
} }
this.bot.pluginManager.load(plugin).then((p) => { await this.bot.pluginManager.load(plugin);
logger.log('Started plugin "%s" successfully.', args[1]); logger.log('Started plugin "%s" successfully.', name);
}, (e) => logger.error(e.stack)); }
break; break;
case 'stop': case 'stop':
case 'kill': case 'kill':
if (!this.bot.pluginManager.getAvailableByName(args[1])) { for (const name of args.slice(1)) {
if (!this.bot.pluginManager.getAvailableByName(name)) {
logger.error('No such plugin is available.'); logger.error('No such plugin is available.');
return; return;
} }
logger.log('Stopping plugin', args[1]); logger.log('Stopping plugin', name);
this.bot.stream.emitTo(args[1], 'pluginUnload', args[1]); this.bot.stream.emitTo(name, 'pluginUnload', name);
}
break; break;
case 'enable': case 'enable':
if (!this.bot.pluginManager.getAvailableByName(args[1])) { for (const name of args.slice(1)) {
logger.error('No such plugin is available.'); if (!this.bot.pluginManager.getAvailableByName(name)) {
logger.error('No such plugin "%s" is available.', name);
return; return;
} }
logger.log('Enabling plugin', args[1]); logger.log('Enabling plugin', name);
if (!this.bot.config.config.enabled) { if (!this.bot.config.config.enabled) {
this.bot.config.config.enabled = [args[1]]; this.bot.config.config.enabled = [name];
return; return;
} }
if (this.bot.config.config.enabled.indexOf(args[1]) === -1) { if (this.bot.config.config.enabled.indexOf(name) === -1) {
this.bot.config.config.enabled.push(args[1]); this.bot.config.config.enabled.push(name);
}
} }
this.bot.config.save(); await this.bot.config.save();
break; break;
case 'disable': case 'disable':
if (!this.bot.pluginManager.getAvailableByName(args[1])) { for (const name of args.slice(1)) {
logger.error('No such plugin is available.'); if (!this.bot.pluginManager.getAvailableByName(name)) {
logger.error('No such plugin "%s" is available.', name);
return; return;
} }
logger.log('Disabling plugin', args[1]); logger.log('Disabling plugin', name);
if (!this.bot.config.config.enabled) { if (!this.bot.config.config.enabled) {
return; return;
} }
const indx = this.bot.config.config.enabled.indexOf(args[1]); const indx = this.bot.config.config.enabled.indexOf(name);
if (indx > -1) { if (indx > -1) {
this.bot.config.config.enabled.splice(indx, 1); this.bot.config.config.enabled.splice(indx, 1);
} }
}
this.bot.config.save(); await this.bot.config.save();
break; break;
default: default:
logger.log(help); logger.log(help);
} }
} }
private channelCommand(...args: any[]): void { private async channelCommand(...args: any[]): Promise<void> {
const help = 'channel new | del | addplugin | delplugin [<name>] [<plugin>]'; const help = 'channel new | del | list | addplugin | delplugin [<name>] [<plugin>]';
if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list')) { if (!args[0] || args[0] === 'help' || (!args[1] && args[0] !== 'list')) {
logger.log(help); logger.log(help);
return; return;
@ -201,15 +211,17 @@ export class SqueebotCLI {
case 'del': case 'del':
case 'delete': case 'delete':
case 'remove': case 'remove':
const chan = this.bot.channelManager.getChannelByName(args[1]); for (const name of args.slice(1)) {
const chan = this.bot.channelManager.getChannelByName(name);
if (!chan) { if (!chan) {
logger.error('No such channel exists!'); logger.error('No such channel "%s" exists!', name);
return; return;
} }
this.bot.channelManager.removeChannel(chan); this.bot.channelManager.removeChannel(chan);
logger.log('Channel removed!'); logger.log('Channel "%s" removed!', name);
}
break; break;
case 'addp': case 'addp':
case 'addplugin': case 'addplugin':
@ -224,11 +236,18 @@ export class SqueebotCLI {
return; return;
} }
if (chan1.plugins.indexOf(args[2]) !== -1) { for (const name of args.slice(2)) {
chan1.plugins.push(args[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; break;
case 'remp': case 'remp':
case 'delp': case 'delp':
@ -245,37 +264,43 @@ export class SqueebotCLI {
return; return;
} }
for (const name of args.slice(2)) {
const idx = chan2.plugins.indexOf(args[2]); const idx = chan2.plugins.indexOf(args[2]);
if (idx !== -1) { if (idx !== -1) {
chan2.plugins.splice(idx, 1); chan2.plugins.splice(idx, 1);
} }
logger.log('Plugin "%s" added to channel!', name);
}
logger.log('Plugin added to channel!');
break; break;
case 'enable': case 'enable':
const chan3 = this.bot.channelManager.getChannelByName(args[1]); for (const name of args.slice(1)) {
if (!chan3) { const chan = this.bot.channelManager.getChannelByName(name);
logger.error('No such channel exists!'); if (!chan) {
logger.error('No such channel "%s" exists!', name);
return; return;
} }
chan3.enabled = true; chan.enabled = true;
logger.log('Channel enabled!'); logger.log('Channel "%s" enabled!', name);
}
break; break;
case 'disable': case 'disable':
const chan4 = this.bot.channelManager.getChannelByName(args[1]); for (const name of args.slice(1)) {
if (!chan4) { const chan = this.bot.channelManager.getChannelByName(name);
logger.error('No such channel exists!'); if (!chan) {
logger.error('No such channel "%s" exists!', name);
return; return;
} }
chan4.enabled = false; chan.enabled = false;
logger.log('Channel disabled!'); logger.log('Channel "%s" disabled!', name);
}
break; break;
} }
this.bot.config.config.channels = this.bot.channelManager.getAll(); this.bot.config.config.channels = this.bot.channelManager.getAll();
this.bot.config.save(); await this.bot.config.save();
} }
public attach(rl: ReadLine) { public attach(rl: ReadLine) {
@ -290,17 +315,20 @@ export class SqueebotCLI {
case 'r': case 'r':
case 'repo': case 'repo':
case 'repository': case 'repository':
this.repositoryCommand(...split.slice(1)); this.repositoryCommand(...split.slice(1)).catch(
e => logger.error(e.message));
break; break;
case 'p': case 'p':
case 'pl': case 'pl':
case 'plugin': case 'plugin':
this.pluginCommand(...split.slice(1)); this.pluginCommand(...split.slice(1)).catch(
e => logger.error(e.message));
break; break;
case 'c': case 'c':
case 'chan': case 'chan':
case 'channel': case 'channel':
this.channelCommand(...split.slice(1)); this.channelCommand(...split.slice(1)).catch(
e => logger.error(e.message));
break; break;
case 'stop': case 'stop':
case 'exit': case 'exit':
@ -309,6 +337,6 @@ export class SqueebotCLI {
this.bot.shutdown(); this.bot.shutdown();
break; break;
} }
}) });
} }
} }

View File

@ -26,6 +26,7 @@ const tsConfig = {
}; };
const gitignore = `/node_modules/ const gitignore = `/node_modules/
/.out/
deployment.json`; deployment.json`;
function dummyEnvironment(location: string): IEnvironment { function dummyEnvironment(location: string): IEnvironment {
@ -108,6 +109,7 @@ async function newRepository(
await fs.writeJson(path.join(location, 'tsconfig.json'), tsConfig); await fs.writeJson(path.join(location, 'tsconfig.json'), tsConfig);
gitIgnore += '\n*.js'; gitIgnore += '\n*.js';
gitIgnore += '\n*.d.ts'; gitIgnore += '\n*.d.ts';
gitIgnore += '\n*.tsbuildinfo';
console.log('Adding TypeScript scripts to package.json'); console.log('Adding TypeScript scripts to package.json');
const pkgjson = path.join(location, 'package.json'); const pkgjson = path.join(location, 'package.json');