@adpt/cli
Version:
AdaptJS command line interface
227 lines • 8.9 kB
JavaScript
;
/*
* Copyright 2020 Unbounded Systems, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("@adpt/utils");
const boxen_1 = tslib_1.__importDefault(require("boxen"));
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const child_process_1 = require("child_process");
const debug_1 = tslib_1.__importDefault(require("debug"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const os_1 = tslib_1.__importDefault(require("os"));
const path_1 = tslib_1.__importDefault(require("path"));
const semver_1 = tslib_1.__importDefault(require("semver"));
// tslint:disable-next-line: no-var-requires
const pjson = require("../../../package.json");
const debug = debug_1.default("adapt:upgrade");
const checkerDefaults = {
timeout: 30 * 1000,
};
class UpgradeChecker {
constructor(config, state) {
this.state = state;
this.config = Object.assign({}, checkerDefaults, config);
}
async check() {
const interval = this.config.upgradeCheckInterval;
if ((Date.now() - this.state.get("lastUpgradeCheck")) < interval) {
debug(`Not time to check (interval=${interval})`);
return;
}
await fs_extra_1.default.ensureDir(this.config.logDir);
const logFile = path_1.default.join(this.config.logDir, "upgrade-check.log");
const logFd = await fs_extra_1.default.open(logFile, "a");
const childPath = path_1.default.join(__dirname, "check.js");
const args = [childPath, JSON.stringify(this.config)];
debug(`Spawning child to check for upgrade: ${args}`);
const child = child_process_1.spawn(process.execPath, args, {
detached: !isWindows(),
stdio: ["ignore", logFd, logFd],
});
child.unref();
}
async notifyString(options = {}) {
const upgradeInfo = this.upgrade;
let current = this.state.get("version");
let latest = upgradeInfo && upgradeInfo.latest;
const summary = upgradeInfo && upgradeInfo.summary;
const latestIsNewer = latest && semver_1.default.gt(latest, current);
const now = remindNow(this.state.get("lastUpgradeReminder"), this.config.upgradeRemindInterval);
const ignoreVer = this.config.upgradeIgnore;
if (!process.stdout.isTTY || !latest || !latestIsNewer || !now || latest === ignoreVer) {
debug(`Not notifying: tty=${process.stdout.isTTY} ` +
`current=${current} latest=${latest} now=${now} ignoreVer=${ignoreVer}`);
return undefined;
}
const pkg = pjson.name;
let cmd;
switch (await utils_1.installType(__dirname)) {
case utils_1.InstallType.npmGlobal:
cmd = "npm install -g";
break;
case utils_1.InstallType.yarnGlobal:
cmd = "yarn global add";
break;
case utils_1.InstallType.yarn:
cmd = "yarn add";
break;
case utils_1.InstallType.npm:
default:
cmd = "npm install";
break;
}
cmd += ` ${pkg}@${latest}`;
let ignore = "Ignore:";
let ignoreCmd = `adapt config:set upgradeIgnore ${latest}`;
if (options.fancy) {
current = chalk_1.default.dim(current);
latest = chalk_1.default.greenBright(latest);
cmd = chalk_1.default.bold.cyanBright(cmd);
ignore = chalk_1.default.dim(ignore);
ignoreCmd = chalk_1.default.cyan(ignoreCmd);
}
let output = `Upgrade available: ${current} → ${latest}\n`;
if (summary && (summary.description || summary.securityFixes)) {
output += "\n";
if (summary.description) {
output += `${latest}: ${summary.description}\n`;
}
if (summary.securityFixes) {
output += `This upgrade contains security fixes\n`;
}
}
output +=
`\nUpgrade: ${cmd}\n` +
`${ignore} ${ignoreCmd}`;
if (options.fancy) {
output = boxen_1.default(output, {
borderStyle: "bold" /* Bold */,
float: "center",
margin: 1,
padding: 1,
});
}
this.state.set("lastUpgradeReminder", Date.now());
return output;
}
/**
* This method should not be called directly. It is called when the
* check method invokes a subprocess that executes check.ts, which calls
* this method.
*/
async fetch() {
const version = this.state.get("version");
// Understand how long users run a particular version before
// updating so we can evaluate deprecation/support timelines.
const q = `?x-installed=${this.state.get("installed")}`;
const ua = await utils_1.userAgent({
name: pjson.name,
version,
docker: true,
});
let resp;
try {
resp = await node_fetch_1.default(this.config.upgradeCheckUrl + q, {
headers: { "User-Agent": ua },
timeout: this.config.timeout,
});
}
catch (err) {
if (err.name === "FetchError") {
throw new utils_1.UserError(`Error fetching version information: ${err.message}`);
}
throw err;
}
const body = await resp.text();
if (resp.status !== 200) {
const trimmed = body.trim();
let msg = `Status ${resp.status} (${resp.statusText})`;
if (trimmed)
msg += " " + trimmed;
throw new utils_1.UserError(msg);
}
debug("Upgrade check response:", body);
let ret;
try {
ret = JSON.parse(body);
}
catch (err) {
if (err.name === "SyntaxError") {
throw new utils_1.UserError(`Invalid JSON response: ${err.message}`);
}
throw err;
}
if (!validateSummary(ret))
throw new Error(`Invalid upgrade check response`);
const latest = ret.channelCurrent[this.config.channel];
if (!latest) {
throw new utils_1.UserError(`No information available for channel '${this.config.channel}'`);
}
const verSummary = ret.versions[latest];
const summary = {
channel: this.config.channel,
description: verSummary && verSummary.description,
securityFixes: hasSecurityFixes(version, latest, this.config.channel, ret),
};
return {
latest,
summary,
};
}
get upgrade() {
return this.state.get("upgrade");
}
}
exports.UpgradeChecker = UpgradeChecker;
class SummaryError extends utils_1.UserError {
constructor(msg) {
super(`Invalid response: ${msg}`);
}
}
function validateSummary(obj) {
if (!utils_1.isObject(obj))
throw new SummaryError(`Response was not a valid object`);
if (typeof obj.name !== "string")
throw new SummaryError(`Invalid name property`);
if (!obj.name.includes("cli"))
throw new SummaryError(`Unrecognized package name`);
if (!utils_1.isObject(obj.channelCurrent))
throw new SummaryError(`Invalid channelCurrent property`);
if (!utils_1.isObject(obj.versions))
throw new SummaryError(`Invalid versions property`);
return true;
}
const isWindows = () => os_1.default.platform() === "win32";
/**
* Exported for testing
*/
function hasSecurityFixes(current, latest, channel, summary) {
const range = `>${current} <=${latest}`;
const versions = Object.entries(summary.versions)
.filter(([ver, sum]) => sum.channel === channel &&
sum.securityFixes &&
semver_1.default.satisfies(ver, range));
return versions.length !== 0;
}
exports.hasSecurityFixes = hasSecurityFixes;
function remindNow(lastReminder, interval) {
if (interval <= 0)
return false;
return (Date.now() - (lastReminder || 0)) >= interval;
}
//# sourceMappingURL=upgrade_checker.js.map