Browse Source

Merge pull request #34 from zeit/add/now-list

Add now list
master
Tony Kovanen 9 years ago
parent
commit
c389f56d7e
  1. 195
      bin/now
  2. 174
      bin/now-deploy
  3. 68
      bin/now-list
  4. 6
      lib/agent.js
  5. 25
      lib/error.js
  6. 32
      lib/index.js
  7. 28
      lib/login.js
  8. 7
      package.json

195
bin/now

@ -1,53 +1,15 @@
#!/usr/bin/env node
import Progress from 'progress';
import copy from '../lib/copy';
import minimist from 'minimist';
import { resolve } from 'path';
import login from '../lib/login';
import * as cfg from '../lib/cfg';
import { version } from '../../package';
import { spawn } from 'child_process';
import checkUpdate from '../lib/check-update';
import Logger from '../lib/build-logger';
import bytes from 'bytes';
import chalk from 'chalk';
import minimist from 'minimist';
import Now from '../lib';
import ms from 'ms';
const argv = minimist(process.argv.slice(2));
const help = () => {
console.log(`
𝚫 now [options] <path>
Options:
-h, --help output usage information
-v, --version output the version number
-d, --debug Debug mode [off]
-f, --force Force a new deployment even if nothing has changed
-L, --login Configure login
-C, --no-clipboard Do not attempt to copy URL to clipboard
`);
};
let path = argv._[0];
if (path) {
if ('/' !== path[0]) {
path = resolve(process.cwd(), path);
}
} else {
path = process.cwd();
}
// options
const debug = argv.debug || argv.d;
const clipboard = !(argv.noClipboard || argv.C);
const force = argv.f || argv.force;
const forceSync = argv.F || argv.forceSync;
const shouldLogin = argv.L || argv.login;
// auto-update checking
const config = cfg.read();
const update = checkUpdate({ debug });
const exit = (code) => {
update.then(() => process.exit(code));
@ -56,146 +18,21 @@ const exit = (code) => {
setTimeout(() => process.exit(code), 1000);
};
if (argv.h || argv.help) {
help();
exit(0);
} else if (argv.v || argv.version) {
console.log(chalk.bold('𝚫 now'), version);
exit(0);
} else if (!config.token || shouldLogin) {
login()
.then((token) => {
if (shouldLogin) {
console.log('> Logged in successfully. Token saved in ~/.now.json');
exit(0);
} else {
sync(token).catch((err) => {
error(`Unknown error: ${err.stack}`);
exit(1);
});
}
})
.catch((e) => {
error(`Authentication error – ${e.message}`);
exit(1);
});
} else {
sync(config.token).catch((err) => {
error(`Unknown error: ${err.stack}`);
exit(1);
});
}
async function sync (token) {
const start = Date.now();
console.log(`> Deploying "${path}"`);
const now = new Now(token, { debug });
try {
await now.create(path, { forceNew: force, forceSync: forceSync });
} catch (err) {
handleError(err);
return;
}
const { url } = now;
const elapsed = ms(new Date() - start);
const commands = new Set(['deploy', 'list', 'ls']);
const aliases = new Map([['ls', 'list']]);
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}]`);
}
let cmd = argv._[0];
let args;
const start_u = new Date();
const complete = () => {
const elapsed_u = ms(new Date() - start_u);
console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsed_u}] `);
// 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', ({ name, data }) => {
const amount = data.length;
if (debug) {
console.log(`> [debug] Uploaded: ${name} (${bytes(data.length)})`);
}
bar.tick(amount);
});
now.on('complete', complete);
now.on('error', (err) => {
error('Upload failed');
handleError(err);
exit(1);
});
} else {
console.log('> Sync complete (cached)');
// close http2 agent
now.close();
// show build logs
printLogs(now.host);
}
}
function printLogs (host) {
// log build
const logger = new Logger(host);
logger.on('error', () => {
console.log('> Connection error.');
exit(1);
});
logger.on('close', () => {
console.log(`${chalk.cyan('> Deployment complete!')}`);
exit(0);
});
}
function handleError (err) {
if (403 === err.status) {
error('Authentication error. Run `now -L` or `now --login` to log-in again.');
} else if (429 === err.status) {
if (null != err.retryAfter) {
error('Rate limit exceeded error. Try again in ' +
ms(err.retryAfter * 1000, { long: true }) +
', or upgrade your account: https://zeit.co/now#pricing');
} else {
error('Rate limit exceeded error. Please try later.');
}
} else if (err.userError) {
error(err.message);
} else if (500 === err.status) {
error('Unexpected server error. Please retry.');
} else {
error(`Unexpected error. Please try later. (${err.message})`);
}
exit(1);
if (commands.has(cmd)) {
cmd = aliases.get(cmd) || cmd;
args = process.argv.slice(3);
} else {
cmd = 'deploy';
args = process.argv.slice(2);
}
function error (err) {
console.error(`> \u001b[31mError!\u001b[39m ${err}`);
}
const bin = resolve(__dirname, 'now-' + cmd);
const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
proc.on('close', (code) => exit(code));
proc.on('error', () => exit(1));

174
bin/now-deploy

@ -0,0 +1,174 @@
#!/usr/bin/env node
import Progress from 'progress';
import copy from '../lib/copy';
import { resolve } from 'path';
import login from '../lib/login';
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 ms from 'ms';
import { handleError, error } from '../lib/error';
const argv = minimist(process.argv.slice(2));
const help = () => {
console.log(`
𝚫 now [options] <command|path>
Commands:
list output list of instances
ls alias of list
Options:
-h, --help output usage information
-v, --version output the version number
-d, --debug Debug mode [off]
-f, --force Force a new deployment even if nothing has changed
-L, --login Configure login
-C, --no-clipboard Do not attempt to copy URL to clipboard
`);
};
let path = argv._[0];
if (path) {
if ('/' !== path[0]) {
path = resolve(process.cwd(), path);
}
} else {
path = process.cwd();
}
// options
const debug = argv.debug || argv.d;
const clipboard = !(argv.noClipboard || argv.C);
const force = argv.f || argv.force;
const forceSync = argv.F || argv.forceSync;
const shouldLogin = argv.L || argv.login;
const apiUrl = argv.url || 'https://api.now.sh';
const config = cfg.read();
if (argv.h || argv.help) {
help();
process.exit(0);
} else if (argv.v || argv.version) {
console.log(chalk.bold('𝚫 now'), version);
process.exit(0);
} else if (!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(config.token).catch((err) => {
error(`Unknown error: ${err.stack}`);
process.exit(1);
});
}
async function sync (token) {
const start = Date.now();
console.log(`> Deploying "${path}"`);
const now = new Now(apiUrl, token, { debug });
try {
await now.create(path, { forceNew: force, forceSync: forceSync });
} catch (err) {
handleError(err);
process.exit(1);
}
const { url } = now;
const elapsed = ms(new Date() - start);
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}]`);
}
const start_u = new Date();
const complete = () => {
const elapsed_u = ms(new Date() - start_u);
console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsed_u}] `);
// 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', ({ name, data }) => {
const amount = data.length;
if (debug) {
console.log(`> [debug] Uploaded: ${name} (${bytes(data.length)})`);
}
bar.tick(amount);
});
now.on('complete', complete);
now.on('error', (err) => {
error('Upload failed');
handleError(err);
process.exit(1);
});
} else {
console.log('> Sync complete (cached)');
// close http2 agent
now.close();
// show build logs
printLogs(now.host);
}
}
function printLogs (host) {
// log build
const logger = new Logger(host);
logger.on('error', () => {
console.log('> Connection error.');
process.exit(1);
});
logger.on('close', () => {
console.log(`${chalk.cyan('> Deployment complete!')}`);
process.exit(0);
});
}

