@oclif/plugin-update
Advanced tools
Comparing version 1.0.3 to 1.0.4
@@ -1,1 +0,1 @@ | ||
{"version":"1.0.3","commands":{"update":{"id":"update","description":"update the <%= config.bin %> CLI","pluginName":"@oclif/plugin-update","pluginType":"core","aliases":[],"flags":{"autoupdate":{"name":"autoupdate","type":"boolean","hidden":true}},"args":[{"name":"channel"}]}}} | ||
{"version":"1.0.4","commands":{"update":{"id":"update","description":"update the <%= config.bin %> CLI","pluginName":"@oclif/plugin-update","pluginType":"core","aliases":[],"flags":{"autoupdate":{"name":"autoupdate","type":"boolean","hidden":true}},"args":[{"name":"channel"}]}}} |
@@ -0,1 +1,9 @@ | ||
<a name="1.0.4"></a> | ||
## [1.0.4](https://github.com/oclif/plugin-update/compare/e8dd1e98a7806d1a67f5294034fb5b99d6012222...v1.0.4) (2018-04-07) | ||
### Bug Fixes | ||
* updated deps ([f652abd](https://github.com/oclif/plugin-update/commit/f652abd)) | ||
<a name="1.0.3"></a> | ||
@@ -2,0 +10,0 @@ ## [1.0.3](https://github.com/oclif/plugin-update/compare/bd0256816d53dd4b56618871741d56c7b3b1d7ca...v1.0.3) (2018-02-28) |
import Command from '@oclif/command'; | ||
import { Updater } from '../update'; | ||
import { Updater } from '..'; | ||
export default class UpdateCommand extends Command { | ||
@@ -4,0 +4,0 @@ static description: string; |
@@ -10,3 +10,3 @@ "use strict"; | ||
const path = require("path"); | ||
const update_1 = require("../update"); | ||
const __1 = require(".."); | ||
const util_1 = require("../util"); | ||
@@ -16,3 +16,3 @@ class UpdateCommand extends command_1.default { | ||
super(...arguments); | ||
this.updater = new update_1.Updater(this.config); | ||
this.updater = new __1.Updater(this.config); | ||
} | ||
@@ -19,0 +19,0 @@ async run() { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const cli_ux_1 = require("cli-ux"); | ||
const update_1 = require("../update"); | ||
const __1 = require(".."); | ||
exports.init = async function (opts) { | ||
@@ -9,4 +9,4 @@ cli_ux_1.default.config.errlog = opts.config.errlog; | ||
return; | ||
const updater = new update_1.Updater(opts.config); | ||
const updater = new __1.Updater(opts.config); | ||
await updater.autoupdate(); | ||
}; |
@@ -1,2 +0,45 @@ | ||
declare const _default: {}; | ||
export default _default; | ||
import * as Config from '@oclif/config'; | ||
export interface IVersion { | ||
version: string; | ||
channel: string; | ||
message?: string; | ||
} | ||
export interface IManifest { | ||
version: string; | ||
channel: string; | ||
sha256gz: string; | ||
priority?: number; | ||
} | ||
export declare class Updater { | ||
config: Config.IConfig; | ||
constructor(config: Config.IConfig); | ||
readonly channel: string; | ||
readonly reexecBin: string | undefined; | ||
readonly name: string; | ||
readonly autoupdatefile: string; | ||
readonly autoupdatelogfile: string; | ||
readonly versionFile: string; | ||
readonly lastrunfile: string; | ||
private readonly clientRoot; | ||
private readonly clientBin; | ||
private readonly binPath; | ||
private readonly s3Host; | ||
s3url(channel: string, p: string): string; | ||
fetchManifest(channel: string): Promise<IManifest>; | ||
fetchVersion(download: boolean): Promise<IVersion>; | ||
warnIfUpdateAvailable(): Promise<void>; | ||
autoupdate(force?: boolean): Promise<void>; | ||
update(manifest: IManifest): Promise<void>; | ||
tidy(): Promise<void>; | ||
private extract(stream, dir, sha); | ||
private base(manifest); | ||
private autoupdateNeeded(); | ||
readonly timestampEnvVar: string; | ||
readonly skipAnalyticsEnvVar: string; | ||
readonly autoupdateEnv: { | ||
[k: string]: string | undefined; | ||
}; | ||
private reexecUpdate(); | ||
private _createBin(manifest); | ||
private _catch(fn); | ||
} |
324
lib/index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = {}; | ||
const cli_ux_1 = require("cli-ux"); | ||
const spawn = require("cross-spawn"); | ||
const dateIsAfter = require("date-fns/is_after"); | ||
const dateSubDays = require("date-fns/sub_days"); | ||
const dateSubHours = require("date-fns/sub_hours"); | ||
const fs = require("fs-extra"); | ||
const path = require("path"); | ||
const util_1 = require("./util"); | ||
const debug = require('debug')('cli:updater'); | ||
async function mtime(f) { | ||
const { mtime } = await fs.stat(f); | ||
return mtime; | ||
} | ||
function timestamp(msg) { | ||
return `[${new Date().toISOString()}] ${msg}`; | ||
} | ||
class Updater { | ||
constructor(config) { | ||
this.config = config; | ||
this.config = config; | ||
} | ||
get channel() { | ||
let pjson = this.config.pjson.oclif; | ||
if (pjson.channel) | ||
return pjson.channel; | ||
return 'stable'; | ||
} | ||
get reexecBin() { | ||
return this.config.scopedEnvVar('CLI_BINPATH'); | ||
} | ||
get name() { | ||
return this.config.name === '@oclif/plugin-update' ? 'heroku-cli' : this.config.name; | ||
} | ||
get autoupdatefile() { | ||
return path.join(this.config.cacheDir, 'autoupdate'); | ||
} | ||
get autoupdatelogfile() { | ||
return path.join(this.config.cacheDir, 'autoupdate.log'); | ||
} | ||
get versionFile() { | ||
return path.join(this.config.cacheDir, `${this.channel}.version`); | ||
} | ||
get lastrunfile() { | ||
return path.join(this.config.cacheDir, 'lastrun'); | ||
} | ||
get clientRoot() { | ||
return path.join(this.config.dataDir, 'client'); | ||
} | ||
get clientBin() { | ||
let b = path.join(this.clientRoot, 'bin', this.config.bin); | ||
return this.config.windows ? `${b}.cmd` : b; | ||
} | ||
get binPath() { | ||
return this.reexecBin || this.config.bin; | ||
} | ||
get s3Host() { | ||
const pjson = this.config.pjson.oclif; | ||
return (pjson.s3 && pjson.s3.host) || this.config.scopedEnvVar('S3_HOST'); | ||
} | ||
s3url(channel, p) { | ||
if (!this.s3Host) | ||
throw new Error('S3 host not defined'); | ||
return `https://${this.s3Host}/${this.name}/channels/${channel}/${p}`; | ||
} | ||
async fetchManifest(channel) { | ||
const http = require('http-call').HTTP; | ||
try { | ||
let { body } = await http.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`)); | ||
return body; | ||
} | ||
catch (err) { | ||
if (err.statusCode === 403) | ||
throw new Error(`HTTP 403: Invalid channel ${channel}`); | ||
throw err; | ||
} | ||
} | ||
async fetchVersion(download) { | ||
const http = require('http-call').HTTP; | ||
let v; | ||
try { | ||
if (!download) | ||
v = await fs.readJSON(this.versionFile); | ||
} | ||
catch (err) { | ||
if (err.code !== 'ENOENT') | ||
throw err; | ||
} | ||
if (!v) { | ||
debug('fetching latest %s version', this.channel); | ||
let { body } = await http.get(this.s3url(this.channel, 'version')); | ||
v = body; | ||
await this._catch(() => fs.outputJSON(this.versionFile, v)); | ||
} | ||
return v; | ||
} | ||
async warnIfUpdateAvailable() { | ||
await this._catch(async () => { | ||
if (!this.s3Host) | ||
return; | ||
let v = await this.fetchVersion(false); | ||
if (util_1.minorVersionGreater(this.config.version, v.version)) { | ||
cli_ux_1.cli.warn(`${this.name}: update available from ${this.config.version} to ${v.version}`); | ||
} | ||
if (v.message) { | ||
cli_ux_1.cli.warn(`${this.name}: ${v.message}`); | ||
} | ||
}); | ||
} | ||
async autoupdate(force = false) { | ||
try { | ||
await util_1.touch(this.lastrunfile); | ||
const clientDir = path.join(this.clientRoot, this.config.version); | ||
if (await fs.pathExists(clientDir)) { | ||
await util_1.touch(clientDir); | ||
} | ||
await this.warnIfUpdateAvailable(); | ||
if (!force && !await this.autoupdateNeeded()) | ||
return; | ||
debug('autoupdate running'); | ||
await fs.outputFile(this.autoupdatefile, ''); | ||
debug(`spawning autoupdate on ${this.binPath}`); | ||
let fd = await fs.open(this.autoupdatelogfile, 'a'); | ||
// @ts-ignore | ||
fs.write(fd, timestamp(`starting \`${this.binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`)); | ||
spawn(this.binPath, ['update', '--autoupdate'], { | ||
detached: !this.config.windows, | ||
stdio: ['ignore', fd, fd], | ||
env: this.autoupdateEnv, | ||
}) | ||
.on('error', (e) => process.emitWarning(e)) | ||
.unref(); | ||
} | ||
catch (e) { | ||
process.emitWarning(e); | ||
} | ||
} | ||
async update(manifest) { | ||
const _ = require('lodash'); | ||
const http = require('http-call').HTTP; | ||
const filesize = require('filesize'); | ||
let base = this.base(manifest); | ||
const output = path.join(this.clientRoot, manifest.version); | ||
const tmp = path.join(this.clientRoot, base); | ||
if (!this.s3Host) | ||
throw new Error('S3 host not defined'); | ||
let url = `https://${this.s3Host}/${this.name}/channels/${manifest.channel}/${base}.tar.gz`; | ||
let { response: stream } = await http.stream(url); | ||
await fs.emptyDir(tmp); | ||
let extraction = this.extract(stream, this.clientRoot, manifest.sha256gz); | ||
// TODO: use cli.action.type | ||
if (cli_ux_1.cli.action.frames) { | ||
// if spinner action | ||
let total = stream.headers['content-length']; | ||
let current = 0; | ||
const updateStatus = _.throttle((newStatus) => { | ||
cli_ux_1.cli.action.status = newStatus; | ||
}, 500, { leading: true, trailing: false }); | ||
stream.on('data', data => { | ||
current += data.length; | ||
updateStatus(`${filesize(current)}/${filesize(total)}`); | ||
}); | ||
} | ||
await extraction; | ||
if (await fs.pathExists(output)) { | ||
const old = `${output}.old`; | ||
await fs.remove(old); | ||
await fs.rename(output, old); | ||
} | ||
await fs.rename(tmp, output); | ||
await util_1.touch(output); | ||
await this._createBin(manifest); | ||
await this.reexecUpdate(); | ||
} | ||
async tidy() { | ||
try { | ||
if (!this.reexecBin) | ||
return; | ||
if (!this.reexecBin.includes(this.config.version)) | ||
return; | ||
let root = this.clientRoot; | ||
if (!await fs.pathExists(root)) | ||
return; | ||
let files = await util_1.ls(root); | ||
let promises = files.map(async (f) => { | ||
if (['bin', this.config.version].includes(path.basename(f.path))) | ||
return; | ||
if (dateIsAfter(f.stat.mtime, dateSubDays(new Date(), 7))) { | ||
await fs.remove(f.path); | ||
} | ||
}); | ||
for (let p of promises) | ||
await p; | ||
} | ||
catch (err) { | ||
cli_ux_1.cli.warn(err); | ||
} | ||
} | ||
extract(stream, dir, sha) { | ||
const zlib = require('zlib'); | ||
const tar = require('tar-fs'); | ||
const crypto = require('crypto'); | ||
return new Promise((resolve, reject) => { | ||
let shaValidated = false; | ||
let extracted = false; | ||
let check = () => { | ||
if (shaValidated && extracted) { | ||
resolve(); | ||
} | ||
}; | ||
let fail = (err) => { | ||
fs.remove(dir) | ||
.then(() => reject(err)) | ||
.catch(reject); | ||
}; | ||
let hasher = crypto.createHash('sha256'); | ||
stream.on('error', fail); | ||
stream.on('data', d => hasher.update(d)); | ||
stream.on('end', () => { | ||
let shasum = hasher.digest('hex'); | ||
if (sha === shasum) { | ||
shaValidated = true; | ||
check(); | ||
} | ||
else { | ||
reject(new Error(`SHA mismatch: expected ${shasum} to be ${sha}`)); | ||
} | ||
}); | ||
let ignore = (_, header) => { | ||
switch (header.type) { | ||
case 'directory': | ||
case 'file': | ||
if (process.env.CLI_ENGINE_DEBUG_UPDATE_FILES) | ||
debug(header.name); | ||
return false; | ||
case 'symlink': | ||
return true; | ||
default: | ||
throw new Error(header.type); | ||
} | ||
}; | ||
let extract = tar.extract(dir, { ignore }); | ||
extract.on('error', fail); | ||
extract.on('finish', () => { | ||
extracted = true; | ||
check(); | ||
}); | ||
let gunzip = zlib.createGunzip(); | ||
gunzip.on('error', fail); | ||
stream.pipe(gunzip).pipe(extract); | ||
}); | ||
} | ||
base(manifest) { | ||
return `${this.name}-v${manifest.version}-${this.config.platform}-${this.config.arch}`; | ||
} | ||
async autoupdateNeeded() { | ||
try { | ||
const m = await mtime(this.autoupdatefile); | ||
return dateIsAfter(m, dateSubHours(new Date(), 5)); | ||
} | ||
catch (err) { | ||
if (err.code !== 'ENOENT') | ||
cli_ux_1.cli.error(err.stack); | ||
if (global.testing) | ||
return false; | ||
debug('autoupdate ENOENT'); | ||
return true; | ||
} | ||
} | ||
get timestampEnvVar() { | ||
// TODO: use function from @cli-engine/config | ||
let bin = this.config.bin.replace('-', '_').toUpperCase(); | ||
return `${bin}_TIMESTAMPS`; | ||
} | ||
get skipAnalyticsEnvVar() { | ||
let bin = this.config.bin.replace('-', '_').toUpperCase(); | ||
return `${bin}_SKIP_ANALYTICS`; | ||
} | ||
get autoupdateEnv() { | ||
return Object.assign({}, process.env, { [this.timestampEnvVar]: '1', [this.skipAnalyticsEnvVar]: '1' }); | ||
} | ||
async reexecUpdate() { | ||
cli_ux_1.cli.action.stop(); | ||
return new Promise((_, reject) => { | ||
debug('restarting CLI after update', this.clientBin); | ||
spawn(this.clientBin, ['update'], { | ||
stdio: 'inherit', | ||
env: Object.assign({}, process.env, { CLI_ENGINE_HIDE_UPDATED_MESSAGE: '1' }), | ||
}) | ||
.on('error', reject) | ||
.on('close', (status) => { | ||
try { | ||
cli_ux_1.cli.exit(status); | ||
} | ||
catch (err) { | ||
reject(err); | ||
} | ||
}); | ||
}); | ||
} | ||
async _createBin(manifest) { | ||
let dst = this.clientBin; | ||
if (this.config.windows) { | ||
let body = `@echo off | ||
"%~dp0\\..\\${manifest.version}\\bin\\${this.config.bin}.cmd" %* | ||
`; | ||
await fs.outputFile(dst, body); | ||
return; | ||
} | ||
let src = path.join('..', manifest.version, 'bin', this.config.bin); | ||
await fs.mkdirp(path.dirname(dst)); | ||
await fs.remove(dst); | ||
await fs.symlink(src, dst); | ||
} | ||
async _catch(fn) { | ||
try { | ||
return await Promise.resolve(fn()); | ||
} | ||
catch (err) { | ||
debug(err); | ||
} | ||
} | ||
} | ||
exports.Updater = Updater; |
{ | ||
"name": "@oclif/plugin-update", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"author": "Jeff Dickey @jdxcode", | ||
@@ -8,13 +8,13 @@ "bugs": "https://github.com/oclif/plugin-update/issues", | ||
"@heroku-cli/color": "^1.1.3", | ||
"@oclif/command": "^1.4.1", | ||
"@oclif/config": "^1.3.57", | ||
"@oclif/errors": "^1.0.2", | ||
"@oclif/command": "^1.4.7", | ||
"@oclif/config": "^1.3.64", | ||
"@oclif/errors": "^1.0.3", | ||
"@types/semver": "^5.5.0", | ||
"cli-ux": "^3.3.23", | ||
"cross-spawn": "^6.0.4", | ||
"cli-ux": "^3.3.27", | ||
"cross-spawn": "^6.0.5", | ||
"date-fns": "^1.29.0", | ||
"debug": "^3.1.0", | ||
"filesize": "^3.6.0", | ||
"filesize": "^3.6.1", | ||
"fs-extra": "^5.0.0", | ||
"http-call": "^5.0.2", | ||
"http-call": "^5.1.0", | ||
"lodash": "^4.17.5", | ||
@@ -26,19 +26,19 @@ "log-chopper": "^1.0.2", | ||
"devDependencies": { | ||
"@oclif/dev-cli": "^1.2.18", | ||
"@oclif/plugin-help": "^1.1.5", | ||
"@oclif/test": "^1.0.1", | ||
"@oclif/tslint": "^1.0.2", | ||
"@oclif/dev-cli": "^1.4.4", | ||
"@oclif/plugin-help": "^1.2.3", | ||
"@oclif/test": "^1.0.4", | ||
"@oclif/tslint": "^1.1.0", | ||
"@types/chai": "^4.1.2", | ||
"@types/cross-spawn": "^6.0.0", | ||
"@types/fs-extra": "^5.0.1", | ||
"@types/lodash": "^4.14.104", | ||
"@types/mocha": "^2.2.48", | ||
"@types/node": "^9.4.6", | ||
"@types/lodash": "^4.14.106", | ||
"@types/mocha": "^5.0.0", | ||
"@types/node": "^9.6.2", | ||
"@types/supports-color": "^3.1.0", | ||
"chai": "^4.1.2", | ||
"globby": "^8.0.1", | ||
"mocha": "^5.0.1", | ||
"ts-node": "^5.0.0", | ||
"mocha": "^5.0.5", | ||
"ts-node": "^5.0.1", | ||
"tslint": "^5.9.1", | ||
"typescript": "^2.7.2" | ||
"typescript": "^2.8.1" | ||
}, | ||
@@ -45,0 +45,0 @@ "engines": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25109
13
558
4
Updated@oclif/command@^1.4.7
Updated@oclif/config@^1.3.64
Updated@oclif/errors@^1.0.3
Updatedcli-ux@^3.3.27
Updatedcross-spawn@^6.0.5
Updatedfilesize@^3.6.1
Updatedhttp-call@^5.1.0