cli/src/squeebot.ts

421 lines
11 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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';
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 deploy(
name: string,
location: string,
outDir: string,
deployment: string): Promise<void> {
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!');
}
}
async function buildRepository(
location?: string,
out = true,
doDeploy?: string,
onlyDeploy = false): Promise<void> {
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[] = [];
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 (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.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<void> {
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);
});
});
}
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('w', {
alias: 'watch',
describe: 'Watch files for changes',
type: 'boolean',
})
.option('o', {
alias: 'deploy-only',
describe: 'Deploy only, without rebuilding',
type: 'boolean',
});
}, (v) => {
const dargs = [
v.path,
v.p !== true,
v.d,
v.o === true,
];
if (v.w) {
return watchRepository(
dargs[0] as string,
dargs[1] as boolean,
dargs[2] as string,
dargs[3] as boolean,
);
}
buildRepository(
dargs[0] as string,
dargs[1] as boolean,
dargs[2] as string,
dargs[3] as boolean,
);
});
})
.argv;