heroku
Version:
CLI to interact with Heroku
206 lines (205 loc) • 9.94 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const color_1 = require("@heroku-cli/color");
const command_1 = require("@heroku-cli/command");
const core_1 = require("@oclif/core");
const heroku_cli_util_1 = require("@heroku/heroku-cli-util");
const time_1 = require("../../lib/time");
const tsheredoc_1 = require("tsheredoc");
function getProcessNumber(s) {
const [processType, dynoNumber] = (s.match(/^([^.]+)\.(.*)$/) || []).slice(1, 3);
if (!processType || !(dynoNumber === null || dynoNumber === void 0 ? void 0 : dynoNumber.match(/^\d+$/)))
return 0;
return Number.parseInt(dynoNumber, 10);
}
function uniqueValues(value, index, self) {
return self.indexOf(value) === index;
}
function byProcessNumber(a, b) {
return getProcessNumber(a.name) - getProcessNumber(b.name);
}
function byProcessName(a, b) {
if (a.name > b.name) {
return 1;
}
if (b.name > a.name) {
return -1;
}
return 0;
}
function byProcessTypeAndNumber(a, b) {
if (a.type > b.type) {
return 1;
}
if (b.type > a.type) {
return -1;
}
return getProcessNumber(a.name) - getProcessNumber(b.name);
}
function truncate(s) {
return s.length > 35 ? `${s.slice(0, 34)}…` : s;
}
function printExtended(dynos) {
const sortedDynos = dynos.sort(byProcessTypeAndNumber);
heroku_cli_util_1.hux.table(sortedDynos, {
ID: { get: (dyno) => dyno.id },
Process: { get: (dyno) => dyno.name },
State: { get: (dyno) => `${dyno.state} ${(0, time_1.ago)(new Date(dyno.updated_at))}` },
Region: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.region) ? dyno.extended.region : ''; } },
'Execution Plane': { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.execution_plane) ? dyno.extended.execution_plane : ''; } },
Fleet: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.fleet) ? dyno.extended.fleet : ''; } },
Instance: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.instance) ? dyno.extended.instance : ''; } },
IP: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.ip) ? dyno.extended.ip : ''; } },
Port: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.port) ? dyno.extended.port.toString() : ''; } },
AZ: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.az) ? dyno.extended.az : ''; } },
Release: { get: (dyno) => dyno.release.version },
Command: { get: (dyno) => truncate(dyno.command) },
Route: { get: (dyno) => { var _a; return ((_a = dyno.extended) === null || _a === void 0 ? void 0 : _a.route) ? dyno.extended.route : ''; } },
Size: { get: (dyno) => dyno.size },
}, {
'no-truncate': true,
});
}
async function printAccountQuota(heroku, app, account) {
if (app.process_tier !== 'free' && app.process_tier !== 'eco') {
return;
}
if (app.owner.id !== account.id) {
return;
}
const { body: quota } = await heroku.request(`/accounts/${account.id}/actions/get-quota`, { headers: { Accept: 'application/vnd.heroku+json; version=3.account-quotas' } }).catch(() => {
return { body: null };
});
if (!quota || (quota.id && quota.id === 'not_found')) {
return;
}
const remaining = (quota.account_quota === 0) ? 0 : quota.account_quota - quota.quota_used;
const percentage = (quota.account_quota === 0) ? 0 : Math.floor(remaining / quota.account_quota * 100);
const remainingMinutes = remaining / 60;
const hours = Math.floor(remainingMinutes / 60);
const minutes = Math.floor(remainingMinutes % 60);
const appQuota = quota.apps.find(appQuota => {
return appQuota.app_uuid === app.id;
});
const appQuotaUsed = appQuota ? appQuota.quota_used / 60 : 0;
const appPercentage = appQuota ? Math.floor(appQuota.quota_used * 100 / quota.account_quota) : 0;
const appHours = Math.floor(appQuotaUsed / 60);
const appMinutes = Math.floor(appQuotaUsed % 60);
if (app.process_tier === 'eco') {
core_1.ux.log(`Eco dyno hours quota remaining this month: ${hours}h ${minutes}m (${percentage}%)`);
core_1.ux.log(`Eco dyno usage for this app: ${appHours}h ${appMinutes}m (${appPercentage}%)`);
core_1.ux.log('For more information on Eco dyno hours, see:');
core_1.ux.log('https://devcenter.heroku.com/articles/eco-dyno-hours');
core_1.ux.log();
}
if (app.process_tier === 'free') {
core_1.ux.log(`Free dyno hours quota remaining this month: ${hours}h ${minutes}m (${percentage}%)`);
core_1.ux.log(`Free dyno usage for this app: ${appHours}h ${appMinutes}m (${appPercentage}%)`);
core_1.ux.log('For more information on dyno sleeping and how to upgrade, see:');
core_1.ux.log('https://devcenter.heroku.com/articles/dyno-sleeping');
core_1.ux.log();
}
}
function decorateOneOffDyno(dyno) {
const since = (0, time_1.ago)(new Date(dyno.updated_at));
// eslint-disable-next-line unicorn/explicit-length-check
const size = dyno.size || '1X';
const state = dyno.state === 'up' ? color_1.default.green(dyno.state) : color_1.default.yellow(dyno.state);
return `${dyno.name} (${color_1.default.cyan(size)}): ${state} ${color_1.default.dim(since)}: ${dyno.command}`;
}
function decorateCommandDyno(dyno) {
const since = (0, time_1.ago)(new Date(dyno.updated_at));
const state = dyno.state === 'up' ? color_1.default.green(dyno.state) : color_1.default.yellow(dyno.state);
return `${dyno.name}: ${state} ${color_1.default.dim(since)}`;
}
function printDynos(dynos) {
const oneOffs = dynos.filter(d => d.type === 'run').sort(byProcessNumber);
const commands = dynos.filter(d => d.type !== 'run').map(d => d.command).filter(uniqueValues);
// Print one-off dynos
if (oneOffs.length > 0) {
heroku_cli_util_1.hux.styledHeader(`${color_1.default.green('run')}: one-off processes (${color_1.default.yellow(oneOffs.length.toString())})`);
oneOffs.forEach(dyno => core_1.ux.log(decorateOneOffDyno(dyno)));
core_1.ux.log();
}
// Print dynos grouped by command
commands.forEach(function (command) {
const commandDynos = dynos.filter(d => d.command === command).sort(byProcessNumber);
const type = commandDynos[0].type;
// eslint-disable-next-line unicorn/explicit-length-check
const size = commandDynos[0].size || '1X';
heroku_cli_util_1.hux.styledHeader(`${color_1.default.green(type)} (${color_1.default.cyan(size)}): ${command} (${color_1.default.yellow(commandDynos.length.toString())})`);
for (const dyno of commandDynos)
core_1.ux.log(decorateCommandDyno(dyno));
core_1.ux.log();
});
}
class Index extends command_1.Command {
async run() {
const _a = await this.parse(Index), { flags } = _a, restParse = tslib_1.__rest(_a, ["flags"]);
const { app, json, extended } = flags;
const types = restParse.argv;
const suffix = extended ? '?extended=true' : '';
const promises = {
dynos: this.heroku.request(`/apps/${app}/dynos${suffix}`, {
headers: { Accept: 'application/vnd.heroku+json; version=3.sdk' },
}),
appInfo: this.heroku.request(`/apps/${app}`, {
headers: { Accept: 'application/vnd.heroku+json; version=3.sdk' },
}),
accountInfo: this.heroku.request('/account', {
headers: { Accept: 'application/vnd.heroku+json; version=3.sdk' },
}),
};
const [{ body: dynos }, { body: appInfo }, { body: accountInfo }] = await Promise.all([promises.dynos, promises.appInfo, promises.accountInfo]);
const shielded = appInfo.space && appInfo.space.shield;
if (shielded) {
dynos.forEach(d => {
d.size = d.size.replace('Private-', 'Shield-');
});
}
let selectedDynos = dynos;
if (types.length > 0) {
selectedDynos = selectedDynos.filter(dyno => types.find((t) => dyno.type === t));
types.forEach(t => {
if (!selectedDynos.some(d => d.type === t)) {
throw new Error(`No ${color_1.default.cyan(t)} dynos on ${color_1.default.magenta(app)}`);
}
});
}
selectedDynos = selectedDynos.sort(byProcessName);
if (json)
heroku_cli_util_1.hux.styledJSON(selectedDynos);
else if (extended)
printExtended(selectedDynos);
else {
await printAccountQuota(this.heroku, appInfo, accountInfo);
if (selectedDynos.length === 0)
core_1.ux.log(`No dynos on ${color_1.default.magenta(app)}`);
else
printDynos(selectedDynos);
}
}
}
exports.default = Index;
Index.topic = 'ps';
Index.description = 'list dynos for an app';
Index.strict = false;
Index.usage = 'ps [TYPE [TYPE ...]]';
Index.examples = [(0, tsheredoc_1.default) `
$ heroku ps
=== run: one-off dyno
run.1: up for 5m: bash
=== web: bundle exec thin start -p $PORT
web.1: created for 30s
`, (0, tsheredoc_1.default) `
$ heroku ps run # specifying types
=== run: one-off dyno
run.1: up for 5m: bash
`];
Index.flags = {
app: command_1.flags.app({ required: true }),
remote: command_1.flags.remote(),
json: command_1.flags.boolean({ description: 'display as json' }),
extended: command_1.flags.boolean({ char: 'x', hidden: true }), // only works with sudo privileges
};
;