vvc
Version:
Vivocha Command Line Tools
568 lines • 28.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const language_1 = require("@vivocha/public-entities/dist/wrappers/language");
const scopes_1 = require("@vivocha/scopes");
const bodyParser = require("body-parser");
const columnify = require("columnify");
const program = require("commander");
const express = require("express");
const fs = require("fs");
const http = require("http");
const inquirer = require("inquirer");
const jsonpolice = require("jsonpolice");
const _ = require("lodash");
const _mkdirp = require("mkdirp");
const openurl_1 = require("openurl");
const path = require("path");
const reload = require("reload");
const util_1 = require("util");
const assets_1 = require("./lib/assets");
const config_1 = require("./lib/config");
const startup_1 = require("./lib/startup");
const strings_1 = require("./lib/strings");
const ws_1 = require("./lib/ws");
const access = util_1.promisify(fs.access);
const writeFile = util_1.promisify(fs.writeFile);
const mkdirp = util_1.promisify(_mkdirp);
(async () => {
try {
await startup_1.checkLoginAndVersion();
const config = await config_1.read();
program.version(config_1.meta.version);
const commands = {
list: program
.command('list [widget_id]')
.description('Display a list of available widgets. If an id is specified, the command lists all versions of the widget')
.option('-e, --engagement', 'Only list engagement widgets')
.option('-i, --interaction', 'Only list interaction widgets')
.option('-v, --verbose', 'Verbose output')
.action(async (widget_id, options) => {
try {
if (widget_id) {
const qs = {
fields: ['id', 'type', 'version', 'draft', 'acct_id'].join(','),
sort: '-version'
};
if (options.global) {
qs.global = true;
}
const data = await ws_1.ws(`widgets/${widget_id}/all`, { qs });
if (!data || !data.length) {
throw 'unknown widget';
}
const columns = columnify(data, {
columns: ['version', 'draft', 'acct_id'],
config: {
draft: {
dataTransform: data => (data ? '✓' : '')
},
acct_id: {
dataTransform: data => (data ? '' : '✓'),
headingTransform: () => 'global'.toUpperCase()
}
}
});
console.log(columns);
}
else {
const qs = {
fields: ['id', 'type', 'version', 'draft', 'acct_id'].join(','),
sort: 'id'
};
if (options.global) {
qs.global = true;
}
if (options.engagement) {
qs.q = 'eq(type,engagement)';
}
if (options.interaction) {
qs.q = 'eq(type,interaction)';
}
const data = (await ws_1.ws('widgets', { qs })).reduce((o, i) => {
o[i.id] = i;
return o;
}, {});
if (!data || !Object.keys(data).length) {
throw 'no widgets found';
}
(await strings_1.fetchStrings(Object.keys(data)
.map(w => `WIDGET.${w}.NAME`)
.join(','), options.global)).reduce((o, i) => {
const id = i.id.replace(/^WIDGET\.([^\.]*)\.NAME$/, '$1');
if (o[id] && i.values && i.values.en && i.values.en.value) {
o[id].name = i.values.en.value;
}
return o;
}, data);
const columns = columnify(Object.entries(data).map(i => i[1]), {
columns: ['id', 'type', 'version', 'draft', 'acct_id', 'name'],
config: {
draft: {
dataTransform: data => (data ? '✓' : '')
},
acct_id: {
dataTransform: data => (data ? '' : '✓'),
headingTransform: () => 'global'.toUpperCase()
}
}
});
console.log(columns);
}
}
catch (e) {
console.error(e);
process.exit(1);
}
}),
push: program
.command('push')
.description('Push a new version of the widget to the Vivocha servers')
.option('-a, --activate', 'Activate the pushed widget')
.option('-d, --directory <widget path>', 'Use the widget at the specified path', process.cwd())
.option('-r, --rescan', 'Rescan and upload all assets')
.action(async (options) => {
const startDir = process.cwd();
let exitCode = 0;
try {
// change to the widget directory
if (options.directory !== startDir) {
process.chdir(options.directory);
}
// check if manifest.json exists
await access('./manifest.json', fs.constants.R_OK | fs.constants.W_OK).catch(() => {
throw 'manifest.json not found';
});
// load manifest.json
let manifest = await new Promise(resolve => {
const raw = fs.readFileSync('./manifest.json').toString('utf8');
resolve(JSON.parse(raw));
}).catch(() => {
throw 'failed to parse manifest.json';
});
delete manifest.acct_id;
delete manifest.version;
delete manifest.draft;
// check if record exists on server
const oldManifest = await ws_1.ws(`widgets/${manifest.id}${options.global ? '?global=true' : ''}`).then(data => {
if (data) {
delete data.acct_id;
delete data.version;
delete data.draft;
}
return data;
}, err => {
return null;
});
// check if strings.json exists
await access('./strings.json', fs.constants.R_OK).catch(() => {
throw 'strings.json not found';
});
// load strings.json
const strings = await new Promise(resolve => {
const raw = fs.readFileSync('./strings.json').toString('utf8');
resolve(JSON.parse(raw));
}).catch(() => {
throw 'failed to parse strings.json';
});
// get the strings schema
let schemaUrl = await ws_1.wsUrl('schemas/string');
let parser = await jsonpolice.create({
type: 'array',
items: { $ref: schemaUrl },
minItems: 2
}, {
scope: await ws_1.wsUrl('schemas/string_array'),
retriever: ws_1.retriever
});
// validate the strings
await parser.validate(strings, { context: 'write' }).catch(err => {
throw `invalid format of strings.json, ${err.message} ${err.path || ''}`;
});
// update strings and put ids in manifest
manifest.stringIds = await strings_1.uploadWidgetStringChanges(manifest.id, strings, options.global);
if (manifest.stringIds.indexOf('NAME') === -1) {
throw 'NAME string missing';
}
if (manifest.stringIds.indexOf('DESCRIPTION') === -1) {
throw 'DESCRIPTION string missing';
}
// update assets and put data in manifest
let assets = await assets_1.scanWidgetAssets('.').catch(err => {
throw `failed to scan assets, ${err.message}`;
});
assets = await assets_1.hashWidgetAssets(assets).catch(err => {
throw `failed to hash assets, ${err.message}`;
});
if (manifest.assets && manifest.assets.length) {
for (let a of manifest.assets) {
if (a.id && ((options.global && a.id.indexOf('_/') !== 0) || (!options.global && a.id.indexOf('_/') === 0))) {
delete a.id;
}
}
}
await assets_1.uploadWidgetAssetChanges(manifest.id, !options.rescan && Array.isArray(manifest.assets) ? manifest.assets : [], assets, options.global).catch(err => {
throw `failed to upload assets, ${err.message}`;
});
manifest.assets = assets;
let a = manifest.assets.find(i => i.path === 'main.html');
if (!a) {
throw 'no main.html in the assets';
}
else {
manifest.htmlId = a.id;
}
a = manifest.assets.find(i => i.path === 'main.scss');
if (!a) {
throw 'no main.scss in the assets';
}
else {
manifest.scssId = a.id;
}
a = manifest.assets.find(i => i.path === 'thumbnail.png');
if (a) {
manifest.thumbnailId = a.id;
}
else {
delete manifest.thumbnailId;
}
// parse manifest with schema
schemaUrl = await ws_1.wsUrl(config.version === '2' ? 'schemas/widget_create' : 'schemas/widget');
parser = await jsonpolice.create(schemaUrl, {
scope: schemaUrl,
retriever: ws_1.retriever
});
// validate the strings
await parser.validate(manifest, { context: 'write' }).catch(err => {
throw `invalid format of manifest.json, ${err.message} ${err.path || ''}`;
});
if (!oldManifest) {
// create new
console.log('uploading manifest.json');
const newManifest = await ws_1.ws(`widgets${options.global ? '?global=true' : ''}`, {
method: 'POST',
body: manifest
}).catch(err => {
throw `failed to upload manifest.json, ${err.message}`;
});
console.log(`created first ${newManifest.draft ? 'draft' : 'version'}`);
}
else if (!_.isEqual(oldManifest, manifest)) {
// update server record
console.log('uploading manifest.json');
const newManifest = await ws_1.ws(`widgets/${manifest.id}${options.global ? '?global=true' : ''}`, {
method: 'PUT',
body: manifest
}).catch(err => {
throw `failed to upload manifest.json, ${err.message || err.name}`;
});
console.log(`saved version ${newManifest.version} ${newManifest.draft ? 'draft' : ''}`);
}
if (options.activate) {
console.log('activating the new version');
await ws_1.ws(`widgets/${manifest.id}/activate${options.global ? '?global=true' : ''}`, {
method: 'POST'
}).catch(err => {
throw `failed to activate the widget, ${err.message || err.name}`;
});
}
// update local manifest
fs.writeFileSync('./manifest.json', JSON.stringify(manifest, null, 2));
}
catch (e) {
console.error(e);
exitCode = 1;
}
finally {
process.chdir(startDir);
process.exit(exitCode);
}
}),
pull: program
.command('pull <widget_id> [version]')
.description('Pull a version of the widget from the Vivocha servers')
.option('-d, --directory <widget path>', 'Pull the widget into the specified path')
.option('-k, --keep-original-id', 'If the requested widget is global, do not add the suffix -custom to the pulled widget')
.option('-v, --verbose', 'Verbose output')
.action(async (widget_id, version, options) => {
const startDir = process.cwd();
let exitCode = 0;
let curr_widget_id = widget_id;
try {
// get the manifest
const manifest = await ws_1.ws(`widgets/${widget_id}${version ? '/' + version : ''}${options.global ? '?global=true' : ''}`).catch(() => {
throw `failed to download ${version ? 'the request version of ' : ''}widget ${widget_id}`;
});
if (!manifest) {
throw 'unknown widget';
}
if (!manifest.acct_id && !options.keepOriginalId) {
curr_widget_id += '-custom';
manifest.id = curr_widget_id;
}
delete manifest.acct_id;
delete manifest.version;
delete manifest.draft;
// check that the destination dir does not exist
const widgetDir = options.directory || `./${curr_widget_id}`;
await access(widgetDir).then(() => {
throw 'destination path already exists';
}, () => { });
// create the destination dir and move into it
await mkdirp(widgetDir).catch(() => {
throw `cannot create directory ${widgetDir}`;
});
process.chdir(widgetDir);
// download and write the strings
console.log(`Downloading strings.json`);
const strings = await strings_1.fetchWidgetStrings(widget_id, options.global).catch(() => {
throw 'failed to download the strings';
});
await writeFile('./strings.json', JSON.stringify(strings, null, 2), 'utf8').catch(() => {
throw 'failed to write the strings';
});
await assets_1.downloadAssets(manifest.assets);
// write the manifest
await writeFile('./manifest.json', JSON.stringify(manifest, null, 2), 'utf8').catch(() => {
throw 'failed to write the manifest';
});
console.log(`widget successfully pulled into directory ${widgetDir}`);
}
catch (e) {
console.error(e);
exitCode = 1;
}
finally {
process.chdir(startDir);
process.exit(exitCode);
}
}),
delete: program
.command('delete <widget_id>')
.description('Permanently delete all versions of a widget')
.option('-y, --yes', 'Do not ask for confirmation')
.option('-v, --verbose', 'Verbose output')
.action(async (widget_id, options) => {
const startDir = process.cwd();
let exitCode = 0;
try {
const proceed = options.yes ||
(await inquirer.prompt([
{
name: 'confirm',
type: 'confirm',
default: false,
message: 'WARNING: this operation is irreversible: are you sure you want to proceed?'
}
])).confirm;
if (proceed) {
await ws_1.ws(`widgets/${widget_id}${options.global ? '?global=true' : ''}`, { method: 'DELETE' }).catch(() => {
throw `failed to remove all version of widget ${widget_id}`;
});
console.log(`widget successfully removed`);
}
}
catch (e) {
console.error(e);
exitCode = 1;
}
finally {
process.chdir(startDir);
process.exit(exitCode);
}
}),
activate: program
.command('activate')
.description('Publish a draft as a new production version')
.option('-d, --directory <widget path>', 'Use the widget at the specified path', process.cwd())
.action(async (options) => {
const startDir = process.cwd();
let exitCode = 0;
try {
// change to the widget directory
if (options.directory !== startDir) {
process.chdir(options.directory);
}
// check if manifest.json exists
await access('./manifest.json', fs.constants.R_OK | fs.constants.W_OK).catch(() => {
throw 'manifest.json not found';
});
// load manifest.json
let manifest = await new Promise(resolve => {
const raw = fs.readFileSync('./manifest.json').toString('utf8');
resolve(JSON.parse(raw));
}).catch(() => {
throw 'failed to parse manifest.json';
});
await ws_1.ws(`widgets/${manifest.id}/activate${options.global ? '?global=true' : ''}`, {
method: 'POST'
}).catch(async (err) => {
let widget = await ws_1.ws(`widgets/${manifest.id}`, { method: 'GET' });
if (widget && !widget.draft) {
throw `widget ${manifest.id} is already active`;
}
else if (!widget) {
throw `failed to activate the widget ${manifest.id}, not found`;
}
else {
throw `failed to activate the widget ${manifest.id}, ${err.message || err.name}`;
}
})
.then(() => {
console.log(`widget ${manifest.id} activated`);
});
}
catch (e) {
console.log(e);
exitCode = 1;
}
finally {
process.chdir(startDir);
process.exit(exitCode);
}
}),
server: program
.command('server')
.description('Start a development server to test the widget on the local machine')
.option('-p, --port <port>', 'Server port, default 8085', 8085)
.option('-h, --host <host>', 'Server host, default localhost', 'localhost')
.option('-n, --no-open', 'Do not attempt to open the test app on a browser')
.option('-w, --watch', 'Automatically reload the page if any file change is detected')
.action(async (options) => {
const startDir = process.cwd();
try {
const manifest = JSON.parse(fs.readFileSync(path.join(startDir, 'manifest.json')).toString('utf8'));
if (manifest.type !== 'engagement') {
throw 'server mode only supports engagement widgets';
}
// update assets and put data in manifest
let assets = await assets_1.scanWidgetAssets('.').catch(err => {
throw `failed to scan assets, ${err.message}`;
});
assets = await assets_1.hashWidgetAssets(assets).catch(err => {
throw `failed to hash assets, ${err.message}`;
});
if (manifest.assets && manifest.assets.length) {
for (let a of manifest.assets) {
if (a.id && ((options.global && a.id.indexOf('_/') !== 0) || (!options.global && a.id.indexOf('_/') === 0))) {
delete a.id;
}
}
}
manifest.assets = assets;
for (let a of manifest.assets) {
if (!a.id) {
a.id = a.hash;
}
}
let a = manifest.assets.find(i => i.path === 'main.html');
if (!a) {
throw 'no main.html in the assets';
}
else {
manifest.htmlId = a.id;
}
a = manifest.assets.find(i => i.path === 'main.scss');
if (!a) {
throw 'no main.scss in the assets';
}
else {
manifest.scssId = a.id;
}
a = manifest.assets.find(i => i.path === 'thumbnail.png');
if (a) {
manifest.thumbnailId = a.id;
}
else {
delete manifest.thumbnailId;
}
const app = express();
app.set('port', options.port);
app.set('host', options.host);
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, '../app')));
app.use('/main.html', express.static(path.join(startDir, 'main.html')));
app.use('/main.scss', express.static(path.join(startDir, 'main.scss')));
app.use('/assets', express.static(path.join(startDir, 'assets')));
app.get('/widget', async (req, res) => {
let settings;
try {
settings = JSON.parse(fs.readFileSync(path.join(startDir, 'settings.json')).toString('utf8'));
}
catch (e) {
settings = {
templateId: manifest.id,
variables: (manifest.variables || []).reduce((o, i) => {
if (typeof i.defaultValue !== 'undefined') {
o[i.id] = i.defaultValue;
}
return o;
}, {}),
requestedLanguage: 'en',
defaultLanguage: 'en'
};
fs.writeFileSync(path.join(startDir, 'settings.json'), JSON.stringify(settings, null, 2));
}
const requestedLanguage = settings.requestedLanguage || 'en';
const defaultLanguage = settings.defaultLanguage || requestedLanguage || 'en';
delete settings.requestedLanguage;
delete settings.defaultLanguage;
const strings = language_1.getStringsObject(JSON.parse(fs.readFileSync(path.join(startDir, 'strings.json')).toString('utf8')), requestedLanguage, defaultLanguage);
res.json({
id: '' + +new Date(),
manifest,
settings,
strings,
requestedLanguage,
defaultLanguage,
assetsBaseUrl: '/'
});
});
const server = http.createServer(app);
const reloader = reload(app);
const serverUrl = `http://${options.host}:${options.port}`;
console.log(`starting debug server at ${serverUrl}`);
server.listen(options.port, options.host);
if (options.watch) {
fs.watch(startDir, 'utf8', (event, filename) => {
reloader.reload();
});
}
if (options.open !== false) {
openurl_1.open(serverUrl);
}
process.on('SIGTERM', () => {
process.exit(0);
});
process.on('SIGINT', () => {
process.exit(0);
});
}
catch (e) {
console.error(e);
process.exit(1);
}
})
};
if (config.info.scopes) {
const scopes = new scopes_1.Scopes(config.info.scopes);
if (scopes.match('Widget.global')) {
commands.list.option('-g, --global', 'List only global widgets');
commands.push.option('-g, --global', 'Push as global widget');
commands.pull.option('-g, --global', 'Pull a global version of the requested widget');
commands.activate.option('-g, --global', 'Activate a global widget');
commands.delete.option('-g, --global', 'Delete a global widget');
}
}
program.parse(process.argv);
// commander 4.0.0 uses rawArgs instead of args to read cli arguments
if (!program.rawArgs.length) {
program.help();
}
}
catch (err) {
console.error(err);
process.exit(1);
}
})();
//# sourceMappingURL=vvc-widget.js.map