68
bin/now-list

@ -0,0 +1,68 @@
#!/usr/bin/env node
import minimist from 'minimist';
import chalk from 'chalk';
import table from 'text-table';
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 app = argv._[0];
// options
const debug = argv.debug || argv.d;
const apiUrl = argv.url || 'https://api.now.sh';
const config = cfg.read();
Promise.resolve(config.token || login(apiUrl))
.then(async (token) => {
try {
await list(token);
} catch (err) {
error(`Unknown error: ${err.stack}`);
process.exit(1);
}
})
.catch((e) => {
error(`Authentication error – ${e.message}`);
process.exit(1);
});
async function list (token) {
const now = new Now(apiUrl, token, { debug });
let deployments;
try {
deployments = await now.list(app);
} catch (err) {
handleError(err);
process.exit(1);
}
now.close();
const apps = new Map();
for (const dep of deployments) {
const deps = apps.get(dep.name) || [];
apps.set(dep.name, deps.concat(dep));
}
const current = Date.now();
const text = [...apps].map(([name, deps]) => {
const t = table(deps.map(({ uid, url, created }) => {
const time = ms(current - created, { long: true }) + ' ago';
return [ uid, time, `https://${url}` ];
}), { align: ['l', 'r', 'l'] });
return chalk.bold(name) + '\n\n' + indent(t, 2).split('\n').join('\n\n');
}).join('\n\n');
if (text) console.log('\n' + text + '\n');
}
function indent (text, n) {
return text.split('\n').map((l) => ' '.repeat(n) + l).join('\n');
}

6
lib/agent.js

