diff --git a/bin/now b/bin/now index 73c1978..d3ae0b6 100755 --- a/bin/now +++ b/bin/now @@ -19,8 +19,8 @@ const exit = (code) => { }; const defaultCommand = 'deploy'; -const commands = new Set([defaultCommand, 'list', 'ls']); -const aliases = new Map([['ls', 'list']]); +const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove']); +const aliases = new Map([['ls', 'list'], ['rm', 'remove']]); let cmd = argv._[0]; let args = []; diff --git a/bin/now-deploy b/bin/now-deploy index e2e282c..7ade442 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -22,6 +22,8 @@ const help = () => { list [app] output list of instances ls [app] alias of list + remove [id] deletes the specified deployment + rm [id] alias of remove help [cmd] display help for [cmd] Options: diff --git a/bin/now-remove b/bin/now-remove new file mode 100755 index 0000000..2d15f41 --- /dev/null +++ b/bin/now-remove @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +import minimist from 'minimist'; +import chalk from 'chalk'; +import ms from 'ms'; +import Now from '../lib'; +import login from '../lib/login'; +import * as cfg from '../lib/cfg'; +import { handleError, error } from '../lib/error'; + +const argv = minimist(process.argv.slice(2)); +const deploymentId = argv._[0]; + +// options +const help = () => { + console.log(` + 𝚫 now remove [deploymentId] + + Alias: rm + + Options: + + -h, --help output usage information + -d, --debug Debug mode [off] +`); +}; + +if (argv.h || argv.help) { + help(); + process.exit(0); +} + +if (!deploymentId) { + error('No deployment id specified. You can see active deployments with `now ls`.'); + process.exit(1); +} + +// options +const debug = argv.debug || argv.d; +const apiUrl = argv.url || 'https://api.now.sh'; +const hard = argv.hard || false; + +const config = cfg.read(); + +function readConfirmation () { + return new Promise((resolve, reject) => { + process.stdout.write(`${chalk.cyan('> Are you sure? ')} ${chalk.bold(deploymentId)} ${chalk.cyan('will be removed permanently')} [${chalk.bold('yN')}]`); + process.stdin.on('data', (d) => { + process.stdin.pause(); + resolve(d.toString().trim()); + }).resume(); + }); +} + +Promise.resolve(config.token || login(apiUrl)) +.then(async (token) => { + try { + await remove(token); + } catch (err) { + error(`Unknown error: ${err.stack}`); + process.exit(1); + } +}) +.catch((e) => { + error(`Authentication error – ${e.message}`); + process.exit(1); +}); + +async function remove (token) { + const now = new Now(apiUrl, token, { debug }); + + try { + const confirmation = (await readConfirmation()).toLowerCase(); + if ('y' !== confirmation) { + console.log(`${chalk.cyan('> Aborted ')}`); + return; + } + + const start = new Date(); + await now.remove(deploymentId, { hard }); + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Deployment ')} ${chalk.bold(deploymentId)} ${chalk.cyan('removed')} [${elapsed}]`); + } catch (err) { + handleError(err); + process.exit(1); + } + + now.close(); +} diff --git a/lib/index.js b/lib/index.js index 4d2f757..ba2bccb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -185,6 +185,33 @@ export default class Now extends EventEmitter { return deployments; } + async remove (deploymentId, { hard }) { + const data = { deploymentId, hard }; + + await retry(async (bail) => { + if (this._debug) console.time('> [debug] /remove'); + const res = await this._fetch('/remove', { + method: 'DELETE', + body: data + }); + if (this._debug) console.timeEnd('> [debug] /remove'); + + // no retry on 4xx + if (400 <= res.status && 500 > res.status) { + if (this._debug) { + console.log('> [debug] bailing on removal due to %s', res.status); + } + return bail(responseError(res)); + } + + if (200 !== res.status) { + throw new Error('Removing deployment failed'); + } + }, { retries: 3, minTimeout: 2500, onRetry: this._onRetry }); + + return true; + } + _onRetry (err) { if (this._debug) { console.log(`> [debug] Retrying: ${err.stack}`);