From df17cb9cbabd08fba0095cce7ee0047e15a85fd2 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 28 Nov 2020 15:33:56 +0200 Subject: [PATCH] Repository tools --- src/core.ts | 9 +- src/squeebot.ts | 334 ++++++++++++++++++++++++++++++++++++++++++++++- src/squeebotd.ts | 2 - 3 files changed, 338 insertions(+), 7 deletions(-) diff --git a/src/core.ts b/src/core.ts index bbf8db7..528e2e5 100644 --- a/src/core.ts +++ b/src/core.ts @@ -62,6 +62,8 @@ export class Squeebot { // Start channels this.channelManager.initialize(this.config.get('channels')); + logger.debug(this.channelManager); + if (autostart) { // Start enabled plugins await this.startPlugins(); @@ -79,12 +81,15 @@ export class Squeebot { try { await this.pluginManager.load(getManifest); } catch (e) { - logger.error(new Error(`Failed to start ${pluginName}: ${e.message}`)); + logger.error('Failed to start %s:', pluginName, e.stack); } } } public attachReadline(rl: any): void { + // Pass readline to logger + logger.setReadline(rl); + // Send readline on request this.stream.on('core', 'request-cli', () => this.stream.emit('cli', rl)); @@ -104,7 +109,7 @@ export class Squeebot { this.shuttingDown = true; this.stream.on('core', 'shutdown', (state: number) => { if (state > 0) { - this.config.save(true).then((x) => process.exit(0)); + this.config.save().then((x) => process.exit(0)); } }); this.stream.emitTo('core', 'shutdown', 0); diff --git a/src/squeebot.ts b/src/squeebot.ts index 4fce850..0ff359f 100644 --- a/src/squeebot.ts +++ b/src/squeebot.ts @@ -1,6 +1,334 @@ +import child_process from 'child_process'; +import fs from 'fs-extra'; +import path from 'path'; +import readline from 'readline'; +import yargs from 'yargs'; -function call(test: string): void { - console.log('it works!', test); +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/ +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'), + }; } -call('hi'); +function execute(cmd: any[], loc: string): Promise { + return new Promise((resolve, reject) => { + const cproc = child_process.spawn(cmd[0], cmd.slice(1), { + cwd: loc, + stdio: 'pipe', + }); + + 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, '/home/evert/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'; + + 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 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') { + continue; + } + const dst = path.join(pluginsPath, f); + await fs.copy(path.join(outDir, f), dst, { + overwrite: true, + }); + } + + console.log('Done!'); + } +} + +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); + } + } + + console.log('Detecting plugins in this environment..'); + + const loader = new PluginMetaLoader(env); + const plugins = await loader.loadAll(false); + const savedList: any[] = []; + let indexFileContents = 'name=' + meta.name + '\n'; + indexFileContents += 'created=' + Math.floor(Date.now() / 1000) + '\n'; + + 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, + }); + indexFileContents += `${plugin.name} ${plugin.version} "${plugin.description}"\n`; + }); + + meta.plugins = savedList; + await fs.writeJson(buildMetaFile, meta, { spaces: 2 }); + + if (meta.typescript) { + console.log('Running build task..'); + await execute(['npm', 'run', 'build'], location); + } + + if (!out) { + console.log('Done!'); + return; + } + + console.log('Creating repository index'); + await fs.remove(outDir); + await fs.ensureDir(outDir); + await fs.writeFile(path.join(outDir, 'repository'), indexFileContents); + + 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)); + } + } + } + } + + if (doDeploy == null) { + console.log('Done!'); + return; + } + + console.log('\n [!!!] DEPLOYING REPOSITORY [!!!]\n'); + + deploy(meta.name, location, outDir, doDeploy); +} + +const yar = yargs.scriptName('squeebot') + .command('new [name] [path]', 'create a new Squeebot environment', (y) => { + y.positional('name', { + describe: 'The name of the new environment', + }) + .positional('path', { + describe: 'The path to create a new Squeebot environment at (default: working directory)', + }); + }, (v) => newEnvironment(v.name as string, v.path as string)) + .command('repository', 'manage repositories', (y) => { + y.command('new [name] [path]', 'create a new repository', (yi) => { + yi.positional('name', { + demandOption: 'The repository requires a name', + describe: 'The name of the new repository', + }) + .positional('path', { + describe: 'The path to create the new Squeebot plugin repository at (default: working directory)', + }) + .option('t', { + alias: 'no-typescript', + describe: 'Do not include typescript in the development environment', + type: 'boolean', + }); + }, (v) => newRepository(v.name as string, v.path as string, v.t !== true)); + y.command('build [path]', 'build a repository of plugins and generate the index file', (yi) => { + yi.positional('path', { + describe: 'The path of the repository', + }) + .option('p', { + alias: 'no-output', + describe: 'Do not create an output directory, just build', + type: 'boolean', + }) + .option('d', { + alias: 'deploy', + describe: 'Deploy the output directory as configured', + nargs: 1, + type: 'string', + }) + .option('o', { + alias: 'deploy-only', + describe: 'Deploy only, without rebuilding', + type: 'boolean', + }); + }, (v) => buildRepository(v.path as string, + v.p !== true, + v.d as string, + v.o === true)); + }) + .argv; diff --git a/src/squeebotd.ts b/src/squeebotd.ts index 695d714..1174c13 100644 --- a/src/squeebotd.ts +++ b/src/squeebotd.ts @@ -29,8 +29,6 @@ async function start(argv: any): Promise { const sb = new Squeebot(env); - sb.stream.on('logger', 'debug', (data: string) => console.log('DEBUG', data)); - await sb.initialize(); sb.attachReadline(rl);