@ -12,8 +12,8 @@ import fetch from 'node-fetch';
*/
export default class Agent {
constructor (host, { debug } = {}) {
this._host = host;
constructor (url, { debug } = {}) {
this._url = url;
this._debug = debug;
this._initAgent();
}
@ -54,7 +54,7 @@ export default class Agent {
opts.headers['Content-Length'] = Buffer.byteLength(opts.body);
}
return fetch(`https://${this._host}${path}`, opts);
return fetch(this._url + path, opts);
}
close () {

25
lib/error.js

@ -0,0 +1,25 @@
import ms from 'ms';
export function handleError (err) {
if (403 === err.status) {
error('Authentication error. Run `now -L` or `now --login` to log-in again.');
} else if (429 === err.status) {
if (null != err.retryAfter) {
error('Rate limit exceeded error. Try again in ' +
ms(err.retryAfter * 1000, { long: true }) +
', or upgrade your account: https://zeit.co/now#pricing');
} else {
error('Rate limit exceeded error. Please try later.');
}
} else if (err.userError) {
error(err.message);
} else if (500 === err.status) {
error('Unexpected server error. Please retry.');
} else {
error(`Unexpected error. Please try later. (${err.message})`);
}
}
export function error (err) {
console.error(`> \u001b[31mError!\u001b[39m ${err}`);
}

32
lib/index.js

@ -20,12 +20,12 @@ const IS_WIN = /^win/.test(process.platform);
const SEP = IS_WIN ? '\\' : '/';
export default class Now extends EventEmitter {
constructor (token, { forceNew = false, debug = false }) {
constructor (url, token, { forceNew = false, debug = false }) {
super();
this._token = token;
this._debug = debug;
this._forceNew = forceNew;
this._agent = new Agent('api.now.sh', { debug });
this._agent = new Agent(url, { debug });
this._onRetry = this._onRetry.bind(this);
}
@ -159,6 +159,32 @@ export default class Now extends EventEmitter {
uploadChunk();
}
async list (app) {
const query = app ? `?app=${encodeURIComponent(app)}` : '';
const { deployments } = await retry(async (bail) => {
if (this._debug) console.time('> [debug] /list');
const res = await this._fetch('/list' + query);
if (this._debug) console.timeEnd('> [debug] /list');
// no retry on 4xx
if (400 <= res.status && 500 > res.status) {
if (this._debug) {
console.log('> [debug] bailing on listing due to %s', res.status);
}
return bail(responseError(res));
}
if (200 !== res.status) {
throw new Error('Fetching deployment list failed');
}
return res.json();
}, { retries: 3, minTimeout: 2500, onRetry: this._onRetry });
return deployments;
}
_onRetry (err) {
if (this._debug) {
console.log(`> [debug] Retrying: ${err.stack}`);
@ -190,7 +216,7 @@ export default class Now extends EventEmitter {
return this._syncAmount;
}
async _fetch (_url, opts) {
async _fetch (_url, opts = {}) {
opts.headers = opts.headers || {};
opts.headers.authorization = `Bearer ${this._token}`;
return await this._agent.fetch(_url, opts);

28
lib/login.js

@ -3,23 +3,19 @@ import fetch from 'node-fetch';
import * as cfg from './cfg';
import { stringify as stringifyQuery } from 'querystring';
const URL = 'https://api.now.sh/registration';
const stdin = process.openStdin();
function readEmail () {
return new Promise((resolve, reject) => {
process.stdout.write('> Enter your email address: ');
stdin.on('data', (d) => {
stdin.destroy();
process.stdin.on('data', (d) => {
process.stdin.pause();
resolve(d.toString().trim());
});
}).resume();
});
}
async function getVerificationToken (email) {
async function getVerificationToken (url, email) {
const data = JSON.stringify({ email });
const res = await fetch(URL, {
const res = await fetch(url + '/registration', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -36,13 +32,13 @@ async function getVerificationToken (email) {
return body.token;
}
async function verify (email, verificationToken) {
async function verify (url, email, verificationToken) {
const query = {
email,
token: verificationToken
};
const res = await fetch(`${URL}/verify?${stringifyQuery(query)}`);
const res = await fetch(`${url}/registration/verify?${stringifyQuery(query)}`);
const body = await res.json();
return body.token;
}
@ -53,9 +49,9 @@ function sleep (ms) {
});
}
async function register () {
async function register (url) {
const email = await readEmail();
const verificationToken = await getVerificationToken(email);
const verificationToken = await getVerificationToken(url, email);
console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`);
process.stdout.write('> Waiting for confirmation..');
@ -64,7 +60,7 @@ async function register () {
do {
await sleep(2500);
try {
final = await verify(email, verificationToken);
final = await verify(url, email, verificationToken);
} catch (e) {}
process.stdout.write('.');
} while (!final);
@ -74,8 +70,8 @@ async function register () {
return { email, token: final };
}
export default async function () {
const loginData = await register();
export default async function (url) {
const loginData = await register(url);
cfg.merge(loginData);
return loginData.token;
}

7
package.json

@ -11,6 +11,7 @@
"now": "./build/bin/now"
},
"dependencies": {
"ansi-escapes": "1.3.0",
"arr-flatten": "1.0.1",
"array-unique": "0.2.1",
"babel-runtime": "6.6.1",
@ -20,15 +21,15 @@
"fs-promise": "0.4.1",
"graceful-fs": "4.1.3",
"minimatch": "3.0.0",
"minimist": "1.2.0",
"ms": "0.7.1",
"node-fetch": "1.3.3",
"progress": "1.1.8",
"resumer": "0.0.0",
"retry": "0.9.0",
"socket.io-client": "1.4.5",
"split-array": "1.0.1",
"minimist": "1.2.0",
"ansi-escapes": "1.3.0",
"socket.io-client": "1.4.5"
"text-table": "0.2.0"
},
"devDependencies": {
"alpha-sort": "1.0.2",

Loading…
Cancel
Save