#!/usr/bin/env node import Progress from 'progress'; import copy from '../lib/copy'; import { resolve } from 'path'; import login from '../lib/login'; import { stat } from 'fs-promise'; import * as cfg from '../lib/cfg'; import { version } from '../../package'; import Logger from '../lib/build-logger'; import bytes from 'bytes'; import chalk from 'chalk'; import minimist from 'minimist'; import Now from '../lib'; import toHumanPath from '../lib/utils/to-human-path'; import promptOptions from '../lib/utils/prompt-options'; import ms from 'ms'; import { handleError, error } from '../lib/error'; const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], boolean: ['help', 'version', 'debug', 'force', 'login', 'no-clipboard', 'forward-npm', 'docker', 'npm'], alias: { env: 'e', help: 'h', config: 'c', debug: 'd', version: 'v', force: 'f', token: 't', forceSync: 'F', login: 'L', public: 'p', 'no-clipboard': 'C', 'forward-npm': 'N' } }); const help = () => { console.log(` ${chalk.bold('𝚫 now')} [options] ${chalk.dim('Commands:')} deploy [path] performs a deployment ${chalk.bold('(default)')} ls | list [app] list deployments rm | remove [id] remove a deployment ln | alias [id] [url] configures aliases for deployments domains [name] manages your domain names help [cmd] displays complete help for [cmd] ${chalk.dim('Options:')} -h, --help output usage information -v, --version output the version number -c ${chalk.underline('FILE')}, --config=${chalk.underline('FILE')} config file -d, --debug debug mode [off] -f, --force force a new deployment even if nothing has changed -t ${chalk.underline('TOKEN')}, --token=${chalk.underline('TOKEN')} login token -L, --login configure login -p, --public deployment is public (${chalk.dim('`/_src`')} is exposed) [on for oss, off for premium] -e, --env include an env var (e.g.: ${chalk.dim('`-e KEY=value`')}). Can appear many times. -C, --no-clipboard do not attempt to copy URL to clipboard -N, --forward-npm forward login information to install private NPM modules ${chalk.dim('Examples:')} ${chalk.gray('–')} Deploys the current directory ${chalk.cyan('$ now')} ${chalk.gray('–')} Deploys a custom path ${chalk.dim('`/usr/src/project`')} ${chalk.cyan('$ now /usr/src/project')} ${chalk.gray('–')} Lists all deployments with their IDs ${chalk.cyan('$ now ls')} ${chalk.gray('–')} Associates deployment ${chalk.dim('`deploymentId`')} with ${chalk.dim('`custom-domain.com`')} ${chalk.cyan('$ now alias deploymentId custom-domain.com')} ${chalk.gray('–')} Stores a secret ${chalk.cyan('$ now secret add mysql-password 123456')} ${chalk.gray('–')} Deploys with ENV vars (using the ${chalk.dim('`mysql-password`')} secret stored above) ${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')} ${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim('`list`')} ${chalk.cyan('$ now help list')} `); }; let path = argv._[0]; if (null != path) { // if path is relative: resolve // if path is absolute: clear up strange `/` etc path = resolve(process.cwd(), path); } else { path = process.cwd(); } const exit = (code) => { // we give stdout some time to flush out // because there's a node bug where // stdout writes are asynchronous // https://github.com/nodejs/node/issues/6456 setTimeout(() => process.exit(code || 0), 100); }; // options const debug = argv.debug; const clipboard = !argv['no-clipboard']; const forwardNpm = argv['forward-npm']; const forceNew = argv.force; const forceSync = argv.forceSync; const shouldLogin = argv.login; const wantsPublic = argv.public; const apiUrl = argv.url || 'https://api.zeit.co'; const isTTY = process.stdout.isTTY; const quiet = !isTTY; if (argv.config) cfg.setConfigFile(argv.config); const config = cfg.read(); const alwaysForwardNpm = config.forwardNpm; if (argv.h || argv.help) { help(); exit(0); } else if (argv.v || argv.version) { console.log(chalk.bold('𝚫 now'), version); process.exit(0); } else if (!(argv.token || config.token) || shouldLogin) { login(apiUrl) .then((token) => { if (shouldLogin) { console.log('> Logged in successfully. Token saved in ~/.now.json'); process.exit(0); } else { sync(token).catch((err) => { error(`Unknown error: ${err.stack}`); process.exit(1); }); } }) .catch((e) => { error(`Authentication error – ${e.message}`); process.exit(1); }); } else { sync(argv.token || config.token).catch((err) => { error(`Unknown error: ${err.stack}`); process.exit(1); }); } async function sync (token) { const start = Date.now(); if (!quiet) { console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`); } try { await stat(path); } catch (err) { error(`Could not read directory ${chalk.bold(path)}`); process.exit(1); } let deploymentType, hasPackage, hasDockerfile; if (argv.docker) { if (debug) { console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``); } deploymentType = 'docker'; } else { if (argv.npm) { deploymentType = 'npm'; } else { try { await stat(resolve(path, 'package.json')); } catch (err) { hasPackage = true; } [hasPackage, hasDockerfile] = await Promise.all([ await (async () => { try { await stat(resolve(path, 'package.json')); } catch (err) { return false; } return true; })(), await (async () => { try { await stat(resolve(path, 'Dockerfile')); } catch (err) { return false; } return true; })() ]); if (hasPackage && hasDockerfile) { if (debug) console.log('[debug] multiple manifests found, disambiguating'); if (isTTY) { try { console.log(`> Two manifests found. Press [${chalk.bold('n')}] to deploy or re-run with --flag`); deploymentType = await promptOptions([ ['npm', `${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `], ['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `] ]); } catch (err) { error(err.message); process.exit(1); } } else { error('Ambiguous deployment (`package.json` and `Dockerfile` found). ' + 'Please supply `--npm` or `--docker` to disambiguate.'); } } else if (hasPackage) { if (debug) console.log('[debug] `package.json` found, assuming `deploymentType` = `npm`'); deploymentType = 'npm'; } else if (hasDockerfile) { if (debug) console.log('[debug] `Dockerfile` found, assuming `deploymentType` = `docker`'); deploymentType = 'docker'; } else { error(`Could not access a ${chalk.dim('`package.json`')} or ${chalk.dim('`Dockerfile`')} in ${chalk.bold(path)}`); console.log(`> To deploy statically, try: ${chalk.dim(chalk.underline('https://zeit.co/blog/serve-it-now'))}.`); process.exit(1); } } } const now = new Now(apiUrl, token, { debug }); const envs = [].concat(argv.env || []); let secrets; const findSecret = async (uidOrName) => { if (!secrets) secrets = await now.listSecrets(); return secrets.filter((secret) => { return secret.name === uidOrName || secret.uid === uidOrName; }); }; const env_ = await Promise.all(envs.map(async (kv) => { const [key, val_, ...rest] = kv.split('='); let val; if (rest.length) { error(`Invalid env ${chalk.bold(`"${kv}"`)}. It cannot contain more than one ${chalk.dim(`=`)} symbol`); return process.exit(1); } if (/[^A-z0-9_]/i.test(key)) { error(`Invalid ${chalk.dim('-e')} key ${chalk.bold(`"${chalk.bold(key)}"`)}. Only letters, digits and underscores are allowed.`); return process.exit(1); } if ('' === key || null == key) { error(`Invalid env option ${chalk.bold(`"${kv}"`)}`); return process.exit(1); } if (val_ == null) { if (!(key in process.env)) { error(`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`); return process.exit(1); } else { console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`); // escape value if it begins with @ val = process.env[key].replace(/^\@/, '\\@'); } } else { val = val_; } if ('@' === val[0]) { const uidOrName = val.substr(1); const secrets = await findSecret(uidOrName); if (secrets.length === 0) { if ('' === uidOrName) { error(`Empty reference provided for env key ${chalk.bold(`"${chalk.bold(key)}"`)}`); } else { error(`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`); } return process.exit(1); } else if (secrets.length > 1) { error(`Ambiguous secret ${chalk.bold(`"${uidOrName}"`)} (matches ${chalk.bold(secrets.length)} secrets)`); return process.exit(1); } else { val = { uid: secrets[0].uid }; } } return [ key, typeof val === 'string' // add support for escaping the @ as \@ ? val.replace(/^\\@/, '@') : val ]; })); let env = {}; env_ .filter(v => !!v) .forEach(([key, val]) => { if (key in env) console.log(`> ${chalk.yellow('NOTE:')} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`); env[key] = val }); try { await now.create(path, { env, deploymentType, forceNew, forceSync, forwardNpm: alwaysForwardNpm || forwardNpm, quiet, wantsPublic }); } catch (err) { if (debug) console.log(`> [debug] error: ${err.stack}`); handleError(err); process.exit(1); } const { url } = now; const elapsed = ms(new Date() - start); if (isTTY) { if (clipboard) { try { await copy(url); console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} (copied to clipboard) [${elapsed}]`); } catch (err) { console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} [${elapsed}]`); } } else { console.log(`> ${url} [${elapsed}]`); } } else { process.stdout.write(url); } const start_u = new Date(); const complete = () => { if (!quiet) { const elapsed_u = ms(new Date() - start_u); console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsed_u}] `); console.log('> Initializing…'); } // close http2 agent now.close(); // show build logs printLogs(now.host); }; if (now.syncAmount) { const bar = new Progress('> Upload [:bar] :percent :etas', { width: 20, complete: '=', incomplete: '', total: now.syncAmount }); now.upload(); now.on('upload', ({ names, data }) => { const amount = data.length; if (debug) { console.log(`> [debug] Uploaded: ${names.join(' ')} (${bytes(data.length)})`); } bar.tick(amount); }); now.on('complete', complete); now.on('error', (err) => { error('Upload failed'); handleError(err); process.exit(1); }); } else { if (!quiet) { console.log(`> Initializing…`); } // close http2 agent now.close(); // show build logs printLogs(now.host); } } function printLogs (host) { // log build const logger = new Logger(host, { debug, quiet }); logger.on('close', () => { if (!quiet) { console.log(`${chalk.cyan('> Deployment complete!')}`); } process.exit(0); }); }