@imqueue/cli
Version:
Command Line Interface for IMQ
694 lines (684 loc) • 25.9 kB
JavaScript
;
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.handler = exports.builder = exports.describe = exports.command = void 0;
/*!
* IMQ-CLI command: service create
*
* I'm Queue Software Project
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* If you want to use this code in a closed source (commercial) project, you can
* purchase a proprietary commercial license. Please contact us at
* <support@imqueue.com> to get commercial licensing options.
*/
const path = require("path");
const fs = require("fs");
const os = require("os");
const chalk_1 = require("chalk");
const semver = require("semver");
const inquirer_1 = require("inquirer");
const lib_1 = require("../../lib");
const child_process_1 = require("child_process");
const commandExists = require('command-exists').sync;
const DEFAULT_SERVICE_VERSION = '1.0.0-0';
let config;
// istanbul ignore next
async function ensureTemplate(template) {
if (fs.existsSync(template)) {
return template;
}
if (/^git@/.test(template)) {
// template is a git url
return await (0, lib_1.loadTemplate)(template);
}
// template is a name
const templates = await (0, lib_1.loadTemplates)();
if (!templates[template]) {
throw new Error(`No such template exists - "${template}"`);
}
return templates[template];
}
// istanbul ignore next
function updateLicenseText(text, author, email, serviceName, homepage) {
const values = {
'year': new Date().getFullYear(),
'fullname': author,
'email': email,
'project': serviceName,
'project_url': homepage
};
for (let varName of Object.keys(values)) {
text = text.replace(`[${varName}]`, values[varName]);
}
return text;
}
// istanbul ignore next
async function ensureLicense(path, license, author, email, homepage, serviceName) {
let text = '';
let header = '';
let name = '';
let tag = 'UNLICENSED';
if (license === 'UNLICENSED' && typeof config.license === 'undefined') {
const userLicense = await (0, lib_1.licensingOptions)();
tag = userLicense.id;
name = userLicense.name;
license = tag;
}
if (license === 'UNLICENSED') {
header = `/*!
* Copyright (c) ${new Date().getFullYear()} ${author} <${email}>
*
* This software is private and is unlicensed. Please, contact
* author for any licensing details.
*/`;
text = `Copyright (c) ${new Date().getFullYear()} ${author} <${email}>
This software is private and is unlicensed. Please, contact
author for any licensing details.\n`;
name = license;
}
else {
const lic = (0, lib_1.findLicense)(license);
text = updateLicenseText(lic.body + '\n', author, email, serviceName, homepage);
name = lic.name;
tag = lic.spdx_id;
header = updateLicenseText(lic.header || '', author, email, serviceName, homepage) || `Copyright (c) ${new Date().getFullYear()} ${author} <${email}>
This software is licensed under ${lic.spdx_id} license.
Please, refer to LICENSE file in project's root directory for details.`;
header = `/*!\n * ${header.split(/\r?\n/).join('\n * ')}\n */`;
}
try {
fs.unlinkSync((0, lib_1.resolve)(path, 'LICENSE'));
}
catch (err) { /* ignore */ }
(0, lib_1.touch)((0, lib_1.resolve)(path, 'LICENSE'), (0, lib_1.wrap)(text));
return { text, header, name, tag };
}
// istanbul ignore next
function ensureName(name) {
if (!name.trim()) {
throw new TypeError(`Service name expected, but was not given!`);
}
return (0, lib_1.dashed)(name.trim());
}
// istanbul ignore next
function ensureVersion(version) {
if (!version.trim()) {
version = DEFAULT_SERVICE_VERSION;
}
if (!semver.valid(version)) {
throw new TypeError('Given version is invalid, please, provide ' +
'valid semver format!');
}
return version;
}
// istanbul ignore next
function ensureDescription(description, name) {
return description || `${(0, lib_1.dashed)(name)} - IMQ based service`;
}
// istanbul ignore next
function ensureServiceRepo(owner, name) {
if (!owner) {
return '';
}
return `\n "repository": {
"type": "git",
"url": "git@github.com:/${owner}/${(0, lib_1.dashed)(name)}"
},\n`;
}
// istanbul ignore next
function ensureServicePages(argv) {
const owner = argv.u.trim();
// noinspection TypeScriptUnresolvedVariable
let url = argv.B.trim();
let home = '';
let bugs = '';
if (!url && !owner) {
home = bugs = '';
}
else if (!url && owner) {
bugs = `https://github.com/${owner}/${(0, lib_1.dashed)(argv.name)}/issues`;
home = `https://github.com/${owner}/${(0, lib_1.dashed)(argv.name)}`;
}
return {
bugs: bugs ? `\n "bugs": {\n "url": "${bugs}"\n },\n` : '',
home: home ? `\n "homepage": "${home}",\n` : '',
};
}
// istanbul ignore next
async function ensureAuthorName(name) {
name = name.trim();
if (!name) {
const answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'authorName',
message: 'Enter author\'s name:',
default: os.userInfo().username
}]);
name = answer.authorName.trim() || os.userInfo().username;
}
return name;
}
// istanbul ignore next
async function ensureAuthorEmail(email) {
email = email.trim();
if (!(0, lib_1.isEmail)(email)) {
const answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'email',
message: 'Enter author\'s email:'
}]);
if (!(0, lib_1.isEmail)(answer.email)) {
throw new TypeError('Author\'s email is required, but was not given!');
}
email = answer.email;
}
return email;
}
// istanbul ignore next
async function ensureTravisTags(argv) {
if (argv.n instanceof Array && argv.n.length) {
return argv.n;
}
let tags = (argv.n || '').split(/\s+|\s*,\s*/).filter((t) => t);
if (!tags.length) {
let answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'tags',
message: 'Enter node version(s) for CI builds (comma-separated ' +
'if multiple):',
default: 'stable, latest'
}]);
if (!answer.tags) {
tags.push('stable', 'latest');
}
else {
tags = answer.tags.split(/\s+|\s*,\s*/);
}
}
argv.n = argv.nodeVersions = tags = await (0, lib_1.toTravisTags)(tags);
return tags;
}
// istanbul ignore next
async function ensureDockerNamespace(argv) {
let ns = (argv.N || '').trim();
let dockerize = argv.D || config.useDocker;
let answer;
if (!dockerize && typeof config.useDocker === 'undefined') {
answer = await inquirer_1.default.prompt([{
type: 'confirm',
name: 'useDocker',
message: 'Would you like to dockerize your service?',
default: true,
}]);
config.useDocker = argv.D = argv.dockerize = dockerize =
answer.useDocker;
}
if (dockerize && !(0, lib_1.isNamespace)(ns)) {
answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'dockerNamespace',
message: 'Enter DockerHub namespace:'
}]);
if (answer.dockerNamespace &&
!(0, lib_1.isNamespace)(answer.dockerNamespace.trim())) {
throw new TypeError('Given DockerHub namespace is invalid!');
}
config.dockerHubNamespace = argv.N = argv.dockerNamespace = ns =
answer.dockerNamespace;
}
return ns;
}
// istanbul ignore next
async function ensureDockerTag(argv) {
if (argv.L.trim()) {
return argv.L.trim();
}
const tags = await ensureTravisTags(argv);
const version = await (0, lib_1.nodeVersion)(tags[0]);
if (!version) {
throw new TypeError('Invalid node version specified!');
}
return version;
}
// istanbul ignore next
async function ensureDockerSecrets(argv) {
const owner = argv.u.trim();
const name = ensureName(argv.name);
let { dockerHubUser, dockerHubPassword, gitHubAuthToken } = config;
if (!owner) {
throw new TypeError('GitHub namespace required, but is empty!');
}
if (!gitHubAuthToken) {
throw new TypeError('Github auth token required, but was not given!');
}
const repo = `${owner}/${name}`;
if (!dockerHubUser) {
const answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'dockerHubUser',
message: 'Docker hub user:'
}]);
if (!answer.dockerHubUser.trim()) {
throw new TypeError('DockerHub username required, but was not given!');
}
dockerHubUser = answer.dockerHubUser;
}
if (!dockerHubPassword) {
const answer = await inquirer_1.default.prompt([{
type: 'password',
name: 'dockerHubPassword',
message: 'Docker hub password:'
}]);
if (!answer.dockerHubPassword.trim()) {
throw new TypeError('DockerHub password required, but was not given!');
}
dockerHubPassword = answer.dockerHubPassword;
}
console.log('Encrypting secrets...');
return [
await (0, lib_1.travisEncrypt)(repo, `DOCKER_USER="${dockerHubUser}"`, argv.p ? gitHubAuthToken : undefined),
await (0, lib_1.travisEncrypt)(repo, `DOCKER_PASS="${dockerHubPassword}"`, argv.p ? gitHubAuthToken : undefined),
];
}
// istanbul ignore next
function stripDockerization(argv) {
const path = (0, lib_1.resolve)(argv.path);
const travis = (0, lib_1.resolve)(path, '.travis.yml');
const docker = (0, lib_1.resolve)(path, 'Dockerfile');
const ignore = (0, lib_1.resolve)(path, '.dockerignore');
if (fs.existsSync(travis)) {
const travisYml = fs.readFileSync(travis, { encoding: 'utf8' });
fs.writeFileSync(travis, travisYml.replace(/services:[\s\S]+?$/, ''), { encoding: 'utf8' });
}
if (fs.existsSync(docker)) {
fs.unlinkSync(docker);
}
if (fs.existsSync(ignore)) {
fs.unlinkSync(ignore);
}
}
// istanbul ignore next
async function enableTravisBuilds(argv) {
console.log('Enabling travis builds...');
let enabled = false;
try {
enabled = await (0, lib_1.enableBuilds)(argv.u, ensureName(argv.name), config.gitHubAuthToken, argv.p);
}
catch (err) { /* ignore */ }
if (!enabled) {
// noinspection TypeScriptValidateJSTypes
console.log(chalk_1.default.red('There was a problem enabling builds for this service. Please ' +
'go to http://travis-ci.org/ and enable builds manually.'));
}
}
// istanbul ignore next
async function buildDockerCi(argv) {
const dockerNs = await ensureDockerNamespace(argv);
const dockerize = !!(gitRepoInitialized && dockerNs && (argv.D || config.useDocker));
const tags = {
TRAVIS_NODE_TAG: (await ensureTravisTags(argv))
.map(t => `- ${t}`).join('\n'),
};
if (!dockerize) {
stripDockerization(argv);
await enableTravisBuilds(argv);
}
else {
await enableTravisBuilds(argv);
console.log('Building docker <-> CI integration...');
Object.assign(tags, {
DOCKER_NAMESPACE: dockerNs,
NODE_DOCKER_TAG: await ensureDockerTag(argv),
DOCKER_SECRETS: `- secure: ${(await ensureDockerSecrets(argv))
.join('\n - secure: ')}`,
});
}
console.log('Updating docker and CI configs...');
compileTemplate((0, lib_1.resolve)(argv.path), tags);
}
// istanbul ignore next
async function buildTags(path, argv) {
const name = ensureName(argv.name);
const author = await ensureAuthorName(argv.author);
const email = await ensureAuthorEmail(argv.email);
const { home, bugs } = ensureServicePages(argv);
const license = await ensureLicense(path, argv.license, author, email, home, name);
// noinspection TypeScriptUnresolvedVariable
return {
SERVICE_NAME: name,
SERVICE_CLASS_NAME: (0, lib_1.camelCase)(name),
SERVICE_VERSION: ensureVersion(argv.serviceVersion),
SERVICE_DESCRIPTION: ensureDescription(argv.description, name),
SERVICE_REPO: ensureServiceRepo(argv.u, name),
SERVICE_BUGS: bugs,
SERVICE_HOMEPAGE: home,
SERVICE_AUTHOR_NAME: author,
SERVICE_AUTHOR_EMAIL: `<${email}>`,
LICENSE_HEADER: license.header,
LICENSE_TEXT: license.text,
LICENSE_NAME: license.name,
LICENSE_TAG: license.tag,
};
}
// istanbul ignore next
function createServiceFile(path, tags) {
console.log('Creating main service file...');
(0, lib_1.touch)((0, lib_1.resolve)(path, 'src', `${tags.SERVICE_CLASS_NAME}.ts`), `${tags.LICENSE_HEADER}
import { expose, IMQService, lock, logged, profile } from '@imqueue/rpc';
export class ${tags.SERVICE_CLASS_NAME} extends IMQService {
/**
* Service package data
*/
private pkg = require('../package.json');
/**
* Returns current version of running service
*
* @return {{
* name: string,
* version: string,
* repository: string
* }} - version of the service
*/
@logged()
@lock()
@profile()
@expose()
public version(): { name: string; version: string; repository: string } {
const { name, version, repository } = this.pkg;
return { name, version, repository: repository.url };
}
// Implement your service methods below this line
}
`);
}
// istanbul ignore next
function createServiceTestFile(path, tags) {
console.log('Creating main service test file...');
(0, lib_1.touch)((0, lib_1.resolve)(path, 'test/src', `${tags.SERVICE_CLASS_NAME}.ts`), `${tags.LICENSE_HEADER}
import { expect } from 'chai';
import { ${tags.SERVICE_CLASS_NAME} } from '../../src';
describe('${tags.SERVICE_CLASS_NAME}', () => {
it('should be a class of IMQService', () => {
expect(typeof ${tags.SERVICE_CLASS_NAME})
.equals('function');
expect(typeof (${tags.SERVICE_CLASS_NAME}.prototype as any).describe)
.equals('function');
});
describe('version()', () => {
const service = new ${tags.SERVICE_CLASS_NAME}();
const pkg = require('../../package.json');
it('should be a function', () => {
expect(typeof service.version).equals('function');
});
it('should return proper name string', async () => {
expect((await service.version()).name).equals(pkg.name);
});
it('should return proper version string', async () => {
expect((await service.version()).version).equals(pkg.version);
});
});
});
`);
}
// istanbul ignore next
function compileTemplateFile(text, tags) {
for (let tag of Object.keys(tags)) {
text = text.replace(new RegExp(`%${tag}`, 'g'), tags[tag]);
}
return text;
}
// istanbul ignore next
function compileTemplate(path, tags) {
fs.readdirSync(path).forEach((file) => {
const filePath = (0, lib_1.resolve)(path, file);
if (fs.statSync(filePath).isDirectory()) {
return compileTemplate(filePath, tags);
}
let content = compileTemplateFile(fs.readFileSync(filePath, { encoding: 'utf8' }), tags);
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
});
}
// istanbul ignore next
async function makeService(path, argv) {
const tags = await buildTags(path, argv);
compileTemplate(path, tags);
createServiceFile(path, tags);
createServiceTestFile(path, tags);
}
// istanbul ignore next
async function buildFromTemplate(argv) {
const template = await ensureTemplate(argv.template);
const path = (0, lib_1.resolve)(argv.path);
console.log(`Building service from template "${template}"...`);
(0, lib_1.cpr)(template, path);
await makeService(path, argv);
}
// istanbul ignore next
async function ensureGitRepo(argv) {
if (!(0, lib_1.isNamespace)(argv.u)) {
const answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'gitNs',
message: 'Enter GitHub owner (user name or organization):',
}]);
if (!(0, lib_1.isNamespace)(answer.gitNs)) {
throw new TypeError(`Given github namespace "${argv.u}" is invalid!`);
}
argv.u = answer.gitNs;
}
return argv.u + '/' + (0, lib_1.dashed)(argv.name);
}
let gitRepoInitialized = false;
// istanbul ignore next
async function createGitRepo(argv) {
const useGit = argv.g || config.useGit;
if (!useGit && typeof config.useGit === 'undefined') {
const answer = await inquirer_1.default.prompt([{
type: 'confirm',
name: 'useGit',
message: 'Would you like to enable automatic GitHub integration ' +
'for this service?',
default: true,
}]);
if (!answer.useGit) {
argv.D = argv.dockerize = config.useDocker = false;
return;
}
}
const url = await ensureGitRepo(argv);
let token = (argv.T || '').trim() || config.gitHubAuthToken;
if (!(0, lib_1.isGuthubToken)(token)) {
const answer = await inquirer_1.default.prompt([{
type: 'input',
name: 'token',
message: 'Enter your GitHub auth token:'
}]);
if (!(0, lib_1.isGuthubToken)(answer.token.trim())) {
throw new Error('Given GitHub auth token is invalid!');
}
config.gitHubAuthToken = argv.T = argv.githubToken = token =
answer.token.trim();
}
let isPrivate = argv.p || config.gitRepoPrivate;
if (!isPrivate && typeof config.gitRepoPrivate === 'undefined') {
const answer = await inquirer_1.default.prompt([{
type: 'confirm',
name: 'isPrivate',
message: 'Should be service created on GitHub as private repo?',
default: true
}]);
isPrivate = answer.isPrivate;
}
argv.p = argv.private = config.gitRepoPrivate = isPrivate;
const descr = ensureDescription(argv.description, ensureName(argv.name));
console.log('Creating github repository...');
await (0, lib_1.createRepository)(url, token, descr, isPrivate);
gitRepoInitialized = true;
}
// istanbul ignore next
async function installPackages(argv) {
if (!commandExists('npm')) {
throw new Error('npm command is not installed!');
}
const cwd = process.cwd();
const path = (0, lib_1.resolve)(argv.path);
const pkg = require((0, lib_1.resolve)(path, 'package.json'));
// noinspection TypeScriptUnresolvedVariable
const deps = Object.keys(pkg.dependencies);
// noinspection TypeScriptUnresolvedVariable
const devDeps = Object.keys(pkg.devDependencies);
process.chdir(path);
if (deps && deps.length) {
console.log('Installing dependencies...');
(0, child_process_1.execSync)(`npm i --save ${deps.join(' ')} 2>&1`);
}
if (devDeps && devDeps.length) {
console.log('Installing dev dependencies...');
(0, child_process_1.execSync)(`npm i --save-dev ${devDeps.join(' ')} 2>&1`);
}
process.chdir(cwd);
}
// istanbul ignore next
async function commit(argv) {
const path = (0, lib_1.resolve)(argv.path);
const name = ensureName(argv.name);
const owner = (argv.u || '').trim();
const pkg = require((0, lib_1.resolve)(path, 'package.json'));
const cwd = process.cwd();
let url = config.gitBaseUrl;
if (!owner && !url) {
throw new TypeError('GitHub namespace missing!');
}
else if (owner) {
url = `git@github.com:${owner}/${name}.git`;
}
else {
url += `/${name}.git`;
}
process.chdir(path);
if (!commandExists('git')) {
throw new Error('Git command expected, but is not installed!');
}
console.log('Committing changes...');
(0, child_process_1.execSync)(`git init && \
git add . && \
git commit -am "Initial commit" &&
git remote add origin ${url} && \
git push origin master`);
console.log('Setting up version tag...');
(0, child_process_1.execSync)(`git tag -d v${pkg.version}; \
git push origin :refs/tags/v${pkg.version}; \
git tag -fa v${pkg.version} -m "Tagging version v${pkg.version}" && \
git push origin master --tags`);
process.chdir(cwd);
}
// noinspection JSUnusedGlobalSymbols
_a = {
command: 'create [name] [path]',
describe: 'Creates new service package with the given service name ' +
'under given path.',
builder(yargs) {
config = (0, lib_1.loadConfig)();
return yargs
.alias('a', 'author')
.describe('a', 'Service author full name (person or organization)')
.default('a', config.author || '')
.alias('e', 'email')
.describe('e', 'Service author\'s contact email')
.default('e', config.email || '')
.alias('g', 'use-git')
.describe('g', 'Turns on automatic git repo creation')
.boolean('g')
.alias('u', 'github-namespace')
.describe('u', 'GitHub namespace (usually user name or ' +
'organization name)')
.default('u', (config.gitBaseUrl || '').split(':').pop() || '')
.describe('no-install', 'Do not install npm packages ' +
'automatically on service creation')
.boolean('no-install')
.default('no-install', false)
.alias('V', 'service-version')
.describe('V', 'Initial service version')
.default('V', DEFAULT_SERVICE_VERSION)
.alias('H', 'homepage')
.describe('H', 'Homepage URL for service, if required')
.default('H', '')
.alias('B', 'bugs-url')
.describe('B', 'Bugs url for service, if required')
.default('B', '')
.alias('l', 'license')
.describe('l', 'License for created service, should be either ' +
'license name in SPDX format or path to a custom license file')
.default('l', config.license || 'UNLICENSED')
.alias('t', 'template')
.describe('t', 'Template used to create service (should be ' +
'either template name, git url or file system directory)')
.default('t', config.template || 'default')
.alias('d', 'description')
.describe('d', 'Service description')
.default('d', '')
.alias('n', 'node-versions')
.describe('n', 'Node version tags to use for builds, separated ' +
'by comma if multiple. First one will be used for docker ' +
'build, if dockerize option enabled.')
.default('n', '')
.alias('D', 'dockerize')
.describe('D', 'Enable service dockerization with CI builds')
.boolean('D')
.alias('L', 'node-docker-tag')
.describe('L', 'Node docker tag to use as base docker image ' +
'for docker builds')
.default('L', '')
.alias('N', 'docker-namespace')
.describe('N', 'Docker hub namespace')
.default('N', config.dockerHubNamespace)
.alias('T', 'github-token')
.describe('T', 'GitHub auth token')
.default('T', config.gitHubAuthToken)
.alias('p', 'private')
.describe('p', 'Service repository will be private at GitHub')
.boolean('p')
.default('name', path.basename(process.cwd()))
.describe('name', 'Service name to create with')
.default('path', '.')
.describe('path', 'Path to directory where service will be generated to');
},
async handler(argv) {
try {
await buildFromTemplate(argv);
await createGitRepo(argv);
await buildDockerCi(argv);
// noinspection TypeScriptUnresolvedVariable
if (!argv.noInstall) {
await installPackages(argv);
}
if (gitRepoInitialized) {
await commit(argv);
}
// noinspection TypeScriptValidateJSTypes
console.log(chalk_1.default.green('Service successfully created!'));
}
catch (err) {
if (argv.path && !~['', '.', './'].indexOf(argv.path.trim())) {
// cleanup service dir
(0, lib_1.rmdir)((0, lib_1.resolve)(argv.path));
}
(0, lib_1.printError)(err);
}
}
}, exports.command = _a.command, exports.describe = _a.describe, exports.builder = _a.builder, exports.handler = _a.handler;
//# sourceMappingURL=create.js.map