mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-22 06:57:48 +00:00
Merge pull request #2309 from balena-io/warn-deprecation-policy
Add deprecation policy checker and --unsupported global flag
This commit is contained in:
commit
cb9b6be24b
18
README.md
18
README.md
@ -156,14 +156,18 @@ of major, minor and patch version releases.
|
|||||||
|
|
||||||
The latest release of a major version of the balena CLI will remain compatible with
|
The latest release of a major version of the balena CLI will remain compatible with
|
||||||
the balenaCloud backend services for at least one year from the date when the
|
the balenaCloud backend services for at least one year from the date when the
|
||||||
following major version is released. For example, balena CLI v10.17.5, as the
|
following major version is released. For example, balena CLI v11.36.0, as the
|
||||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||||
year from the date when v11.0.0 is released.
|
year from the date when v12.0.0 was released.
|
||||||
|
|
||||||
At the end of this period, the older major version is considered deprecated and
|
Half way through to that period (6 months after the release of the next major
|
||||||
some of the functionality that depends on balenaCloud services may stop working
|
version), older major versions of the balena CLI will start printing a deprecation
|
||||||
at any time.
|
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
device file). At the end of that period, older major versions will exit with an
|
||||||
|
error message unless the `--unsupported` flag is used. This behavior was
|
||||||
|
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||||
|
To take advantage of the latest backend features and ensure compatibility, users
|
||||||
|
are encouraged to regularly update the balena CLI to the latest version.
|
||||||
|
|
||||||
## Contributing (including editing documentation files)
|
## Contributing (including editing documentation files)
|
||||||
|
|
||||||
|
@ -22,25 +22,26 @@ import * as archiver from 'archiver';
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import * as filehound from 'filehound';
|
import * as filehound from 'filehound';
|
||||||
|
import { Stats } from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
|
import * as klaw from 'klaw';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import * as util from 'util';
|
import { promisify } from 'util';
|
||||||
import * as klaw from 'klaw';
|
|
||||||
import { Stats } from 'fs';
|
|
||||||
|
|
||||||
import { stripIndent } from '../lib/utils/lazy';
|
import { stripIndent } from '../lib/utils/lazy';
|
||||||
import {
|
import {
|
||||||
diffLines,
|
diffLines,
|
||||||
getSubprocessStdout,
|
|
||||||
loadPackageJson,
|
loadPackageJson,
|
||||||
ROOT,
|
ROOT,
|
||||||
StdOutTap,
|
StdOutTap,
|
||||||
whichSpawn,
|
whichSpawn,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export const packageJSON = loadPackageJson();
|
export const packageJSON = loadPackageJson();
|
||||||
export const version = 'v' + packageJSON.version;
|
export const version = 'v' + packageJSON.version;
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
@ -246,7 +247,17 @@ async function testPkg() {
|
|||||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
||||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
||||||
// reported Node.js major version matches semver.major(process.version)
|
// reported Node.js major version matches semver.major(process.version)
|
||||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||||
|
'version',
|
||||||
|
'-j',
|
||||||
|
]);
|
||||||
|
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||||
|
const filtered = filterCliOutputForTests({
|
||||||
|
err: stderr.split(/\r?\n/),
|
||||||
|
out: stdout.split(/\r?\n/),
|
||||||
|
});
|
||||||
|
stdout = filtered.out.join('\n');
|
||||||
|
stderr = filtered.err.join('\n');
|
||||||
let pkgNodeVersion = '';
|
let pkgNodeVersion = '';
|
||||||
let pkgNodeMajorVersion = 0;
|
let pkgNodeMajorVersion = 0;
|
||||||
try {
|
try {
|
||||||
@ -263,6 +274,10 @@ async function testPkg() {
|
|||||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (filtered.err.length > 0) {
|
||||||
|
const err = filtered.err.join('\n');
|
||||||
|
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
||||||
|
}
|
||||||
console.log('Success! (standalone package test successful)');
|
console.log('Success! (standalone package test successful)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,8 +426,6 @@ async function renameInstallerFiles() {
|
|||||||
async function signWindowsInstaller() {
|
async function signWindowsInstaller() {
|
||||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||||
const exeName = renamedOclifInstallers[process.platform];
|
const exeName = renamedOclifInstallers[process.platform];
|
||||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
|
||||||
|
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
await execFileAsync(MSYS2_BASH, [
|
await execFileAsync(MSYS2_BASH, [
|
||||||
'sign-exe.sh',
|
'sign-exe.sh',
|
||||||
|
@ -21,22 +21,6 @@ import * as path from 'path';
|
|||||||
|
|
||||||
export const ROOT = path.join(__dirname, '..');
|
export const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
const nodeEngineWarn = `\
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
|
|
||||||
This may cause unexpected behavior. To upgrade Node, visit:
|
|
||||||
https://nodejs.org/en/download/
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
`;
|
|
||||||
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
|
|
||||||
|
|
||||||
export function matchesNodeEngineVersionWarn(line: string) {
|
|
||||||
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
|
|
||||||
return (
|
|
||||||
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tap and buffer this process' stdout and stderr */
|
/** Tap and buffer this process' stdout and stderr */
|
||||||
export class StdOutTap {
|
export class StdOutTap {
|
||||||
public stdoutBuf: string[] = [];
|
public stdoutBuf: string[] = [];
|
||||||
@ -104,60 +88,6 @@ export function loadPackageJson() {
|
|||||||
return require(path.join(ROOT, 'package.json'));
|
return require(path.join(ROOT, 'package.json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the executable at execPath as a child process, and resolve a promise
|
|
||||||
* to the executable's stdout output as a string. Reject the promise if
|
|
||||||
* anything is printed to stderr, or if the child process exits with a
|
|
||||||
* non-zero exit code.
|
|
||||||
* @param execPath Executable path
|
|
||||||
* @param args Command-line argument for the executable
|
|
||||||
*/
|
|
||||||
export async function getSubprocessStdout(
|
|
||||||
execPath: string,
|
|
||||||
args: string[],
|
|
||||||
): Promise<string> {
|
|
||||||
const child = spawn(execPath, args);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let stdout = '';
|
|
||||||
child.stdout.on('error', reject);
|
|
||||||
child.stderr.on('error', reject);
|
|
||||||
child.stdout.on('data', (data: Buffer) => {
|
|
||||||
try {
|
|
||||||
stdout = data.toString();
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
child.stderr.on('data', (data: Buffer) => {
|
|
||||||
try {
|
|
||||||
const stderr = data.toString();
|
|
||||||
|
|
||||||
// ignore any debug lines, but ensure that we parse
|
|
||||||
// every line provided to the stderr stream
|
|
||||||
const lines = _.filter(
|
|
||||||
stderr.trim().split(/\r?\n/),
|
|
||||||
(line) =>
|
|
||||||
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
|
|
||||||
);
|
|
||||||
if (lines.length > 0) {
|
|
||||||
reject(
|
|
||||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
child.on('exit', (code: number) => {
|
|
||||||
if (code) {
|
|
||||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
|
||||||
} else {
|
|
||||||
resolve(stdout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error handling wrapper around the npm `which` package:
|
* Error handling wrapper around the npm `which` package:
|
||||||
* "Like the unix which utility. Finds the first instance of a specified
|
* "Like the unix which utility. Finds the first instance of a specified
|
||||||
|
@ -144,14 +144,18 @@ of major, minor and patch version releases.
|
|||||||
|
|
||||||
The latest release of a major version of the balena CLI will remain compatible with
|
The latest release of a major version of the balena CLI will remain compatible with
|
||||||
the balenaCloud backend services for at least one year from the date when the
|
the balenaCloud backend services for at least one year from the date when the
|
||||||
following major version is released. For example, balena CLI v10.17.5, as the
|
following major version is released. For example, balena CLI v11.36.0, as the
|
||||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||||
year from the date when v11.0.0 is released.
|
year from the date when v12.0.0 was released.
|
||||||
|
|
||||||
At the end of this period, the older major version is considered deprecated and
|
Half way through to that period (6 months after the release of the next major
|
||||||
some of the functionality that depends on balenaCloud services may stop working
|
version), older major versions of the balena CLI will start printing a deprecation
|
||||||
at any time.
|
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
device file). At the end of that period, older major versions will exit with an
|
||||||
|
error message unless the `--unsupported` flag is used. This behavior was
|
||||||
|
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||||
|
To take advantage of the latest backend features and ensure compatibility, users
|
||||||
|
are encouraged to regularly update the balena CLI to the latest version.
|
||||||
|
|
||||||
|
|
||||||
# CLI Command Reference
|
# CLI Command Reference
|
||||||
|
45
lib/app.ts
45
lib/app.ts
@ -16,8 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as packageJSON from '../package.json';
|
import * as packageJSON from '../package.json';
|
||||||
|
import {
|
||||||
|
AppOptions,
|
||||||
|
checkDeletedCommand,
|
||||||
|
preparseArgs,
|
||||||
|
unsupportedFlag,
|
||||||
|
} from './preparser';
|
||||||
import { CliSettings } from './utils/bootstrap';
|
import { CliSettings } from './utils/bootstrap';
|
||||||
import { onceAsync, stripIndent } from './utils/lazy';
|
import { onceAsync } from './utils/lazy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry.io setup
|
* Sentry.io setup
|
||||||
@ -43,13 +49,8 @@ export const setupSentry = onceAsync(async () => {
|
|||||||
async function checkNodeVersion() {
|
async function checkNodeVersion() {
|
||||||
const validNodeVersions = packageJSON.engines.node;
|
const validNodeVersions = packageJSON.engines.node;
|
||||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||||
console.warn(stripIndent`
|
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||||
------------------------------------------------------------------------------
|
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
|
||||||
This may cause unexpected behavior. To upgrade Node, visit:
|
|
||||||
https://nodejs.org/en/download/
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,10 +94,20 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Execute the oclif parser and the CLI command. */
|
/** Execute the oclif parser and the CLI command. */
|
||||||
async function oclifRun(
|
async function oclifRun(command: string[], options: AppOptions) {
|
||||||
command: string[],
|
let deprecationPromise: Promise<void>;
|
||||||
options: import('./preparser').AppOptions,
|
// check and enforce the CLI's deprecation policy
|
||||||
) {
|
if (unsupportedFlag) {
|
||||||
|
deprecationPromise = Promise.resolve();
|
||||||
|
} else {
|
||||||
|
const { DeprecationChecker } = await import('./deprecation');
|
||||||
|
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||||
|
// warnAndAbortIfDeprecated uses previously cached data only
|
||||||
|
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||||
|
// checkForNewReleasesIfNeeded may query the npm registry
|
||||||
|
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
const runPromise = (async function (shouldFlush: boolean) {
|
const runPromise = (async function (shouldFlush: boolean) {
|
||||||
const { CustomMain } = await import('./utils/oclif-utils');
|
const { CustomMain } = await import('./utils/oclif-utils');
|
||||||
let isEEXIT = false;
|
let isEEXIT = false;
|
||||||
@ -130,14 +141,12 @@ async function oclifRun(
|
|||||||
})(!options.noFlush);
|
})(!options.noFlush);
|
||||||
|
|
||||||
const { trackPromise } = await import('./hooks/prerun/track');
|
const { trackPromise } = await import('./hooks/prerun/track');
|
||||||
await Promise.all([trackPromise, runPromise]);
|
|
||||||
|
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||||
export async function run(
|
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
||||||
cliArgs = process.argv,
|
|
||||||
options: import('./preparser').AppOptions = {},
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||||
normalizeEnvVars();
|
normalizeEnvVars();
|
||||||
@ -150,8 +159,6 @@ export async function run(
|
|||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
|
||||||
|
|
||||||
// Look for commands that have been removed and if so, exit with a notice
|
// Look for commands that have been removed and if so, exit with a notice
|
||||||
checkDeletedCommand(cliArgs.slice(2));
|
checkDeletedCommand(cliArgs.slice(2));
|
||||||
|
|
||||||
|
241
lib/deprecation.ts
Normal file
241
lib/deprecation.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2021 Balena Ltd.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ReleaseTimestampsByVersion {
|
||||||
|
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
|
||||||
|
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warn about and enforce the CLI deprecation policy stated in the README
|
||||||
|
* file. In particular:
|
||||||
|
* The latest release of a major version will remain compatible with
|
||||||
|
* the backend services for at least one year from the date when the
|
||||||
|
* following major version is released. [...]
|
||||||
|
* Half way through to that period (6 months), old major versions of the
|
||||||
|
* balena CLI will start printing a deprecation warning message.
|
||||||
|
* At the end of that period, older major versions will abort with an error
|
||||||
|
* message unless the `--unsupported` flag is used.
|
||||||
|
*
|
||||||
|
* - Check for new balena-cli releases by querying the npm registry.
|
||||||
|
* - Cache results for a number of days to improve performance.
|
||||||
|
*
|
||||||
|
* For this feature's specification and planning, see (restricted access):
|
||||||
|
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
|
||||||
|
*/
|
||||||
|
export class DeprecationChecker {
|
||||||
|
protected static disabled = false; // for the benefit of testing
|
||||||
|
readonly majorVersionFetchIntervalDays = 7;
|
||||||
|
readonly expiryDays = 365;
|
||||||
|
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
|
||||||
|
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
|
||||||
|
readonly debugPrefix = 'Deprecation check';
|
||||||
|
readonly cacheFile = 'cachedReleaseTimestamps';
|
||||||
|
readonly now = new Date().getTime();
|
||||||
|
private initialized = false;
|
||||||
|
storage: ReturnType<typeof import('balena-settings-storage')>;
|
||||||
|
cachedTimestamps: ReleaseTimestampsByVersion;
|
||||||
|
nextMajorVersion: string; // semver without the 'v' prefix
|
||||||
|
|
||||||
|
constructor(protected currentVersion: string) {
|
||||||
|
const semver = require('semver') as typeof import('semver');
|
||||||
|
const major = semver.major(this.currentVersion, { loose: true });
|
||||||
|
this.nextMajorVersion = `${major + 1}.0.0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init() {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
const settings = await import('balena-settings-client');
|
||||||
|
const getStorage = await import('balena-settings-storage');
|
||||||
|
const dataDirectory = settings.get<string>('dataDirectory');
|
||||||
|
this.storage = getStorage({ dataDirectory });
|
||||||
|
let stored: ReleaseTimestampsByVersion | undefined;
|
||||||
|
try {
|
||||||
|
stored = (await this.storage.get(
|
||||||
|
this.cacheFile,
|
||||||
|
)) as ReleaseTimestampsByVersion;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
this.cachedTimestamps = {
|
||||||
|
...stored,
|
||||||
|
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
|
||||||
|
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get NPM registry URL to retrieve the package.json file for a given version.
|
||||||
|
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
|
||||||
|
*/
|
||||||
|
protected getNpmUrl(version: string) {
|
||||||
|
return `http://registry.npmjs.org/balena-cli/${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the npm registry (HTTP request) for a given balena-cli version.
|
||||||
|
*
|
||||||
|
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
|
||||||
|
* @returns `undefined` if the request status code is 404 (version not
|
||||||
|
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
|
||||||
|
* '2021-06-27T16:46:10.000Z'.
|
||||||
|
*/
|
||||||
|
protected async fetchPublishedTimestampForVersion(
|
||||||
|
version: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const { default: got } = await import('got');
|
||||||
|
const url = this.getNpmUrl(version);
|
||||||
|
let response: import('got').Response<Dictionary<any>> | undefined;
|
||||||
|
try {
|
||||||
|
response = await got(url, { responseType: 'json', retry: 0 });
|
||||||
|
} catch (e) {
|
||||||
|
// 404 is expected if `version` hasn't been published yet
|
||||||
|
if (e.response?.statusCode !== 404) {
|
||||||
|
throw new Error(`Failed to query "${url}":\n${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// response.body looks like a package.json file, plus possibly a
|
||||||
|
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
|
||||||
|
const publishedAt: string | undefined =
|
||||||
|
response?.body?.versionist?.publishedAt;
|
||||||
|
if (!publishedAt && process.env.DEBUG) {
|
||||||
|
console.error(`\
|
||||||
|
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
|
||||||
|
or release date not available`);
|
||||||
|
}
|
||||||
|
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we already know (cached value) when the next major version
|
||||||
|
* was released. If we don't know, check how long ago the npm registry
|
||||||
|
* was last fetched, and fetch again if it has been longer than
|
||||||
|
* `majorVersionFetchIntervalDays`.
|
||||||
|
*/
|
||||||
|
public async checkForNewReleasesIfNeeded() {
|
||||||
|
if (DeprecationChecker.disabled) {
|
||||||
|
return; // for the benefit of code testing
|
||||||
|
}
|
||||||
|
await this.init();
|
||||||
|
if (this.cachedTimestamps[this.nextMajorVersion]) {
|
||||||
|
// A cached value exists: no need to check the npm registry
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
|
||||||
|
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
|
||||||
|
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
// toFixed(5) results in a precision of ~1 second
|
||||||
|
const days = daysSinceLastFetch.toFixed(5);
|
||||||
|
console.error(`\
|
||||||
|
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
|
||||||
|
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error(`\
|
||||||
|
[debug] ${
|
||||||
|
this.debugPrefix
|
||||||
|
}: Cache miss for the balena CLI next major version release date.
|
||||||
|
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const publishedAt = await this.fetchPublishedTimestampForVersion(
|
||||||
|
this.nextMajorVersion,
|
||||||
|
);
|
||||||
|
if (publishedAt) {
|
||||||
|
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error(`[debug] ${this.debugPrefix}: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Refresh `lastFetched` regardless of whether or not the request to the npm
|
||||||
|
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
|
||||||
|
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
|
||||||
|
await this.storage.set(this.cacheFile, this.cachedTimestamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use previously cached data (local cache only, fast execution) to check
|
||||||
|
* whether this version of the CLI is deprecated as per deprecation policy,
|
||||||
|
* in which case warn about it and conditionally throw an error.
|
||||||
|
*/
|
||||||
|
public async warnAndAbortIfDeprecated() {
|
||||||
|
if (DeprecationChecker.disabled) {
|
||||||
|
return; // for the benefit of code testing
|
||||||
|
}
|
||||||
|
await this.init();
|
||||||
|
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
|
||||||
|
if (!nextMajorDateStr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextMajorDate = new Date(nextMajorDateStr).getTime();
|
||||||
|
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
|
||||||
|
if (daysElapsed > this.expiryDays) {
|
||||||
|
const { ExpectedError } = await import('./errors');
|
||||||
|
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
|
||||||
|
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
|
||||||
|
console.error(this.getDeprecationMsg(daysElapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Separate function for the benefit of code testing */
|
||||||
|
getDeprecationMsg(daysElapsed: number) {
|
||||||
|
const { warnify } =
|
||||||
|
require('./utils/messages') as typeof import('./utils/messages');
|
||||||
|
return warnify(`\
|
||||||
|
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
|
||||||
|
This version of the balena CLI (${this.currentVersion}) will exit with an error
|
||||||
|
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
|
||||||
|
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
|
||||||
|
|
||||||
|
The --unsupported flag may be used to bypass this deprecation check and
|
||||||
|
allow the CLI to keep working beyond the deprecation period. However,
|
||||||
|
note that the balenaCloud or openBalena backends may be updated in a way
|
||||||
|
that is no longer compatible with this version.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Separate function the benefit of code testing */
|
||||||
|
getExpiryMsg(daysElapsed: number) {
|
||||||
|
return `
|
||||||
|
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
|
||||||
|
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
|
||||||
|
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
|
||||||
|
|
||||||
|
The --unsupported flag may be used to bypass this deprecation check and
|
||||||
|
continue using this version of the CLI. However, note that the balenaCloud
|
||||||
|
or openBalena backends may be updated in a way that is no longer compatible
|
||||||
|
with this CLI version.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disable deprecation checks (for the benefit of code testing). */
|
||||||
|
public static disable() {
|
||||||
|
DeprecationChecker.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-enable deprecation checks (for the benefit of code testing). */
|
||||||
|
public static enable() {
|
||||||
|
DeprecationChecker.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
@ -60,11 +60,11 @@ export async function trackCommand(commandSignature: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const settings = await import('balena-settings-client');
|
const settings = await import('balena-settings-client');
|
||||||
const balenaUrl = settings.get('balenaUrl') as string;
|
const balenaUrl = settings.get<string>('balenaUrl');
|
||||||
|
|
||||||
const username = await (async () => {
|
const username = await (async () => {
|
||||||
const getStorage = await import('balena-settings-storage');
|
const getStorage = await import('balena-settings-storage');
|
||||||
const dataDirectory = settings.get('dataDirectory') as string;
|
const dataDirectory = settings.get<string>('dataDirectory');
|
||||||
const storage = getStorage({ dataDirectory });
|
const storage = getStorage({ dataDirectory });
|
||||||
let token;
|
let token;
|
||||||
try {
|
try {
|
||||||
|
107
lib/help.ts
107
lib/help.ts
@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
|
|||||||
const subject = getHelpSubject(argv);
|
const subject = getHelpSubject(argv);
|
||||||
if (!subject) {
|
if (!subject) {
|
||||||
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
||||||
this.showCustomRootHelp(verbose);
|
console.log(this.getCustomRootHelp(verbose));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,19 +80,26 @@ export default class BalenaHelp extends Help {
|
|||||||
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
showCustomRootHelp(showAllCommands: boolean): void {
|
getCustomRootHelp(showAllCommands: boolean): string {
|
||||||
const chalk = getChalk();
|
const { bold, cyan } = getChalk();
|
||||||
const bold = chalk.bold;
|
|
||||||
const cmd = chalk.cyan.bold;
|
|
||||||
|
|
||||||
let commands = this.config.commands;
|
let commands = this.config.commands;
|
||||||
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
||||||
|
|
||||||
// Get Primary Commands, sorted as in manual list
|
// Get Primary Commands, sorted as in manual list
|
||||||
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
|
const primaryCommands = this.manuallySortedPrimaryCommands
|
||||||
|
.map((pc) => {
|
||||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||||
});
|
})
|
||||||
|
.filter((c): c is typeof commands[0] => !!c);
|
||||||
|
|
||||||
|
let usageLength = 0;
|
||||||
|
for (const cmd of primaryCommands) {
|
||||||
|
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let additionalCmdSection: string[];
|
||||||
|
if (showAllCommands) {
|
||||||
// Get the rest as Additional Commands
|
// Get the rest as Additional Commands
|
||||||
const additionalCommands = commands.filter(
|
const additionalCommands = commands.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
@ -101,46 +108,78 @@ export default class BalenaHelp extends Help {
|
|||||||
|
|
||||||
// Find longest usage, and pad usage of first command in each category
|
// Find longest usage, and pad usage of first command in each category
|
||||||
// This is to ensure that both categories align visually
|
// This is to ensure that both categories align visually
|
||||||
const usageLength = commands
|
for (const cmd of additionalCommands) {
|
||||||
.map((c) => c.usage?.length || 0)
|
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||||
.reduce((longest, l) => {
|
}
|
||||||
return l > longest ? l : longest;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof primaryCommands[0]?.usage === 'string' &&
|
typeof primaryCommands[0].usage === 'string' &&
|
||||||
typeof additionalCommands[0]?.usage === 'string'
|
typeof additionalCommands[0].usage === 'string'
|
||||||
) {
|
) {
|
||||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||||
additionalCommands[0].usage =
|
additionalCommands[0].usage =
|
||||||
additionalCommands[0].usage.padEnd(usageLength);
|
additionalCommands[0].usage.padEnd(usageLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output help
|
additionalCmdSection = [
|
||||||
console.log(bold('USAGE'));
|
bold('\nADDITIONAL COMMANDS'),
|
||||||
console.log('$ balena [COMMAND] [OPTIONS]');
|
this.formatCommands(additionalCommands),
|
||||||
|
];
|
||||||
console.log(bold('\nPRIMARY COMMANDS'));
|
|
||||||
console.log(this.formatCommands(primaryCommands));
|
|
||||||
|
|
||||||
if (showAllCommands) {
|
|
||||||
console.log(bold('\nADDITIONAL COMMANDS'));
|
|
||||||
console.log(this.formatCommands(additionalCommands));
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
const cmd = cyan.bold('balena help --verbose');
|
||||||
`\n${bold('...MORE')} run ${cmd(
|
additionalCmdSection = [
|
||||||
'balena help --verbose',
|
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
|
||||||
)} to list additional commands.`,
|
];
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(bold('\nGLOBAL OPTIONS'));
|
const globalOps = [
|
||||||
console.log(' --help, -h');
|
['--help, -h', 'display command help'],
|
||||||
console.log(' --debug\n');
|
['--debug', 'enable debug output'],
|
||||||
|
[
|
||||||
|
'--unsupported',
|
||||||
|
`\
|
||||||
|
prevent exit with an error as per Deprecation Policy
|
||||||
|
See: https://git.io/JRHUW#deprecation-policy`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
|
||||||
|
|
||||||
const { reachingOut } =
|
const { deprecationPolicyNote, reachingOut } =
|
||||||
require('./utils/messages') as typeof import('./utils/messages');
|
require('./utils/messages') as typeof import('./utils/messages');
|
||||||
console.log(reachingOut);
|
|
||||||
|
return [
|
||||||
|
bold('USAGE'),
|
||||||
|
'$ balena [COMMAND] [OPTIONS]',
|
||||||
|
bold('\nPRIMARY COMMANDS'),
|
||||||
|
this.formatCommands(primaryCommands),
|
||||||
|
...additionalCmdSection,
|
||||||
|
bold('\nGLOBAL OPTIONS'),
|
||||||
|
this.formatGlobalOpts(globalOps),
|
||||||
|
bold('\nDeprecation Policy Reminder'),
|
||||||
|
deprecationPolicyNote,
|
||||||
|
reachingOut,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected formatGlobalOpts(opts: string[][]) {
|
||||||
|
const { dim } = getChalk();
|
||||||
|
const outLines: string[] = [];
|
||||||
|
let flagWidth = 0;
|
||||||
|
for (const opt of opts) {
|
||||||
|
flagWidth = Math.max(flagWidth, opt[0].length);
|
||||||
|
}
|
||||||
|
for (const opt of opts) {
|
||||||
|
const descriptionLines = opt[1].split('\n');
|
||||||
|
outLines.push(
|
||||||
|
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
|
||||||
|
);
|
||||||
|
outLines.push(
|
||||||
|
...descriptionLines
|
||||||
|
.slice(1)
|
||||||
|
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return outLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected formatCommands(commands: any[]): string {
|
protected formatCommands(commands: any[]): string {
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { stripIndent } from './utils/lazy';
|
|
||||||
import { exitWithExpectedError } from './errors';
|
export let unsupportedFlag = false;
|
||||||
|
|
||||||
export interface AppOptions {
|
export interface AppOptions {
|
||||||
// Prevent the default behavior of flushing stdout after running a command
|
// Prevent the default behavior of flushing stdout after running a command
|
||||||
@ -50,11 +50,10 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// support global --debug flag
|
// support global --debug flag
|
||||||
const debugIndex = cmdSlice.indexOf('--debug');
|
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||||
if (debugIndex > -1) {
|
|
||||||
process.env.DEBUG = '1';
|
process.env.DEBUG = '1';
|
||||||
cmdSlice.splice(debugIndex, 1);
|
|
||||||
}
|
}
|
||||||
|
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable bluebird long stack traces when in debug mode, must be set
|
// Enable bluebird long stack traces when in debug mode, must be set
|
||||||
@ -87,11 +86,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractBooleanFlag(argv: string[], flag: string): boolean {
|
||||||
|
const index = argv.indexOf(flag);
|
||||||
|
if (index >= 0) {
|
||||||
|
argv.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the command line refers to a command that has been deprecated
|
* Check whether the command line refers to a command that has been deprecated
|
||||||
* and removed and, if so, exit with an informative error message.
|
* and removed and, if so, exit with an informative error message.
|
||||||
*/
|
*/
|
||||||
export function checkDeletedCommand(argvSlice: string[]): void {
|
export function checkDeletedCommand(argvSlice: string[]): void {
|
||||||
|
const { ExpectedError } = require('./errors') as typeof import('./errors');
|
||||||
|
|
||||||
if (argvSlice[0] === 'help') {
|
if (argvSlice[0] === 'help') {
|
||||||
argvSlice = argvSlice.slice(1);
|
argvSlice = argvSlice.slice(1);
|
||||||
}
|
}
|
||||||
@ -101,17 +111,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
|
|||||||
version: string,
|
version: string,
|
||||||
verb = 'replaced',
|
verb = 'replaced',
|
||||||
) {
|
) {
|
||||||
exitWithExpectedError(stripIndent`
|
throw new ExpectedError(`\
|
||||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||||
Please use "balena ${alternative}" instead.
|
Please use "balena ${alternative}" instead.`);
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
function removed(oldCmd: string, alternative: string, version: string) {
|
function removed(oldCmd: string, alternative: string, version: string) {
|
||||||
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
|
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
|
||||||
if (alternative) {
|
if (alternative) {
|
||||||
msg = [msg, alternative].join('\n');
|
msg = [msg, alternative].join('\n');
|
||||||
}
|
}
|
||||||
exitWithExpectedError(msg);
|
throw new ExpectedError(msg);
|
||||||
}
|
}
|
||||||
const stopAlternative =
|
const stopAlternative =
|
||||||
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
|
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
|
||||||
|
@ -30,6 +30,12 @@ export const help = reachingOut;
|
|||||||
// is parsed, so its evaluation cannot happen at module loading time.
|
// is parsed, so its evaluation cannot happen at module loading time.
|
||||||
export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
|
export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
|
||||||
|
|
||||||
|
export const deprecationPolicyNote = `\
|
||||||
|
The balena CLI enforces its deprecation policy by exiting with an error a year
|
||||||
|
after the release of the next major version, unless the --unsupported option is
|
||||||
|
used. Find out more at: https://git.io/JRHUW#deprecation-policy
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take a multiline string like:
|
* Take a multiline string like:
|
||||||
* Line One
|
* Line One
|
||||||
@ -41,8 +47,8 @@ export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
|
|||||||
* ---------------
|
* ---------------
|
||||||
* where the length of the dash rows matches the length of the longest line.
|
* where the length of the dash rows matches the length of the longest line.
|
||||||
*/
|
*/
|
||||||
export function warnify(msg: string) {
|
export function warnify(msg: string, prefix = '[Warn] ') {
|
||||||
const lines = msg.split('\n').map((l) => `[Warn] ${l}`);
|
const lines = msg.split('\n').map((l) => `${prefix}${l}`);
|
||||||
const maxLength = Math.max(...lines.map((l) => l.length));
|
const maxLength = Math.max(...lines.map((l) => l.length));
|
||||||
const hr = '-'.repeat(maxLength);
|
const hr = '-'.repeat(maxLength);
|
||||||
return [hr, ...lines, hr].join('\n');
|
return [hr, ...lines, hr].join('\n');
|
||||||
@ -184,3 +190,13 @@ the next major version of the CLI (v13). The --v13 option may be used
|
|||||||
to enable the new names already now, and suppress a warning message.
|
to enable the new names already now, and suppress a warning message.
|
||||||
(The --v13 option will be silently ignored in CLI v13.)
|
(The --v13 option will be silently ignored in CLI v13.)
|
||||||
Find out more at: https://git.io/JRuZr`;
|
Find out more at: https://git.io/JRuZr`;
|
||||||
|
|
||||||
|
export function getNodeEngineVersionWarn(
|
||||||
|
version: string,
|
||||||
|
validVersions: string,
|
||||||
|
) {
|
||||||
|
version = version.startsWith('v') ? version.substring(1) : version;
|
||||||
|
return warnify(`\
|
||||||
|
Node.js version "${version}" does not satisfy requirement "${validVersions}"
|
||||||
|
This may cause unexpected behavior.`);
|
||||||
|
}
|
||||||
|
207
npm-shrinkwrap.json
generated
207
npm-shrinkwrap.json
generated
@ -2385,6 +2385,17 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/cacheable-request": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/http-cache-semantics": "*",
|
||||||
|
"@types/keyv": "*",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/responselike": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/caseless": {
|
"@types/caseless": {
|
||||||
"version": "0.12.2",
|
"version": "0.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
|
||||||
@ -2534,6 +2545,11 @@
|
|||||||
"integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==",
|
"integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/http-cache-semantics": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||||
|
},
|
||||||
"@types/http-proxy": {
|
"@types/http-proxy": {
|
||||||
"version": "1.17.7",
|
"version": "1.17.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
|
||||||
@ -2600,6 +2616,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
|
||||||
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
|
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
|
||||||
},
|
},
|
||||||
|
"@types/keyv": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/klaw": {
|
"@types/klaw": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz",
|
||||||
@ -2791,6 +2815,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/responselike": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/rewire": {
|
"@types/rewire": {
|
||||||
"version": "2.5.28",
|
"version": "2.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz",
|
||||||
@ -4561,6 +4593,11 @@
|
|||||||
"unset-value": "^1.0.0"
|
"unset-value": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cacheable-lookup": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
|
||||||
|
},
|
||||||
"cacheable-request": {
|
"cacheable-request": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
|
||||||
@ -8429,6 +8466,31 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"got": "^6.2.0",
|
"got": "^6.2.0",
|
||||||
"is-plain-obj": "^1.1.0"
|
"is-plain-obj": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"get-stream": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||||
|
},
|
||||||
|
"got": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||||
|
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
||||||
|
"requires": {
|
||||||
|
"create-error-class": "^3.0.0",
|
||||||
|
"duplexer3": "^0.1.4",
|
||||||
|
"get-stream": "^3.0.0",
|
||||||
|
"is-redirect": "^1.0.0",
|
||||||
|
"is-retry-allowed": "^1.0.0",
|
||||||
|
"is-stream": "^1.0.0",
|
||||||
|
"lowercase-keys": "^1.0.0",
|
||||||
|
"safe-buffer": "^5.0.1",
|
||||||
|
"timed-out": "^4.0.0",
|
||||||
|
"unzip-response": "^2.0.1",
|
||||||
|
"url-parse-lax": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ghauth": {
|
"ghauth": {
|
||||||
@ -8905,27 +8967,111 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"got": {
|
"got": {
|
||||||
"version": "6.7.1",
|
"version": "11.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||||
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"create-error-class": "^3.0.0",
|
"@sindresorhus/is": "^4.0.0",
|
||||||
"duplexer3": "^0.1.4",
|
"@szmarczak/http-timer": "^4.0.5",
|
||||||
"get-stream": "^3.0.0",
|
"@types/cacheable-request": "^6.0.1",
|
||||||
"is-redirect": "^1.0.0",
|
"@types/responselike": "^1.0.0",
|
||||||
"is-retry-allowed": "^1.0.0",
|
"cacheable-lookup": "^5.0.3",
|
||||||
"is-stream": "^1.0.0",
|
"cacheable-request": "^7.0.1",
|
||||||
"lowercase-keys": "^1.0.0",
|
"decompress-response": "^6.0.0",
|
||||||
"safe-buffer": "^5.0.1",
|
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||||
"timed-out": "^4.0.0",
|
"lowercase-keys": "^2.0.0",
|
||||||
"unzip-response": "^2.0.1",
|
"p-cancelable": "^2.0.0",
|
||||||
"url-parse-lax": "^1.0.0"
|
"responselike": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sindresorhus/is": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g=="
|
||||||
|
},
|
||||||
|
"@szmarczak/http-timer": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||||
|
"requires": {
|
||||||
|
"defer-to-connect": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cacheable-request": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
|
||||||
|
"requires": {
|
||||||
|
"clone-response": "^1.0.2",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"http-cache-semantics": "^4.0.0",
|
||||||
|
"keyv": "^4.0.0",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"normalize-url": "^6.0.1",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"requires": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defer-to-connect": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
|
||||||
|
},
|
||||||
"get-stream": {
|
"get-stream": {
|
||||||
"version": "3.0.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"requires": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json-buffer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||||
|
},
|
||||||
|
"keyv": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==",
|
||||||
|
"requires": {
|
||||||
|
"json-buffer": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lowercase-keys": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
|
||||||
|
},
|
||||||
|
"mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||||
|
},
|
||||||
|
"normalize-url": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
|
||||||
|
},
|
||||||
|
"p-cancelable": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
|
||||||
|
},
|
||||||
|
"responselike": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
|
||||||
|
"requires": {
|
||||||
|
"lowercase-keys": "^2.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -9388,6 +9534,15 @@
|
|||||||
"sshpk": "^1.7.0"
|
"sshpk": "^1.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http2-wrapper": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||||
|
"requires": {
|
||||||
|
"quick-lru": "^5.1.1",
|
||||||
|
"resolve-alpn": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"https-proxy-agent": {
|
"https-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||||
@ -14676,6 +14831,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||||
},
|
},
|
||||||
|
"quick-lru": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -15851,6 +16011,11 @@
|
|||||||
"path-parse": "^1.0.6"
|
"path-parse": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"resolve-alpn": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA=="
|
||||||
|
},
|
||||||
"resolve-dir": {
|
"resolve-dir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
|
||||||
@ -16347,13 +16512,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"sinon": {
|
"sinon": {
|
||||||
"version": "11.1.1",
|
"version": "11.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
|
||||||
"integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==",
|
"integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sinonjs/commons": "^1.8.3",
|
"@sinonjs/commons": "^1.8.3",
|
||||||
"@sinonjs/fake-timers": "^7.1.0",
|
"@sinonjs/fake-timers": "^7.1.2",
|
||||||
"@sinonjs/samsam": "^6.0.2",
|
"@sinonjs/samsam": "^6.0.2",
|
||||||
"diff": "^5.0.0",
|
"diff": "^5.0.0",
|
||||||
"nise": "^5.1.0",
|
"nise": "^5.1.0",
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"postinstall": "node patches/apply-patches.js",
|
"postinstall": "node patches/apply-patches.js",
|
||||||
"prebuild": "rimraf build/ build-bin/",
|
"prebuild": "rimraf build/ build-bin/",
|
||||||
"build": "npm run build:src && npm run catch-uncommitted",
|
"build": "npm run build:src && npm run catch-uncommitted",
|
||||||
|
"build:t": "npm run lint && npm run build:fast && npm run build:test",
|
||||||
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
|
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
|
||||||
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
|
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
|
||||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
|
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
|
||||||
@ -185,7 +186,7 @@
|
|||||||
"publish-release": "^1.6.1",
|
"publish-release": "^1.6.1",
|
||||||
"rewire": "^5.0.0",
|
"rewire": "^5.0.0",
|
||||||
"simple-git": "^2.40.0",
|
"simple-git": "^2.40.0",
|
||||||
"sinon": "^11.1.1",
|
"sinon": "^11.1.2",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
@ -236,6 +237,7 @@
|
|||||||
"glob": "^7.1.7",
|
"glob": "^7.1.7",
|
||||||
"global-agent": "^2.1.12",
|
"global-agent": "^2.1.12",
|
||||||
"global-tunnel-ng": "^2.1.1",
|
"global-tunnel-ng": "^2.1.1",
|
||||||
|
"got": "^11.8.2",
|
||||||
"humanize": "0.0.9",
|
"humanize": "0.0.9",
|
||||||
"ignore": "^5.1.8",
|
"ignore": "^5.1.8",
|
||||||
"inquirer": "^7.3.3",
|
"inquirer": "^7.3.3",
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
|
|
||||||
const HELP_MESSAGE = '';
|
const HELP_MESSAGE = '';
|
||||||
|
@ -22,9 +22,9 @@ import { promises as fs } from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { stripIndent } from '../../lib/utils/lazy';
|
import { stripIndent } from '../../lib/utils/lazy';
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
import {
|
import {
|
||||||
ExpectedTarStreamFiles,
|
ExpectedTarStreamFiles,
|
||||||
|
@ -21,9 +21,9 @@ import * as _ from 'lodash';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||||
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||||
import {
|
import {
|
||||||
ExpectedTarStreamFiles,
|
ExpectedTarStreamFiles,
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
|
|
||||||
describe('balena device move', function () {
|
describe('balena device move', function () {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
|
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
|
|
||||||
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
|
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
|
|
||||||
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../../helpers';
|
import { cleanOutput, runCommand } from '../../helpers';
|
||||||
|
|
||||||
import { isV13 } from '../../../lib/utils/version';
|
import { isV13 } from '../../../lib/utils/version';
|
||||||
|
2
tests/commands/env/add.spec.ts
vendored
2
tests/commands/env/add.spec.ts
vendored
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { runCommand } from '../../helpers';
|
import { runCommand } from '../../helpers';
|
||||||
|
|
||||||
describe('balena env add', function () {
|
describe('balena env add', function () {
|
||||||
|
2
tests/commands/env/envs.spec.ts
vendored
2
tests/commands/env/envs.spec.ts
vendored
@ -18,7 +18,7 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { stripIndent } from '../../../lib/utils/lazy';
|
import { stripIndent } from '../../../lib/utils/lazy';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { runCommand } from '../../helpers';
|
import { runCommand } from '../../helpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
2
tests/commands/env/rename.spec.ts
vendored
2
tests/commands/env/rename.spec.ts
vendored
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { runCommand } from '../../helpers';
|
import { runCommand } from '../../helpers';
|
||||||
|
|
||||||
describe('balena env rename', function () {
|
describe('balena env rename', function () {
|
||||||
|
2
tests/commands/env/rm.spec.ts
vendored
2
tests/commands/env/rm.spec.ts
vendored
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
import { runCommand } from '../../helpers';
|
import { runCommand } from '../../helpers';
|
||||||
|
|
||||||
describe('balena env rm', function () {
|
describe('balena env rm', function () {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
import * as messages from '../../build/utils/messages';
|
import * as messages from '../../build/utils/messages';
|
||||||
|
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
import { SupervisorMock } from '../supervisor-mock';
|
import { SupervisorMock } from '../nock/supervisor-mock';
|
||||||
|
|
||||||
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
|
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import * as tmp from 'tmp';
|
|||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
const tmpNameAsync = promisify(tmp.tmpName);
|
const tmpNameAsync = promisify(tmp.tmpName);
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
describe('balena os configure', function () {
|
describe('balena os configure', function () {
|
||||||
|
@ -19,8 +19,8 @@ import { expect } from 'chai';
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { BuilderMock, builderResponsePath } from '../builder-mock';
|
import { BuilderMock, builderResponsePath } from '../nock/builder-mock';
|
||||||
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
|
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
import {
|
import {
|
||||||
|
@ -19,7 +19,7 @@ import { expect } from 'chai';
|
|||||||
import mock = require('mock-require');
|
import mock = require('mock-require');
|
||||||
import { createServer, Server } from 'net';
|
import { createServer, Server } from 'net';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { cleanOutput, runCommand } from '../helpers';
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
|
|
||||||
// "itSS" means "it() Skip Standalone"
|
// "itSS" means "it() Skip Standalone"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
||||||
import { BalenaAPIMock } from '../balena-api-mock';
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
import { runCommand } from '../helpers';
|
import { runCommand } from '../helpers';
|
||||||
|
|
||||||
const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||||
|
@ -19,10 +19,20 @@ import { set as setEsVersion } from '@balena/es-version';
|
|||||||
// Set the desired es version for downstream modules that support it
|
// Set the desired es version for downstream modules that support it
|
||||||
setEsVersion('es2018');
|
setEsVersion('es2018');
|
||||||
|
|
||||||
|
// Disable Sentry.io error reporting while running test code
|
||||||
|
process.env.BALENARC_NO_SENTRY = '1';
|
||||||
|
|
||||||
|
// Disable deprecation checks while running test code
|
||||||
|
import { DeprecationChecker } from '../build/deprecation';
|
||||||
|
DeprecationChecker.disable();
|
||||||
|
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
// Use a temporary dir for tests data
|
// Use a temporary dir for tests data
|
||||||
process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name;
|
process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name;
|
||||||
|
console.error(
|
||||||
|
`[debug] tests/config-tests.ts: BALENARC_DATA_DIRECTORY="${process.env.BALENARC_DATA_DIRECTORY}"`,
|
||||||
|
);
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug?
|
EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug?
|
||||||
|
320
tests/deprecation.spec.ts
Normal file
320
tests/deprecation.spec.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2021 Balena Ltd.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as settings from 'balena-settings-client';
|
||||||
|
import * as getStorage from 'balena-settings-storage';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import mock = require('mock-require');
|
||||||
|
import * as semver from 'semver';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
|
import * as packageJSON from '../package.json';
|
||||||
|
import {
|
||||||
|
DeprecationChecker,
|
||||||
|
ReleaseTimestampsByVersion,
|
||||||
|
} from '../build/deprecation';
|
||||||
|
import { BalenaAPIMock } from './nock/balena-api-mock';
|
||||||
|
import { NpmMock } from './nock/npm-mock';
|
||||||
|
import { runCommand, TestOutput } from './helpers';
|
||||||
|
|
||||||
|
// "itSS" means "it() Skip Standalone"
|
||||||
|
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||||
|
|
||||||
|
describe('DeprecationChecker', function () {
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const anHourAgo = now - 3600000;
|
||||||
|
const currentMajor = semver.major(packageJSON.version, { loose: true });
|
||||||
|
const nextMajorVersion = `${currentMajor + 1}.0.0`;
|
||||||
|
const dataDirectory = settings.get<string>('dataDirectory');
|
||||||
|
const storageModPath = 'balena-settings-storage';
|
||||||
|
const mockStorage = getStorage({ dataDirectory });
|
||||||
|
let api: BalenaAPIMock;
|
||||||
|
let npm: NpmMock;
|
||||||
|
let checker: DeprecationChecker;
|
||||||
|
let getStub: sinon.SinonStub<
|
||||||
|
Parameters<typeof mockStorage.get>,
|
||||||
|
ReturnType<typeof mockStorage.get>
|
||||||
|
>;
|
||||||
|
let setStub: sinon.SinonStub<
|
||||||
|
Parameters<typeof mockStorage.set>,
|
||||||
|
ReturnType<typeof mockStorage.set>
|
||||||
|
>;
|
||||||
|
|
||||||
|
// see also DeprecationChecker.disable() in tests/config-tests.ts
|
||||||
|
this.beforeAll(() => DeprecationChecker.enable());
|
||||||
|
this.afterAll(() => DeprecationChecker.disable());
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
npm = new NpmMock();
|
||||||
|
api = new BalenaAPIMock();
|
||||||
|
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||||
|
api.expectGetMixpanel({ optional: true });
|
||||||
|
checker = new DeprecationChecker(packageJSON.version);
|
||||||
|
|
||||||
|
getStub = sandbox.stub(mockStorage, 'get').withArgs(checker.cacheFile);
|
||||||
|
|
||||||
|
setStub = sandbox
|
||||||
|
.stub(mockStorage, 'set')
|
||||||
|
.withArgs(checker.cacheFile, sinon.match.any);
|
||||||
|
|
||||||
|
mock(storageModPath, () => mockStorage);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(() => {
|
||||||
|
// Check all expected api calls have been made and clean up.
|
||||||
|
(mockStorage.get as sinon.SinonStub).restore();
|
||||||
|
(mockStorage.set as sinon.SinonStub).restore();
|
||||||
|
|
||||||
|
// originalStorage.set.restore();
|
||||||
|
api.done();
|
||||||
|
npm.done();
|
||||||
|
mock.stop(storageModPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
itSS(
|
||||||
|
'should warn if this version of the CLI is deprecated (isTTY is true)',
|
||||||
|
async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just over half a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
// Force isTTY to be true. It happens to be false (undefined) when
|
||||||
|
// the tests run on balenaCI on Windows.
|
||||||
|
const originalIsTTY = process.stderr.isTTY;
|
||||||
|
process.stderr.isTTY = true;
|
||||||
|
let result: TestOutput;
|
||||||
|
try {
|
||||||
|
result = await runCommand('version');
|
||||||
|
} finally {
|
||||||
|
process.stderr.isTTY = originalIsTTY;
|
||||||
|
}
|
||||||
|
const { out, err } = result;
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal(
|
||||||
|
checker.getDeprecationMsg(checker.deprecationDays + 1) + '\n',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
itSS(
|
||||||
|
'should NOT warn if this version of the CLI is deprecated (isTTY is false)',
|
||||||
|
async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just over half a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
// Force isTTY to be false (undefined). It happens to be true when
|
||||||
|
// the tests run on balenaCI on macOS and Linux.
|
||||||
|
const originalIsTTY = process.stderr.isTTY;
|
||||||
|
process.stderr.isTTY = undefined;
|
||||||
|
let result: TestOutput;
|
||||||
|
try {
|
||||||
|
result = await runCommand('version');
|
||||||
|
} finally {
|
||||||
|
process.stderr.isTTY = originalIsTTY;
|
||||||
|
}
|
||||||
|
const { out, err } = result;
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal('');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
itSS(
|
||||||
|
'should NOT warn with --unsupported (deprecated but not expired)',
|
||||||
|
async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just over half a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version --unsupported');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(0);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.be.empty;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
itSS('should exit if this version of the CLI has expired', async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just over a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.expiryDays + 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal('');
|
||||||
|
expect(err.join('')).to.include(
|
||||||
|
checker.getExpiryMsg(checker.expiryDays + 1) + '\n\n',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
itSS('should NOT exit with --unsupported (expired version)', async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just over a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.expiryDays + 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('--unsupported version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(0);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query the npm registry (empty cache file)', async () => {
|
||||||
|
npm.expectGetBalenaCli({
|
||||||
|
version: nextMajorVersion,
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
getStub.resolves(undefined);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(1);
|
||||||
|
expect(setStub.firstCall.args.length).to.equal(2);
|
||||||
|
const [name, obj] = setStub.firstCall.args;
|
||||||
|
expect(name).to.equal(checker.cacheFile);
|
||||||
|
expect(obj).to.have.property(nextMajorVersion);
|
||||||
|
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
|
||||||
|
expect(lastFetched).to.be.greaterThan(anHourAgo);
|
||||||
|
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query the npm registry (not recently fetched)', async () => {
|
||||||
|
npm.expectGetBalenaCli({
|
||||||
|
version: nextMajorVersion,
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: new Date(
|
||||||
|
checker.now -
|
||||||
|
(checker.majorVersionFetchIntervalDays + 1) * checker.msInDay,
|
||||||
|
).toISOString(),
|
||||||
|
};
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(1);
|
||||||
|
expect(setStub.firstCall.args.length).to.equal(2);
|
||||||
|
const [name, obj] = setStub.firstCall.args;
|
||||||
|
expect(name).to.equal(checker.cacheFile);
|
||||||
|
expect(obj).to.have.property(nextMajorVersion);
|
||||||
|
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
|
||||||
|
expect(lastFetched).to.be.greaterThan(anHourAgo);
|
||||||
|
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
itSS('should NOT query the npm registry (recently fetched)', async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: new Date(
|
||||||
|
checker.now -
|
||||||
|
(checker.majorVersionFetchIntervalDays - 1) * checker.msInDay,
|
||||||
|
).toISOString(),
|
||||||
|
};
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
itSS('should NOT query the npm registry (cached value)', async () => {
|
||||||
|
const mockCache: ReleaseTimestampsByVersion = {
|
||||||
|
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
// pretend the next major was released just under half a year ago
|
||||||
|
mockCache[nextMajorVersion] = new Date(
|
||||||
|
checker.now - (checker.deprecationDays - 1) * checker.msInDay,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
getStub.resolves(mockCache);
|
||||||
|
|
||||||
|
const { out, err } = await runCommand('version');
|
||||||
|
|
||||||
|
expect(setStub.callCount).to.equal(0);
|
||||||
|
expect(getStub.callCount).to.equal(1);
|
||||||
|
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||||
|
|
||||||
|
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||||
|
expect(err.join('')).to.equal('');
|
||||||
|
});
|
||||||
|
});
|
@ -28,8 +28,8 @@ import { streamToBuffer } from 'tar-utils';
|
|||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
import { stripIndent } from '../lib/utils/lazy';
|
import { stripIndent } from '../lib/utils/lazy';
|
||||||
import { BuilderMock } from './builder-mock';
|
import { BuilderMock } from './nock/builder-mock';
|
||||||
import { DockerMock } from './docker-mock';
|
import { DockerMock } from './nock/docker-mock';
|
||||||
import {
|
import {
|
||||||
cleanOutput,
|
cleanOutput,
|
||||||
deepJsonParse,
|
deepJsonParse,
|
||||||
|
118
tests/helpers.ts
118
tests/helpers.ts
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2019-2020 Balena Ltd.
|
* Copyright 2019-2021 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,36 +15,59 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFile } from 'child_process';
|
|
||||||
import intercept = require('intercept-stdout');
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import * as nock from 'nock';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import * as balenaCLI from '../build/app';
|
import * as packageJSON from '../package.json';
|
||||||
|
|
||||||
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
||||||
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
||||||
|
|
||||||
interface TestOutput {
|
export interface TestOutput {
|
||||||
err: string[]; // stderr
|
err: string[]; // stderr
|
||||||
out: string[]; // stdout
|
out: string[]; // stdout
|
||||||
exitCode?: number; // process.exitCode
|
exitCode?: number; // process.exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesNodeEngineVersionWarn(msg: string) {
|
||||||
|
if (/^-----+\r?\n?$/.test(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const cleanup = (line: string): string[] =>
|
||||||
|
line
|
||||||
|
.replace(/-----+/g, '')
|
||||||
|
.replace(/"\d+\.\d+\.\d+"/, '"x.y.z"')
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l);
|
||||||
|
|
||||||
|
const { getNodeEngineVersionWarn } = require('../build/utils/messages');
|
||||||
|
let nodeEngineWarn: string = getNodeEngineVersionWarn(
|
||||||
|
'x.y.z',
|
||||||
|
packageJSON.engines.node,
|
||||||
|
);
|
||||||
|
const nodeEngineWarnArray = cleanup(nodeEngineWarn);
|
||||||
|
nodeEngineWarn = nodeEngineWarnArray.join('\n');
|
||||||
|
msg = cleanup(msg).join('\n');
|
||||||
|
return msg === nodeEngineWarn || nodeEngineWarnArray.includes(msg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
|
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
|
||||||
* other lines that can be ignored for testing purposes.
|
* other lines that can be ignored for testing purposes.
|
||||||
* @param testOutput
|
* @param testOutput
|
||||||
*/
|
*/
|
||||||
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
export function filterCliOutputForTests({
|
||||||
const { matchesNodeEngineVersionWarn } =
|
err,
|
||||||
require('../automation/utils') as typeof import('../automation/utils');
|
out,
|
||||||
|
}: {
|
||||||
|
err: string[];
|
||||||
|
out: string[];
|
||||||
|
}): { err: string[]; out: string[] } {
|
||||||
return {
|
return {
|
||||||
exitCode: testOutput.exitCode,
|
err: err.filter(
|
||||||
err: testOutput.err.filter(
|
|
||||||
(line: string) =>
|
(line: string) =>
|
||||||
|
line &&
|
||||||
!line.match(/\[debug\]/i) &&
|
!line.match(/\[debug\]/i) &&
|
||||||
// TODO stop this warning message from appearing when running
|
// TODO stop this warning message from appearing when running
|
||||||
// sdk.setSharedOptions multiple times in the same process
|
// sdk.setSharedOptions multiple times in the same process
|
||||||
@ -52,7 +75,7 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
|||||||
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
|
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
|
||||||
!matchesNodeEngineVersionWarn(line),
|
!matchesNodeEngineVersionWarn(line),
|
||||||
),
|
),
|
||||||
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)),
|
out: out.filter((line: string) => line && !line.match(/\[debug\]/i)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +84,9 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
|||||||
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
||||||
*/
|
*/
|
||||||
async function runCommandInProcess(cmd: string): Promise<TestOutput> {
|
async function runCommandInProcess(cmd: string): Promise<TestOutput> {
|
||||||
|
const balenaCLI = await import('../build/app');
|
||||||
|
const intercept = await import('intercept-stdout');
|
||||||
|
|
||||||
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
||||||
|
|
||||||
const err: string[] = [];
|
const err: string[] = [];
|
||||||
@ -85,12 +111,13 @@ async function runCommandInProcess(cmd: string): Promise<TestOutput> {
|
|||||||
} finally {
|
} finally {
|
||||||
unhookIntercept();
|
unhookIntercept();
|
||||||
}
|
}
|
||||||
return filterCliOutputForTests({
|
const filtered = filterCliOutputForTests({ err, out });
|
||||||
err,
|
return {
|
||||||
out,
|
err: filtered.err,
|
||||||
|
out: filtered.out,
|
||||||
// this makes sense if `process.exit()` was stubbed with sinon
|
// this makes sense if `process.exit()` was stubbed with sinon
|
||||||
exitCode: process.exitCode,
|
exitCode: process.exitCode,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,6 +156,7 @@ async function runCommandInSubprocess(
|
|||||||
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
|
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
|
||||||
BALENARC_DO_PROXY: '127.0.0.1,localhost',
|
BALENARC_DO_PROXY: '127.0.0.1,localhost',
|
||||||
};
|
};
|
||||||
|
const { execFile } = await import('child_process');
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const child = execFile(
|
const child = execFile(
|
||||||
standalonePath,
|
standalonePath,
|
||||||
@ -141,11 +169,12 @@ async function runCommandInSubprocess(
|
|||||||
// non-zero exit code. Usually this is harmless/expected, as
|
// non-zero exit code. Usually this is harmless/expected, as
|
||||||
// the CLI child process is tested for error conditions.
|
// the CLI child process is tested for error conditions.
|
||||||
if ($error && process.env.DEBUG) {
|
if ($error && process.env.DEBUG) {
|
||||||
console.error(`
|
const msg = `
|
||||||
[debug] Error (possibly expected) executing child CLI process "${standalonePath}"
|
Error (possibly expected) executing child CLI process "${standalonePath}"
|
||||||
------------------------------------------------------------------
|
${$error}`;
|
||||||
${$error}
|
const { warnify } =
|
||||||
------------------------------------------------------------------`);
|
require('../build/utils/messages') as typeof import('../build/utils/messages');
|
||||||
|
console.error(warnify(msg, '[debug] '));
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
@ -166,11 +195,16 @@ ${$error}
|
|||||||
.filter((l) => l)
|
.filter((l) => l)
|
||||||
.map((l) => l + '\n');
|
.map((l) => l + '\n');
|
||||||
|
|
||||||
return filterCliOutputForTests({
|
const filtered = filterCliOutputForTests({
|
||||||
exitCode,
|
|
||||||
err: splitLines(stderr),
|
err: splitLines(stderr),
|
||||||
out: splitLines(stdout),
|
out: splitLines(stdout),
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
err: filtered.err,
|
||||||
|
out: filtered.out,
|
||||||
|
// this makes sense if `process.exit()` was stubbed with sinon
|
||||||
|
exitCode,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -190,11 +224,12 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const { promises: fs } = await import('fs');
|
||||||
await fs.access(standalonePath);
|
await fs.access(standalonePath);
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Standalone executable not found: "${standalonePath}"`);
|
throw new Error(`Standalone executable not found: "${standalonePath}"`);
|
||||||
}
|
}
|
||||||
const proxy = await import('./proxy-server');
|
const proxy = await import('./nock/proxy-server');
|
||||||
const [proxyPort] = await proxy.createProxyServerOnce();
|
const [proxyPort] = await proxy.createProxyServerOnce();
|
||||||
return runCommandInSubprocess(cmd, proxyPort);
|
return runCommandInSubprocess(cmd, proxyPort);
|
||||||
} else {
|
} else {
|
||||||
@ -202,22 +237,6 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const balenaAPIMock = () => {
|
|
||||||
if (!nock.isActive()) {
|
|
||||||
nock.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return nock(/./).get('/config/vars').reply(200, {
|
|
||||||
reservedNames: [],
|
|
||||||
reservedNamespaces: [],
|
|
||||||
invalidRegex: '/^d|W/',
|
|
||||||
whiteListedNames: [],
|
|
||||||
whiteListedNamespaces: [],
|
|
||||||
blackListedNames: [],
|
|
||||||
configVarSchema: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function cleanOutput(
|
export function cleanOutput(
|
||||||
output: string[] | string,
|
output: string[] | string,
|
||||||
collapseBlank = false,
|
collapseBlank = false,
|
||||||
@ -226,11 +245,17 @@ export function cleanOutput(
|
|||||||
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
|
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
|
||||||
: (line: string) => monochrome(line.trim());
|
: (line: string) => monochrome(line.trim());
|
||||||
|
|
||||||
return _(_.castArray(output))
|
const result: string[] = [];
|
||||||
.map((log: string) => log.split('\n').map(cleanLine))
|
output = typeof output === 'string' ? [output] : output;
|
||||||
.flatten()
|
for (const lines of output) {
|
||||||
.compact()
|
for (let line of lines.split('\n')) {
|
||||||
.value();
|
line = cleanLine(line);
|
||||||
|
if (line) {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -320,6 +345,7 @@ export function deepJsonParse(data: any): any {
|
|||||||
export async function switchSentry(
|
export async function switchSentry(
|
||||||
enabled: boolean | undefined,
|
enabled: boolean | undefined,
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean | undefined> {
|
||||||
|
const balenaCLI = await import('../build/app');
|
||||||
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
||||||
if (sentryOpts) {
|
if (sentryOpts) {
|
||||||
const sentryStatus = sentryOpts.enabled;
|
const sentryStatus = sentryOpts.enabled;
|
||||||
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
|||||||
import { NockMock, ScopeOpts } from './nock-mock';
|
import { NockMock, ScopeOpts } from './nock-mock';
|
||||||
|
|
||||||
export const apiResponsePath = path.normalize(
|
export const apiResponsePath = path.normalize(
|
||||||
path.join(__dirname, 'test-data', 'api-response'),
|
path.join(__dirname, '..', 'test-data', 'api-response'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const jHeader = { 'Content-Type': 'application/json' };
|
const jHeader = { 'Content-Type': 'application/json' };
|
@ -22,7 +22,7 @@ import * as zlib from 'zlib';
|
|||||||
import { NockMock } from './nock-mock';
|
import { NockMock } from './nock-mock';
|
||||||
|
|
||||||
export const builderResponsePath = path.normalize(
|
export const builderResponsePath = path.normalize(
|
||||||
path.join(__dirname, 'test-data', 'builder-response'),
|
path.join(__dirname, '..', 'test-data', 'builder-response'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export class BuilderMock extends NockMock {
|
export class BuilderMock extends NockMock {
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
|||||||
import { NockMock, ScopeOpts } from './nock-mock';
|
import { NockMock, ScopeOpts } from './nock-mock';
|
||||||
|
|
||||||
export const dockerResponsePath = path.normalize(
|
export const dockerResponsePath = path.normalize(
|
||||||
path.join(__dirname, 'test-data', 'docker-response'),
|
path.join(__dirname, '..', 'test-data', 'docker-response'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export class DockerMock extends NockMock {
|
export class DockerMock extends NockMock {
|
50
tests/nock/npm-mock.ts
Normal file
50
tests/nock/npm-mock.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Balena Ltd.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NockMock } from './nock-mock';
|
||||||
|
|
||||||
|
const jHeader = { 'Content-Type': 'application/json' };
|
||||||
|
|
||||||
|
export class NpmMock extends NockMock {
|
||||||
|
constructor() {
|
||||||
|
super(/registry\.npmjs\.org/);
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectGetBalenaCli({
|
||||||
|
version,
|
||||||
|
publishedAt,
|
||||||
|
notFound = false,
|
||||||
|
optional = false,
|
||||||
|
persist = false,
|
||||||
|
}: {
|
||||||
|
version: string;
|
||||||
|
publishedAt: string;
|
||||||
|
notFound?: boolean;
|
||||||
|
optional?: boolean;
|
||||||
|
persist?: boolean;
|
||||||
|
}) {
|
||||||
|
const interceptor = this.optGet(`/balena-cli/${version}`, {
|
||||||
|
optional,
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
if (notFound) {
|
||||||
|
interceptor.reply(404, `version not found: ${version}`, jHeader);
|
||||||
|
} else {
|
||||||
|
interceptor.reply(200, { versionist: { publishedAt } }, jHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ import { Readable } from 'stream';
|
|||||||
import { NockMock, ScopeOpts } from './nock-mock';
|
import { NockMock, ScopeOpts } from './nock-mock';
|
||||||
|
|
||||||
export const dockerResponsePath = path.normalize(
|
export const dockerResponsePath = path.normalize(
|
||||||
path.join(__dirname, 'test-data', 'docker-response'),
|
path.join(__dirname, '..', 'test-data', 'docker-response'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export class SupervisorMock extends NockMock {
|
export class SupervisorMock extends NockMock {
|
Loading…
Reference in New Issue
Block a user