From 1ec3870ad78857079e9c2c4b86b61979c76b6b34 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 19 Sep 2021 01:20:34 +0300 Subject: [PATCH] split squeebot executable --- README.md | 32 +- package-lock.json | 84 ++--- package.json | 4 +- src/build/default/environment.default.ts | 12 + src/build/default/gitignore.default.ts | 4 + src/build/default/tsconfig.default.ts | 12 + src/build/environment.ts | 32 ++ src/build/execute.ts | 24 ++ src/build/repository/build.ts | 130 +++++++ src/build/repository/create.ts | 69 ++++ src/build/repository/deploy.ts | 45 +++ src/build/repository/deployment/develop.ts | 46 +++ src/build/repository/deployment/ssh.ts | 46 +++ src/build/watch.ts | 57 +++ src/core.ts | 10 +- src/squeebot.ts | 393 +-------------------- src/squeebotd.ts | 6 +- 17 files changed, 544 insertions(+), 462 deletions(-) create mode 100644 src/build/default/environment.default.ts create mode 100644 src/build/default/gitignore.default.ts create mode 100644 src/build/default/tsconfig.default.ts create mode 100644 src/build/environment.ts create mode 100644 src/build/execute.ts create mode 100644 src/build/repository/build.ts create mode 100644 src/build/repository/create.ts create mode 100644 src/build/repository/deploy.ts create mode 100644 src/build/repository/deployment/develop.ts create mode 100644 src/build/repository/deployment/ssh.ts create mode 100644 src/build/watch.ts diff --git a/README.md b/README.md index 2967d49..cc707c2 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The primary configuration will be located in `/configs/squeebot.json`. 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` +1. `repository install https://squeebot.lunasqu.ee/pkg/plugins-core/repository.json` +2. `plugin install control simplecommands` 3. `plugin list` ## Interactive mode commands @@ -58,7 +58,31 @@ Plugin manifest (`plugin.json`) example: ``` ### Deploying the repository -You can configure deployment options using a file called `deployment.json` in your repository root. Currently supported deployment methods: -1. TODO! +You can configure deployment options using a file called `deployment.json` in your repository root. In order to activate your configured deployment, use the `-d` flag when building your repository. `-o` flag skips build and deploys immediately. + +#### Development +``` +{ + "devSqueebot": ".json", +} +``` + +Use `-d dev` to deploy this. + +#### SSH deployment +``` +{ + "prod": { + "ssh": true, + "key": "", + "host": "", + "user": "", + "target": "" + } +} +``` + +## Official repositories +All official repositories [are here](https://gitlab.icynet.eu/Squeebot/official-plugins). Production-ready builds are available to download via Squeebot CLI from `https://squeebot.lunasqu.ee/pkg//repository.json` by using the `repository install` command. diff --git a/package-lock.json b/package-lock.json index 75f978d..6d21673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@squeebot/cli", - "version": "3.1.1", + "version": "3.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@squeebot/cli", - "version": "3.1.1", + "version": "3.4.0", "license": "MIT", "dependencies": { - "@squeebot/core": "^3.3.1", + "@squeebot/core": "^3.3.3", "fs-extra": "^10.0.0", "node-watch": "^0.7.1", "tar": "^6.1.11", @@ -76,28 +76,14 @@ } }, "node_modules/@squeebot/core": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@squeebot/core/-/core-3.3.1.tgz", - "integrity": "sha512-pkLMbZ0ZLC0isBlGbOCyu28NalBOjmwAkC0RmPN19DysohEF7XBdMMBss72IT3fXeAK6mEKO7pHZS4XAHVZa0Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@squeebot/core/-/core-3.3.3.tgz", + "integrity": "sha512-JKmHXAl2E3nXk5lE96DVyLFp/oQXu7zQb0ZcNuL6p5tdzjJW2f9GMzKYkc/6UxqijqRuwX0Km5qcSBYkp1JRJg==", "dependencies": { - "dateformat": "^4.0.0", - "fs-extra": "^9.0.1", - "semver": "^7.3.2", - "tar": "^6.0.5" - } - }, - "node_modules/@squeebot/core/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" + "dateformat": "^4.5.1", + "fs-extra": "^10.0.0", + "semver": "^7.3.5", + "tar": "^6.1.11" } }, "node_modules/@squeebot/core/node_modules/semver": { @@ -192,14 +178,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -575,9 +553,9 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "node_modules/require-directory": { @@ -852,27 +830,16 @@ } }, "@squeebot/core": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@squeebot/core/-/core-3.3.1.tgz", - "integrity": "sha512-pkLMbZ0ZLC0isBlGbOCyu28NalBOjmwAkC0RmPN19DysohEF7XBdMMBss72IT3fXeAK6mEKO7pHZS4XAHVZa0Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@squeebot/core/-/core-3.3.3.tgz", + "integrity": "sha512-JKmHXAl2E3nXk5lE96DVyLFp/oQXu7zQb0ZcNuL6p5tdzjJW2f9GMzKYkc/6UxqijqRuwX0Km5qcSBYkp1JRJg==", "requires": { - "dateformat": "^4.0.0", - "fs-extra": "^9.0.1", - "semver": "^7.3.2", - "tar": "^6.0.5" + "dateformat": "^4.5.1", + "fs-extra": "^10.0.0", + "semver": "^7.3.5", + "tar": "^6.1.11" }, "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -955,11 +922,6 @@ "sprintf-js": "~1.0.2" } }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1262,9 +1224,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "require-directory": { diff --git a/package.json b/package.json index 2dfff9e..f3d371c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@squeebot/cli", - "version": "3.3.2", + "version": "3.4.0", "description": "Squeebot v3 runtime, environments and configuration", "main": "dist/squeebot.js", "bin": { @@ -29,7 +29,7 @@ "typescript": "^4.4.2" }, "dependencies": { - "@squeebot/core": "^3.3.1", + "@squeebot/core": "^3.3.3", "fs-extra": "^10.0.0", "node-watch": "^0.7.1", "tar": "^6.1.11", diff --git a/src/build/default/environment.default.ts b/src/build/default/environment.default.ts new file mode 100644 index 0000000..1f28b25 --- /dev/null +++ b/src/build/default/environment.default.ts @@ -0,0 +1,12 @@ +import { IEnvironment } from '@squeebot/core/lib/types'; +import path from 'path'; + +export function defaultEnvironment(location: string): IEnvironment { + return { + configurationPath: path.join(location, 'plugins'), + environment: 'development', + path: location, + pluginsPath: path.join(location, 'plugins'), + repositoryPath: path.join(location, 'plugins'), + }; +} diff --git a/src/build/default/gitignore.default.ts b/src/build/default/gitignore.default.ts new file mode 100644 index 0000000..e53070c --- /dev/null +++ b/src/build/default/gitignore.default.ts @@ -0,0 +1,4 @@ + +export const gitignore = `/node_modules/ +/.out/ +deployment.json`; diff --git a/src/build/default/tsconfig.default.ts b/src/build/default/tsconfig.default.ts new file mode 100644 index 0000000..652d68d --- /dev/null +++ b/src/build/default/tsconfig.default.ts @@ -0,0 +1,12 @@ +export const tsConfig = { + compilerOptions: { + downlevelIteration: true, + esModuleInterop: true, + experimentalDecorators: true, + forceConsistentCasingInFileNames: true, + skipLibCheck: true, + sourceMap: false, + strict: true, + target: 'es5', + }, +}; diff --git a/src/build/environment.ts b/src/build/environment.ts new file mode 100644 index 0000000..8d97d5c --- /dev/null +++ b/src/build/environment.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import fs from 'fs-extra'; + +/** + * Initialize a Squeebot environment + * @param name Environment name + * @param location Install path + */ +export async function newEnvironment( + name?: string, + location?: string +): Promise { + if (!name) { + name = 'squeebot'; + } + + if (!location) { + location = name; + } + + location = path.resolve(process.cwd(), location); + + const envFile = path.join(location, name + '.json'); + await fs.ensureDir(location); + await fs.writeJson(envFile, { + environment: 'production', + path: location, + }); + + console.log('\nNew environment file:\t', envFile); + console.log('Environment path:\t', location); +} diff --git a/src/build/execute.ts b/src/build/execute.ts new file mode 100644 index 0000000..5b5cc7c --- /dev/null +++ b/src/build/execute.ts @@ -0,0 +1,24 @@ +import child_process from 'child_process'; + +/** + * Execute a shell command + * @param cmd Shell command + * @param loc Working directory + */ +export function execute(cmd: any[], loc: string): Promise { + return new Promise((resolve, reject) => { + const cproc = child_process.spawn(cmd[0], cmd.slice(1), { + cwd: loc, + }); + + cproc.stdout.pipe(process.stdout); + cproc.stderr.pipe(process.stderr); + + cproc.on('exit', (code: number) => { + if (code !== 0) { + return reject(new Error('Build failed!')); + } + resolve(); + }); + }); +} diff --git a/src/build/repository/build.ts b/src/build/repository/build.ts new file mode 100644 index 0000000..396f17d --- /dev/null +++ b/src/build/repository/build.ts @@ -0,0 +1,130 @@ +import { PluginMetaLoader } from '@squeebot/core/lib/plugin'; +import fs from 'fs-extra'; +import path from 'path'; +import tar from 'tar'; + +import { defaultEnvironment } from '../default/environment.default'; +import { execute } from '../execute'; +import { deploy } from './deploy'; + +/** + * Build repository ready for deployment + * @param location Repository location + * @param out Create output files for deployment (false - only compile tsc in-place) + * @param doDeploy Deployment name + * @param onlyDeploy Deploy only without building + */ +export async function buildRepository( + location?: string, + out = true, + doDeploy?: string, + onlyDeploy = false +): Promise { + + if (!location) { + location = process.cwd(); + } + + location = path.resolve(process.cwd(), location); + const outDir = path.join(location, '.out'); + + // Check for repository metadata + const buildMetaFile = path.join(location, 'squeebot.repo.json'); + if (!await fs.pathExists(buildMetaFile)) { + throw new Error(`${location} is not a valid squeebot repository development environment!`); + } + + // Read repository metadata + const meta = await fs.readJson(buildMetaFile); + const env = defaultEnvironment(location); + env.pluginsPath = location; + + if (!meta.name) { + throw new Error(`${location} is not a valid squeebot repository development environment!`); + } + + console.log('Detected repository "%s"!', meta.name); + + if (onlyDeploy) { + if (!await fs.pathExists(outDir)) { + throw new Error(`You need to build before deploying!`); + } else { + return deploy(meta.name, location, outDir, doDeploy as string); + } + } + + // Run typescript build + if (meta.typescript) { + console.log('Running build task..'); + await execute(['npm', 'run', 'build'], location); + } + + console.log('Detecting plugins in this environment..'); + + const loader = new PluginMetaLoader(env); + const plugins = await loader.loadAll(false); + + console.log('Found the following plugins:', plugins.map((plugin) => { + return `${plugin.name}@${plugin.version}`; + }).join(', ')); + + const savedList = plugins.map((plugin) => ({ + name: plugin.name, + version: plugin.version, + })); + + meta.plugins = savedList; + await fs.writeJson(buildMetaFile, meta, { spaces: 2 }); + + if (!out) { + console.log('Done!'); + return; + } + + console.log('Creating repository index'); + await fs.remove(outDir); + await fs.ensureDir(outDir); + await fs.writeJSON(path.join(outDir, 'repository.json'), { + created: Math.floor(Date.now() / 1000), + name: meta.name, + plugins: meta.plugins, + schema: 1, + }); + + console.log('Copying plugins'); + for (const plugin of plugins) { + const src = path.join(location, plugin.name); + const dst = path.join(outDir, plugin.name); + await fs.copy(src, dst); + } + + if (meta.typescript) { + console.log('Stripping source files'); + for (const plugin of plugins) { + const plOut = path.join(outDir, plugin.name); + const listAllFiles = await fs.readdir(plOut); + for (const f of listAllFiles) { + if (f.match(/(\.d)?\.ts$/i) != null) { + await fs.remove(path.join(plOut, f)); + } + } + } + } + + console.log('Creating tarballs'); + for (const plugin of plugins) { + const plOut = path.join(outDir, plugin.name); + await tar.c({ + gzip: true, + file: plOut + '.plugin.tgz', + C: outDir, + }, [plugin.name]); + } + + if (doDeploy == null) { + console.log('Done!'); + return; + } + + deploy(meta.name, location, outDir, doDeploy); +} diff --git a/src/build/repository/create.ts b/src/build/repository/create.ts new file mode 100644 index 0000000..12fef7b --- /dev/null +++ b/src/build/repository/create.ts @@ -0,0 +1,69 @@ +import path from 'path'; +import fs from 'fs-extra'; + +import { NPMExecutor } from '@squeebot/core/lib/npm'; + +import { defaultEnvironment } from '../default/environment.default'; +import { gitignore } from '../default/gitignore.default'; +import { tsConfig } from '../default/tsconfig.default'; + +/** + * Create a new repository, complete with typescript and git. + * @param name Repository name + * @param location Repository path + * @param typescript Use typescript + */ +export async function newRepository( + name: string, + location?: string, + typescript = true): Promise { + if (!name) { + throw new Error('Repository needs a name!'); + } + + if (!location) { + location = name; + } + + location = path.resolve(process.cwd(), location); + console.log('Creating a new repository development environment at', location); + + await fs.ensureDir(location); + const env = defaultEnvironment(location); + + console.log('Creating package.json and installing base requirements'); + const executor = new NPMExecutor(env, '@squeebot/core'); + await executor.loadPackageFile(); + + let gitIgnore = gitignore + ''; + + if (typescript) { + console.log('Installing TypeScript support'); + await executor.installPackage('typescript'); + 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'); + const contents = await fs.readJson(pkgjson); + contents.scripts = { + build: 'tsc', + watch: 'tsc -w', + }; + await fs.writeJson(pkgjson, contents, { spaces: 2 }); + } + + console.log('Creating .gitignore'); + await fs.writeFile(path.join(location, '.gitignore'), gitIgnore); + + console.log('Writing build metadata'); + await fs.writeJson(path.join(location, 'squeebot.repo.json'), { + name, + plugins: [], + typescript, + }, { spaces: 2 }); + + console.log('\nDone! Your repository "%s" lives at:', name, location); +} diff --git a/src/build/repository/deploy.ts b/src/build/repository/deploy.ts new file mode 100644 index 0000000..f4a6e1c --- /dev/null +++ b/src/build/repository/deploy.ts @@ -0,0 +1,45 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { sshDeploy } from './deployment/ssh'; +import { developDeploy } from './deployment/develop'; + +/** + * Deploy according to configuration + * @param name Repository name + * @param location Repository path + * @param outDir Output directory + * @param deployment Deployment name + */ +export async function deploy( + name: string, + location: string, + outDir: string, + deployment: string): Promise { + const deployFile = path.join(location, 'deployment.json'); + + if (!await fs.pathExists(deployFile)) { + throw new Error('deployment.json file is missing! Can\'t deploy!'); + } + + const meta = await fs.readJson(deployFile); + + // If we have a path to a metadata file, we copy the output directory to + // the plugins directory, overwriting existing plugins with the same name! + if ((deployment.indexOf('dev') === 0 || deployment.indexOf('squee') === 0) + && meta.devSqueebot) { + console.log('\n [!!!] DEPLOYING REPOSITORY FOR DEVELOPMENT [!!!]\n'); + developDeploy(location, meta, outDir); + return; + } + + // Other deployment methods come here + if (deployment in meta) { + console.log('\n [!!!] DEPLOYING REPOSITORY [!!!]\n'); + const dpl = meta[deployment]; + if (dpl.ssh) { + await sshDeploy(outDir, dpl); + } + } +} + diff --git a/src/build/repository/deployment/develop.ts b/src/build/repository/deployment/develop.ts new file mode 100644 index 0000000..3719ddb --- /dev/null +++ b/src/build/repository/deployment/develop.ts @@ -0,0 +1,46 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { loadEnvironment } from '@squeebot/core/lib/core'; + +/** + * Deploy to a local environment + * @param location Repository location + * @param meta Deployment metadata + * @param outDir Repository output files + */ +export async function developDeploy( + location: string, + meta: Record, + outDir: string, +): Promise { + console.log('Deploying to a configured Squeebot development environment'); + const devbotPath = path.resolve(location, meta.devSqueebot as string); + + console.log('Path:', devbotPath); + + if (!await fs.pathExists(devbotPath)) { + throw new Error('Development Squeebot environment file doesn\'t exist!'); + } + + const devEnv = await loadEnvironment(devbotPath); + if (!devEnv.pluginsPath) { + throw new Error('Bad development Squeebot environment file!'); + } + + const pluginsPath = path.resolve(devEnv.path, devEnv.pluginsPath); + + console.log('Copying plugins to', pluginsPath); + const listAllFiles = await fs.readdir(outDir); + for (const f of listAllFiles) { + if (f === 'repository' || f.indexOf('.tgz') !== -1) { + continue; + } + const dst = path.join(pluginsPath, f); + await fs.copy(path.join(outDir, f), dst, { + overwrite: true, + }); + } + + console.log('Done!'); +} diff --git a/src/build/repository/deployment/ssh.ts b/src/build/repository/deployment/ssh.ts new file mode 100644 index 0000000..5a3853d --- /dev/null +++ b/src/build/repository/deployment/ssh.ts @@ -0,0 +1,46 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { execute } from '../../execute'; + +/** + * Deploy using ssh and rsync + * @param outDir Repoistory output + * @param sshconf ssh deployment configuration + */ +export async function sshDeploy( + outDir: string, + sshconf: any +): Promise { + + const key = sshconf.key; + const host = sshconf.host; + const username = sshconf.user; + const port = sshconf.port || 22; + const target = sshconf.target; + const args = sshconf.args || '-rltgoDzvO'; + + if (!key || !host || !username || !target) { + console.error('SSH deployment requires more arguments.'); + return; + } + + console.log('Deploying to %s@%s:%d%s', username, host, port, target); + + const fileList = (await fs.readdir(outDir)).filter((val) => { + return val === 'repository.json' || val.match('.tgz$'); + }).map((x) => path.join(outDir, x)); + + const pargs = [ + 'rsync', + args, + '-e', + `ssh -i ${key} -p ${port}`, + ...fileList, + `${username}@${host}:${target}`, + ]; + + console.log(pargs.join(' ')); + await execute(pargs, outDir); + + console.log('Done!'); +} diff --git a/src/build/watch.ts b/src/build/watch.ts new file mode 100644 index 0000000..844bdcd --- /dev/null +++ b/src/build/watch.ts @@ -0,0 +1,57 @@ +import fs from 'fs-extra'; +import watch from 'node-watch'; +import path from 'path'; + +import { buildRepository } from './repository/build'; + +/** + * Create a filesystem watch task for a repository directory + * Equivalent to tsc -w + * @param location Repository path + * @param out Create output files. False - dry run. + * @param doDeploy Use deployment after build success + */ +export async function watchRepository( + location?: string, + out = true, + doDeploy?: string): Promise { + + if (!location) { + location = process.cwd(); + } + + const buildMetaFile = path.join(location, 'squeebot.repo.json'); + if (!await fs.pathExists(buildMetaFile)) { + throw new Error(`${location} is not a valid squeebot repository development environment!`); + } + + const meta = await fs.readJson(buildMetaFile); + if (!meta.name) { + throw new Error(`${location} is not a valid squeebot repository development environment!`); + } + + console.log('Watching repository "%s"!', meta.name); + + watch(location, { + recursive: true, + filter(f, skip): any { + if (/\/node_modules/.test(f) || + /\.out/.test(f) || + /\.git/.test(f) || + /\.json$/.test(f)) { + return skip; + } + + if (meta.typescript && /\.js$/.test(f)) { + return skip; + } + + return true; + }, + }, (evt, f) => { + console.log(`\n ==> ${f} changed, rebuilding...`); + buildRepository(location, out, doDeploy).catch((e) => { + console.error(e); + }); + }); +} diff --git a/src/core.ts b/src/core.ts index ca6bede..548052e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -20,6 +20,9 @@ const defaultConfiguration: {[key: string]: any} = { name: 'squeebot', }; +/** + * Reference implementation of a Squeebot system using core classes. + */ export class Squeebot implements ISqueebotCore { public npm: NPMExecutor = new NPMExecutor(this.environment, '@squeebot/core'); @@ -86,7 +89,7 @@ export class Squeebot implements ISqueebotCore { const getManifest = this.pluginManager.getAvailableByName(pluginName); if (!getManifest) { logger.error(new Error(`Failed to start ${pluginName}: no manifest available. You might not have installed it.`)); - return; + continue; } try { @@ -116,13 +119,16 @@ export class Squeebot implements ISqueebotCore { } logger.warn('Shutting down..'); - this.shuttingDown = true; + + // Subsystems confirmed shutdown, go ahead and exit. this.stream.on('core', 'shutdown', (state: number) => { if (state > 0) { this.config.save().then((x) => process.exit(0)); } }); + + // Prompt subsystems to prepare for shutting down this.stream.emitTo('core', 'shutdown', 0); } } diff --git a/src/squeebot.ts b/src/squeebot.ts index 79200b2..455a89e 100644 --- a/src/squeebot.ts +++ b/src/squeebot.ts @@ -1,394 +1,10 @@ #!/usr/bin/env node -import child_process from 'child_process'; -import fs from 'fs-extra'; -import watch from 'node-watch'; -import path from 'path'; import yargs from 'yargs'; -import tar from 'tar'; -import { logger } from '@squeebot/core/lib/core'; -import { NPMExecutor } from '@squeebot/core/lib/npm'; -import { IEnvironment } from '@squeebot/core/lib/types'; - -import { loadEnvironment } from '@squeebot/core/lib/core'; -import { PluginMetaLoader } from '@squeebot/core/lib/plugin'; - -const tsConfig = { - compilerOptions: { - downlevelIteration: true, - esModuleInterop: true, - experimentalDecorators: true, - forceConsistentCasingInFileNames: true, - skipLibCheck: true, - sourceMap: false, - strict: true, - target: 'es5', - }, -}; - -const gitignore = `/node_modules/ -/.out/ -deployment.json`; - -function dummyEnvironment(location: string): IEnvironment { - return { - configurationPath: path.join(location, 'plugins'), - environment: 'development', - path: location, - pluginsPath: path.join(location, 'plugins'), - repositoryPath: path.join(location, 'plugins'), - }; -} - -function execute(cmd: any[], loc: string): Promise { - return new Promise((resolve, reject) => { - const cproc = child_process.spawn(cmd[0], cmd.slice(1), { - cwd: loc, - }); - - cproc.stdout.pipe(process.stdout); - cproc.stderr.pipe(process.stderr); - - cproc.on('exit', (code: number) => { - if (code !== 0) { - return reject(new Error('Build failed!')); - } - resolve(); - }); - }); -} - -async function newEnvironment( - name?: string, - location?: string): Promise { - if (!name) { - name = 'squeebot'; - } - - if (!location) { - location = name; - } - - location = path.resolve(process.cwd(), location); - - const envFile = path.join(location, name + '.json'); - await fs.ensureDir(location); - await fs.writeJson(envFile, { - environment: 'production', - path: location, - }); - - console.log('\nNew environment file:\t', envFile); - console.log('Environment path:\t', location); -} - -async function newRepository( - name: string, - location?: string, - typescript = true): Promise { - if (!name) { - throw new Error('Repository needs a name!'); - } - - if (!location) { - location = name; - } - - location = path.resolve(process.cwd(), location); - console.log('Creating a new repository development environment at', location); - - await fs.ensureDir(location); - const env = dummyEnvironment(location); - - console.log('Creating package.json and installing base requirements'); - const executor = new NPMExecutor(env, '@squeebot/core'); - await executor.loadPackageFile(); - - let gitIgnore = gitignore + ''; - - if (typescript) { - console.log('Installing TypeScript support'); - await executor.installPackage('typescript'); - 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'); - const contents = await fs.readJson(pkgjson); - contents.scripts = { - build: 'tsc', - watch: 'tsc -w', - }; - await fs.writeJson(pkgjson, contents, { spaces: 2 }); - } - - console.log('Creating .gitignore'); - await fs.writeFile(path.join(location, '.gitignore'), gitIgnore); - - console.log('Writing build metadata'); - await fs.writeJson(path.join(location, 'squeebot.repo.json'), { - name, - plugins: [], - typescript, - }, { spaces: 2 }); - - console.log('\nDone! Your repository "%s" lives at:', name, location); -} - -async function sshDeploy( - outDir: string, - sshconf: any): Promise { - - const key = sshconf.key; - const host = sshconf.host; - const username = sshconf.user; - const port = sshconf.port || 22; - const target = sshconf.target; - const args = sshconf.args || '-rltgoDzvO'; - - if (!key || !host || !username || !target) { - console.error('SSH deployment requires more arguments.'); - return; - } - - console.log('Deploying to %s@%s:%d%s', username, host, port, target); - - const fileList = (await fs.readdir(outDir)).filter((val) => { - return val === 'repository.json' || val.match('.tgz$'); - }).map((x) => path.join(outDir, x)); - - const pargs = [ - 'rsync', - args, - '-e', - `ssh -i ${key} -p ${port}`, - ...fileList, - `${username}@${host}:${target}`, - ]; - - console.log(pargs.join(' ')); - await execute(pargs, outDir); - - console.log('Done!'); -} - -async function deploy( - name: string, - location: string, - outDir: string, - deployment: string): Promise { - const deployFile = path.join(location, 'deployment.json'); - - if (!await fs.pathExists(deployFile)) { - throw new Error('deployment.json file is missing! Can\'t deploy!'); - } - - const meta = await fs.readJson(deployFile); - - // If we have a path to a metadata file, we copy the output directory to - // the plugins directory, overwriting existing plugins with the same name! - if ((deployment.indexOf('dev') === 0 || deployment.indexOf('squee') === 0) && - meta.devSqueebot) { - console.log('Deploying to a configured Squeebot development environment'); - const devbotPath = path.resolve(location, meta.devSqueebot); - - console.log('Path:', devbotPath); - - if (!await fs.pathExists(devbotPath)) { - throw new Error('Development Squeebot environment file doesn\'t exist!'); - } - - const devEnv = await loadEnvironment(devbotPath); - if (!devEnv.pluginsPath) { - throw new Error('Bad development Squeebot environment file!'); - } - - const pluginsPath = path.resolve(devEnv.path, devEnv.pluginsPath); - - console.log('Copying plugins to', pluginsPath); - const listAllFiles = await fs.readdir(outDir); - for (const f of listAllFiles) { - if (f === 'repository' || f.indexOf('.tgz') !== -1) { - continue; - } - const dst = path.join(pluginsPath, f); - await fs.copy(path.join(outDir, f), dst, { - overwrite: true, - }); - } - - console.log('Done!'); - } - - if (deployment in meta) { - const dpl = meta[deployment]; - if (dpl.ssh) { - await sshDeploy(outDir, dpl); - } - } -} - -async function buildRepository( - location?: string, - out = true, - doDeploy?: string, - onlyDeploy = false): Promise { - if (!location) { - location = process.cwd(); - } - location = path.resolve(process.cwd(), location); - const outDir = path.join(location, '.out'); - - const buildMetaFile = path.join(location, 'squeebot.repo.json'); - if (!await fs.pathExists(buildMetaFile)) { - throw new Error(`${location} is not a valid squeebot repository development environment!`); - } - - const meta = await fs.readJson(buildMetaFile); - const env = dummyEnvironment(location); - env.pluginsPath = location; - - if (!meta.name) { - throw new Error(`${location} is not a valid squeebot repository development environment!`); - } - - console.log('Detected repository "%s"!', meta.name); - - if (onlyDeploy) { - if (!await fs.pathExists(outDir)) { - throw new Error(`You need to build before deploying!`); - } else { - return deploy(meta.name, location, outDir, doDeploy as string); - } - } - - if (meta.typescript) { - console.log('Running build task..'); - await execute(['npm', 'run', 'build'], location); - } - - console.log('Detecting plugins in this environment..'); - - const loader = new PluginMetaLoader(env); - const plugins = await loader.loadAll(false); - const savedList: any[] = []; - - console.log('Found the following plugins:', plugins.map((plugin) => { - return `${plugin.name}@${plugin.version}`; - }).join(', ')); - - plugins.forEach((plugin) => { - savedList.push({ - name: plugin.name, - version: plugin.version, - }); - }); - - meta.plugins = savedList; - await fs.writeJson(buildMetaFile, meta, { spaces: 2 }); - - if (!out) { - console.log('Done!'); - return; - } - - console.log('Creating repository index'); - await fs.remove(outDir); - await fs.ensureDir(outDir); - await fs.writeJSON(path.join(outDir, 'repository.json'), { - created: Math.floor(Date.now() / 1000), - name: meta.name, - plugins: meta.plugins, - schema: 1, - }); - - console.log('Copying plugins'); - for (const plugin of plugins) { - const src = path.join(location, plugin.name); - const dst = path.join(outDir, plugin.name); - await fs.copy(src, dst); - } - - if (meta.typescript) { - console.log('Stripping source files'); - for (const plugin of plugins) { - const plOut = path.join(outDir, plugin.name); - const listAllFiles = await fs.readdir(plOut); - for (const f of listAllFiles) { - if (f.match(/(\.d)?\.ts$/i) != null) { - await fs.remove(path.join(plOut, f)); - } - } - } - } - - console.log('Creating tarballs'); - for (const plugin of plugins) { - const plOut = path.join(outDir, plugin.name); - await tar.c({ - gzip: true, - file: plOut + '.plugin.tgz', - C: outDir, - }, [plugin.name]); - } - - if (doDeploy == null) { - console.log('Done!'); - return; - } - - console.log('\n [!!!] DEPLOYING REPOSITORY [!!!]\n'); - - deploy(meta.name, location, outDir, doDeploy); -} - -async function watchRepository( - location?: string, - out = true, - doDeploy?: string, - onlyDeploy = false): Promise { - - if (!location) { - location = process.cwd(); - } - - const buildMetaFile = path.join(location, 'squeebot.repo.json'); - if (!await fs.pathExists(buildMetaFile)) { - throw new Error(`${location} is not a valid squeebot repository development environment!`); - } - - const meta = await fs.readJson(buildMetaFile); - if (!meta.name) { - throw new Error(`${location} is not a valid squeebot repository development environment!`); - } - - console.log('Watching repository "%s"!', meta.name); - - watch(location, { - recursive: true, - filter(f, skip): any { - if (/\/node_modules/.test(f) || - /\.out/.test(f) || - /\.git/.test(f) || - /\.json$/.test(f)) { - return skip; - } - - if (meta.typescript && /\.js$/.test(f)) { - return skip; - } - - return true; - }, - }, (evt, f) => { - console.log(`\n ==> ${f} changed, rebuilding...`); - buildRepository(location, out, doDeploy, onlyDeploy).catch((e) => { - console.error(e); - }); - }); -} +import { newEnvironment } from './build/environment'; +import { buildRepository } from './build/repository/build'; +import { newRepository } from './build/repository/create'; +import { watchRepository } from './build/watch'; const yar = yargs.scriptName('squeebot') .command('new [name] [path]', 'create a new Squeebot environment', (y) => { @@ -452,7 +68,6 @@ const yar = yargs.scriptName('squeebot') dargs[0] as string, dargs[1] as boolean, dargs[2] as string, - dargs[3] as boolean, ); } diff --git a/src/squeebotd.ts b/src/squeebotd.ts index 12a1e09..3885d89 100644 --- a/src/squeebotd.ts +++ b/src/squeebotd.ts @@ -4,7 +4,6 @@ import readline from 'readline'; import yargs from 'yargs'; import { loadEnvironment } from '@squeebot/core/lib/core'; -import { IEnvironment } from '@squeebot/core/lib/types'; import { Squeebot } from './core'; import { SqueebotCLI } from './cli'; @@ -24,14 +23,13 @@ async function start(argv: any): Promise { root = path.resolve(argv.root); } - const enviroFile = path.resolve(process.cwd(), argv.environment); - const env: IEnvironment = await loadEnvironment(argv.environment, root); + const env = await loadEnvironment(argv.environment, root); // Change working directory to the environment process.chdir(env.path as string); + // Create and initialize the Squeebot instance const sb = new Squeebot(env); - await sb.initialize(argv.e !== true); // Create a CLI if interactive mode is enabled