diff --git a/src/providers/sh/commands/bin/remove.js b/src/providers/sh/commands/bin/remove.js new file mode 100644 index 0000000..c076c62 --- /dev/null +++ b/src/providers/sh/commands/bin/remove.js @@ -0,0 +1,225 @@ +#!/usr/bin/env node + +// Packages +const minimist = require('minimist') +const chalk = require('chalk') +const ms = require('ms') +const table = require('text-table') + +// Ours +const Now = require('../lib') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const { handleError, error } = require('../lib/error') +const logo = require('../lib/utils/output/logo') +const { normalizeURL } = require('../lib/utils/url') + +const help = () => { + console.log(` + ${chalk.bold( + `${logo} now remove` + )} deploymentId|deploymentName [...deploymentId|deploymentName] + + ${chalk.dim('Options:')} + + -h, --help Output usage information + -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline( + 'FILE' + )} Config file + -d, --debug Debug mode [off] + -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( + 'TOKEN' + )} Login token + -y, --yes Skip confirmation + --safe Skip deployments with an active alias + + ${chalk.dim('Examples:')} + + ${chalk.gray('–')} Remove a deployment identified by ${chalk.dim( + '`deploymentId`' + )}: + + ${chalk.cyan('$ now rm deploymentId')} + + ${chalk.gray('–')} Remove all deployments with name ${chalk.dim('`my-app`')}: + + ${chalk.cyan('$ now rm my-app')} + + ${chalk.gray('–')} Remove two deployments with IDs ${chalk.dim( + '`eyWt6zuSdeus`' + )} and ${chalk.dim('`uWHoA9RQ1d1o`')}: + + ${chalk.cyan('$ now rm eyWt6zuSdeus uWHoA9RQ1d1o')} + + ${chalk.dim('Alias:')} rm +`) +} + +// Options +let argv +let debug +let apiUrl +let hard +let skipConfirmation +let ids + +const main = async ctx => { + argv = minimist(ctx.argv.slice(2), { + string: ['config', 'token'], + boolean: ['help', 'debug', 'hard', 'yes', 'safe'], + alias: { + help: 'h', + config: 'c', + debug: 'd', + token: 't', + yes: 'y' + } + }) + + argv._ = argv._.slice(1) + + debug = argv.debug + apiUrl = argv.url || 'https://api.zeit.co' + hard = argv.hard || false + skipConfirmation = argv.yes || false + ids = argv._ + + if (argv.help || ids.length === 0) { + help() + process.exit(0) + } + + const config = await cfg.read({ token: argv.token }) + + let token + try { + token = config.token || (await login(apiUrl)) + } catch (err) { + error(`Authentication error – ${err.message}`) + process.exit(1) + } + + try { + await remove({ token, config }) + } catch (err) { + error(`Unknown error: ${err}\n${err.stack}`) + process.exit(1) + } +} + +module.exports = async ctx => { + try { + await main(ctx) + } catch (err) { + handleError(err) + process.exit(1) + } +} + +function readConfirmation(matches) { + return new Promise(resolve => { + process.stdout.write( + `> The following deployment${matches.length === 1 + ? '' + : 's'} will be removed permanently:\n` + ) + + const tbl = table( + matches.map(depl => { + const time = chalk.gray(ms(new Date() - depl.created) + ' ago') + const url = depl.url ? chalk.underline(`https://${depl.url}`) : '' + return [depl.uid, url, time] + }), + { align: ['l', 'r', 'l'], hsep: ' '.repeat(6) } + ) + process.stdout.write(tbl + '\n') + + for (const depl of matches) { + for (const alias of depl.aliases) { + process.stdout.write( + `> ${chalk.yellow('Warning!')} Deployment ${chalk.bold(depl.uid)} ` + + `is an alias for ${chalk.underline( + `https://${alias.alias}` + )} and will be removed.\n` + ) + } + } + + process.stdout.write( + `${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}` + ) + + process.stdin + .on('data', d => { + process.stdin.pause() + resolve(d.toString().trim()) + }) + .resume() + }) +} + +async function remove({ token, config: { currentTeam } }) { + const now = new Now({ apiUrl, token, debug, currentTeam }) + + const deployments = await now.list() + + let matches = deployments.filter(d => { + return ids.some(id => { + return d.uid === id || d.name === id || d.url === normalizeURL(id) + }) + }) + + const aliases = await Promise.all( + matches.map(depl => now.listAliases(depl.uid)) + ) + + matches = matches.filter((match, i) => { + if (argv.safe && aliases[i].length > 0) { + return false + } + + match.aliases = aliases[i] + return true + }) + + if (matches.length === 0) { + error( + `Could not find ${argv.safe + ? 'unaliased' + : 'any'} deployments matching ${ids + .map(id => chalk.bold(`"${id}"`)) + .join(', ')}. Run ${chalk.dim(`\`now ls\``)} to list.` + ) + return process.exit(1) + } + + try { + if (!skipConfirmation) { + const confirmation = (await readConfirmation(matches)).toLowerCase() + + if (confirmation !== 'y' && confirmation !== 'yes') { + console.log('\n> Aborted') + process.exit(0) + } + } + + const start = new Date() + + await Promise.all(matches.map(depl => now.remove(depl.uid, { hard }))) + + const elapsed = ms(new Date() - start) + console.log(`${chalk.cyan('> Success!')} [${elapsed}]`) + console.log( + table( + matches.map(depl => { + return [`Deployment ${chalk.bold(depl.uid)} removed`] + }) + ) + ) + } catch (err) { + handleError(err) + process.exit(1) + } + + now.close() +} diff --git a/src/providers/sh/index.js b/src/providers/sh/index.js index c8f009f..5cb42e9 100644 --- a/src/providers/sh/index.js +++ b/src/providers/sh/index.js @@ -9,7 +9,9 @@ module.exports = { 'scale', 'certs', 'dns', - 'domains' + 'domains', + 'rm', + 'remove' ]), get deploy() { return require('./deploy') @@ -37,5 +39,11 @@ module.exports = { }, get domains() { return require('./commands/bin/domains') + }, + get rm() { + return require('./commands/bin/remove') + }, + get remove() { + return require('./commands/bin/remove') } }