2020-12-06 20:24:20 +02:00
|
|
|
#!/usr/bin/env node
|
2020-11-28 15:33:56 +02:00
|
|
|
import child_process from 'child_process';
|
|
|
|
import fs from 'fs-extra';
|
2020-11-28 15:59:54 +02:00
|
|
|
import watch from 'node-watch';
|
2020-11-28 15:33:56 +02:00
|
|
|
import path from 'path';
|
|
|
|
import yargs from 'yargs';
|
2020-11-28 21:08:56 +02:00
|
|
|
import tar from 'tar';
|
2020-11-21 17:41:31 +02:00
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
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/
|
2020-11-28 22:07:20 +02:00
|
|
|
/.out/
|
2020-11-28 15:33:56 +02:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2020-12-13 12:48:48 +02:00
|
|
|
cproc.stdout.pipe(process.stdout);
|
|
|
|
cproc.stderr.pipe(process.stderr);
|
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
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');
|
2020-12-06 20:21:09 +02:00
|
|
|
const executor = new NPMExecutor(env, '@squeebot/core');
|
2020-11-28 15:33:56 +02:00
|
|
|
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';
|
2020-11-28 22:07:20 +02:00
|
|
|
gitIgnore += '\n*.tsbuildinfo';
|
2020-11-28 15:33:56 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-12-13 12:48:48 +02:00
|
|
|
async function sshDeploy(
|
|
|
|
outDir: string,
|
|
|
|
sshconf: any): Promise<void> {
|
|
|
|
|
|
|
|
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!');
|
|
|
|
}
|
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
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) {
|
2020-11-28 21:08:56 +02:00
|
|
|
if (f === 'repository' || f.indexOf('.tgz') !== -1) {
|
2020-11-28 15:33:56 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const dst = path.join(pluginsPath, f);
|
|
|
|
await fs.copy(path.join(outDir, f), dst, {
|
|
|
|
overwrite: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('Done!');
|
|
|
|
}
|
2020-12-13 12:48:48 +02:00
|
|
|
|
|
|
|
if (deployment in meta) {
|
|
|
|
const dpl = meta[deployment];
|
|
|
|
if (dpl.ssh) {
|
|
|
|
await sshDeploy(outDir, dpl);
|
|
|
|
}
|
|
|
|
}
|
2020-11-28 15:33:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-29 21:25:17 +02:00
|
|
|
if (meta.typescript) {
|
|
|
|
console.log('Running build task..');
|
|
|
|
await execute(['npm', 'run', 'build'], location);
|
|
|
|
}
|
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
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);
|
2020-11-28 21:08:56 +02:00
|
|
|
await fs.writeJSON(path.join(outDir, 'repository.json'), {
|
|
|
|
created: Math.floor(Date.now() / 1000),
|
|
|
|
name: meta.name,
|
|
|
|
plugins: meta.plugins,
|
|
|
|
schema: 1,
|
|
|
|
});
|
2020-11-28 15:33:56 +02:00
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-28 21:08:56 +02:00
|
|
|
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]);
|
|
|
|
}
|
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
if (doDeploy == null) {
|
|
|
|
console.log('Done!');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('\n [!!!] DEPLOYING REPOSITORY [!!!]\n');
|
|
|
|
|
|
|
|
deploy(meta.name, location, outDir, doDeploy);
|
2020-11-21 17:41:31 +02:00
|
|
|
}
|
|
|
|
|
2020-11-28 15:59:54 +02:00
|
|
|
async function watchRepository(
|
|
|
|
location?: string,
|
|
|
|
out = true,
|
|
|
|
doDeploy?: string,
|
2020-11-28 21:08:56 +02:00
|
|
|
onlyDeploy = false): Promise<void> {
|
2020-11-28 15:59:54 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-11-28 15:33:56 +02:00
|
|
|
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',
|
|
|
|
})
|
2020-11-28 15:59:54 +02:00
|
|
|
.option('w', {
|
|
|
|
alias: 'watch',
|
|
|
|
describe: 'Watch files for changes',
|
|
|
|
type: 'boolean',
|
|
|
|
})
|
2020-11-28 15:33:56 +02:00
|
|
|
.option('o', {
|
|
|
|
alias: 'deploy-only',
|
|
|
|
describe: 'Deploy only, without rebuilding',
|
|
|
|
type: 'boolean',
|
|
|
|
});
|
2020-11-28 15:59:54 +02:00
|
|
|
}, (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,
|
|
|
|
);
|
|
|
|
});
|
2020-11-28 15:33:56 +02:00
|
|
|
})
|
|
|
|
.argv;
|