UNPKG

vvc

Version:

Vivocha Command Line Tools

568 lines 28.5 kB
#!/usr/bin/env node "use strict"; 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