@amplitude/ampli
Version:
Amplitude CLI
533 lines (532 loc) • 22.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.zoneFlagOptions = void 0;
const command_1 = require("@oclif/command");
const lodash_1 = require("lodash");
const path = require("path");
const json5 = require("json5");
const chalk_1 = require("chalk");
const inquirer = require("inquirer");
const inquirerPrompt = require("inquirer-autocomplete-prompt");
const fs = require("./util/fs");
const debug_1 = require("./debug");
const ampli_1 = require("./ampli");
const sentry_1 = require("./sentry");
const settings_1 = require("./settings");
const errors_1 = require("./errors");
const constants_1 = require("./constants");
const TerminalWriter_1 = require("./stdout/TerminalWriter");
const SettingsLoadError_1 = require("./errors/SettingsLoadError");
const semver_1 = require("./util/semver");
const base_1 = require("./actions/base");
const init_1 = require("./actions/init");
const runtime_1 = require("./util/runtime");
const icons_1 = require("./ui/icons");
const mergeConflicts_1 = require("./util/git/mergeConflicts");
const pull_1 = require("./actions/pull");
const types_1 = require("./types");
const string_1 = require("./util/string");
const configure_1 = require("./actions/configure");
inquirer.registerPrompt('autocomplete', inquirerPrompt);
const stdout = new TerminalWriter_1.default();
const HINT_INFO = { level: sentry_1.Severity.Info };
exports.zoneFlagOptions = {
char: 'z',
description: 'regional zone',
default: '',
options: [...types_1.ALL_ZONES],
};
class Base extends command_1.Command {
constructor() {
super(...arguments);
this.setFlags = [];
this.branchMapping = {};
this.print = (text) => stdout.print(text);
this.println = (text) => stdout.println(text);
this.actionConfig = (id) => ({
id,
print: this.print,
println: this.println,
debug: Base.debug.extend(id),
error: (err, exitCode) => this.error(err, { exit: exitCode }),
});
}
getSettings() {
return settings_1.getSettings();
}
async init() {
const { flags: commonFlags } = super.parse(undefined, [...this.argv]);
const { renameConfigs, projectDir } = commonFlags;
let { userDir } = commonFlags;
let userTokensDir;
const { token, withOrg } = commonFlags;
if (token || withOrg) {
const tempUserDir = await fs.tmpDir();
this.tempUserDir = tempUserDir;
if (withOrg) {
await this.createTempUserConfigWithOrg(userDir, withOrg);
userTokensDir = userDir;
}
userDir = tempUserDir;
}
this.withOrg = withOrg;
settings_1.initSettings({ userDir, projectDir, userTokensDir });
this.parse(undefined, [...this.argv]);
this.setFlags = this.getSetFlags();
this.projectDir = projectDir;
sentry_1.default.captureEnvironment(this.config);
sentry_1.default.captureCommand(this.id);
ampli_1.ampli.load({
environment: (!process.env.APP_ENV || process.env.APP_ENV === 'production')
? 'production'
: 'development',
});
this.ampliLoaded = true;
if (semver_1.default.fromString(process.versions.node).major < constants_1.APP_SETTINGS.NODE_VERSION_MINIMUM) {
this.println(errors_1.USER_ERROR_MESSAGES.updateNode());
}
const { current: userCurrentConfigPath } = settings_1.userConfigPaths(userDir);
const { legacy: projectLegacyConfigPath, current: projectCurrentConfigPath } = settings_1.projectConfigPaths(projectDir);
if (renameConfigs) {
await this.renameProjectConfigFile(projectLegacyConfigPath, projectCurrentConfigPath, userCurrentConfigPath);
}
await this.checkConfigIsValid(userCurrentConfigPath, 'login');
await this.checkConfigIsValid(projectCurrentConfigPath, 'init', await this.resolveProjectMergeConflicts.bind(this));
const settings = this.getSettings();
const user = settings.projectUser();
if (user && user.id) {
sentry_1.default.captureUser(user);
}
}
parse(options, argv) {
const output = super.parse(options, argv);
const safeFlags = Base.getSanitizedFlags(output.flags);
sentry_1.default.captureCommandFlags(safeFlags);
if (output.flags.debug) {
debug_1.default.enable();
const commandName = this.id || 'unknown';
const date = (new Date()).toISOString().replace(/:/g, '_');
const logfile = path.join(this.config.cacheDir, `ampli-${commandName}-${date}.log`);
debug_1.default.logToFile(logfile);
Base.logLogFileDetails('Logging to file.');
Base.debug(`command: %o\n`, {
id: this.id || 'unknown',
flags: safeFlags,
args: output.args,
});
const config = this.config;
Base.debug(`system: %o\n`, {
itly: config.version,
node: process.version,
platform: config.platform,
arch: config.arch,
windows: config.windows ? 'yes' : 'no',
});
const settings = this.getSettings();
const user = settings.projectUser();
const org = settings.getOrgId();
const workspace = settings.getWorkspaceId();
const source = settings.getSourceId();
Base.debug(`user: %o\n`, {
user: user === null || user === void 0 ? void 0 : user.id,
org,
workspace,
source,
});
}
return output;
}
async catch(e) {
var _a;
if (e && e.code === 'EEXIT') {
throw e;
}
this.isFailed = true;
let hint;
let errorMessage = (e && e.message) ? e.message : e;
if (!e || (e.name === 'Error' && !e.message)) {
errorMessage = errors_1.USER_ERROR_MESSAGES.unexpectedError('');
}
Base.debug(e);
if (constants_1.APP_SETTINGS.localDevelopment && !Base.debug.enabled) {
stdout.println(errorMessage);
}
const { response } = e;
if (response) {
const { errors, status } = response;
let { error } = response;
if (!error) {
error = lodash_1.isArray(errors) ? (_a = errors[0]) === null || _a === void 0 ? void 0 : _a.message : errors;
}
switch (status) {
case 401:
errorMessage = errors_1.USER_ERROR_MESSAGES.unauthorized(`${lodash_1.upperFirst(error)}.`);
hint = HINT_INFO;
break;
default:
errorMessage = errors_1.USER_ERROR_MESSAGES.unexpectedError(`
${icons_1.ICON_ERROR} ${lodash_1.upperFirst(error)}`);
}
}
if (errorMessage === 'Response Error: 400 Bad Request') {
errorMessage = errors_1.USER_ERROR_MESSAGES.badRequest;
hint = HINT_INFO;
}
stdout.println(`${icons_1.ICON_ERROR_W_TEXT} ${errorMessage}`);
if (this.ampliLoaded) {
try {
const user = this.getCommandUser();
if (user && user.id) {
if (!this.withOrg) {
base_1.default.identifyUser(user);
}
ampli_1.ampli.commandFailed(user.id, Object.assign(Object.assign(Object.assign({ name: Base.convertIdToName(this.id), version: constants_1.APP_SETTINGS.app.version }, runtime_1.getAmpliRuntimeProperties(this.getSettings().getRawRuntime())), { flags: this.setFlags }), this.branchMapping));
}
}
catch (err) {
}
}
sentry_1.default.captureException(e, hint);
await this.finally(e);
process.exit((e && e.oclif && e.oclif.exit > 0)
? e.oclif.exit
: 1);
}
async finally(e) {
if (this.ampliLoaded && !this.isFailed) {
const user = this.getCommandUser();
if (user && user.id) {
if (!this.withOrg) {
base_1.default.identifyUser(user);
}
ampli_1.ampli.commandRan(user.id, Object.assign(Object.assign(Object.assign({ name: Base.convertIdToName(this.id), version: constants_1.APP_SETTINGS.app.version }, runtime_1.getAmpliRuntimeProperties(this.getSettings().getRawRuntime())), { flags: this.setFlags }), this.branchMapping));
}
}
const promises = [Base.cleanupSentryClient()];
if (this.ampliLoaded) {
promises.push(ampli_1.ampli.flush().promise);
}
await Promise.all(promises);
if (this.tempUserDir) {
await fs.rimraf(this.tempUserDir);
}
Base.logLogFileDetails('Log file created.');
}
static async cleanupSentryClient() {
try {
const client = sentry_1.default.getCurrentHub().getClient();
if (client) {
await client.close(5000);
}
}
catch (error) {
if (constants_1.APP_SETTINGS.localDevelopment) {
stdout.println(chalk_1.default.red(`${chalk_1.default.bold(`Error`)}: Error closing sentry. ${error}`));
}
}
}
static convertIdToName(id) {
switch (id) {
case 'export': return 'Export';
case 'help': return 'Help';
case 'import': return 'Import';
case 'info': return 'Info';
case 'whoami': return 'WhoAmI';
case 'source': return 'Source';
case 'branch': return 'Branch';
case 'init': return 'Init';
case 'login': return 'Login';
case 'logout': return 'Logout';
case 'pull': return 'Pull';
case 'checkout': return 'Checkout';
case 'verify': return 'Verify';
case 'status': return 'Status';
case 'setup': return 'Setup';
default: return 'Unknown';
}
}
static getSanitizedFlags(flagDict) {
const sanitizedFlags = {};
const passthruFlags = ['runtime', 'update'];
for (const [key, value] of Object.entries(flagDict)) {
if (passthruFlags.includes(key)) {
sanitizedFlags[key] = value;
}
else {
sanitizedFlags[key] = (flagDict[key]) ? 'set' : 'empty';
}
}
return sanitizedFlags;
}
static logLogFileDetails(message) {
const logFilePath = debug_1.default.getLogFilePath();
if (logFilePath) {
stdout.println(`${message} ${chalk_1.default.yellow(logFilePath)}`);
}
}
async shouldBeInitialized(commonFlags, initDefault = false) {
const initialized = await new init_1.default(Object.assign(Object.assign({}, commonFlags), { org: undefined, workspace: undefined, user: undefined }), {}, this.actionConfig('init')).run(initDefault);
return initialized;
}
async mustBeInitialized(commonFlags, initDefault = false) {
const initialized = await this.shouldBeInitialized(commonFlags, initDefault);
if (!initialized) {
this.error(new errors_1.UserFixableError(errors_1.USER_ERROR_MESSAGES.folderNotInitialized), { exit: 1 });
}
}
async renameProjectConfigFile(legacyPath, currentPath, userConfigPath) {
if (await fs.exists(currentPath) || !(await fs.exists(legacyPath))) {
return;
}
const orgIdByWorkspaceId = {};
try {
const rawUserConfig = await fs.readTextFile(userConfigPath);
const userConfig = JSON.parse(rawUserConfig);
for (const [, value] of Object.entries(userConfig)) {
const user = value.User;
if (user) {
if (Array.isArray(user.orgs)) {
user.orgs.forEach((org) => {
if (org.id && Array.isArray(org.workspaces)) {
org.workspaces.forEach((workspace) => {
orgIdByWorkspaceId[workspace.id] = org.id;
});
}
});
}
}
}
}
catch (e) {
}
try {
const rawLegacyConfig = await fs.readTextFile(legacyPath);
const legacyConfig = json5.parse(rawLegacyConfig);
const newConfig = {};
if (legacyConfig.CompanyId && !legacyConfig.WorkspaceId) {
const workspaceId = legacyConfig.CompanyId;
const orgId = orgIdByWorkspaceId[workspaceId];
if (orgId) {
newConfig.OrgId = orgId;
}
newConfig.WorkspaceId = workspaceId;
}
['SourceId', 'Path', 'Branch', 'Version'].forEach(key => {
if (legacyConfig[key] !== undefined) {
newConfig[key] = legacyConfig[key];
}
});
await fs.writeFile(currentPath, JSON.stringify(newConfig, undefined, 2));
}
catch (e) {
this.println(chalk_1.default.magenta(`Can't convert ${this.getShortPath(legacyPath)} to ${this.getShortPath(currentPath)}`));
return;
}
try {
await fs.unlink(legacyPath);
}
catch (e) {
this.println(chalk_1.default.magenta(`Can't remove ${this.getShortPath(legacyPath)}`));
return;
}
this.println(chalk_1.default.magenta(`${legacyPath} was renamed to ${this.getShortPath(currentPath)}`));
}
async checkConfigIsValid(configPath, command, resolveConfigMergeConflicts) {
if (!(await fs.exists(configPath))) {
return;
}
let content = '';
try {
content = await fs.readTextFile(configPath);
settings_1.deserializeConfig(content);
}
catch (err) {
if (err instanceof SettingsLoadError_1.default && err.code === SettingsLoadError_1.SettingsLoadErrorCode.MERGE_CONFLICTS) {
if (await (resolveConfigMergeConflicts === null || resolveConfigMergeConflicts === void 0 ? void 0 : resolveConfigMergeConflicts(configPath, content))) {
return;
}
this.error(err.toUserFixableError(this.getShortPath(configPath), command), { exit: 1 });
return;
}
const { clearConfig } = await inquirer.prompt([{
name: 'clearConfig',
message: `${this.getShortPath(configPath)} is unreadable. Would you like to remove it?`,
type: 'confirm',
default: false,
}]);
if (clearConfig) {
await fs.unlink(configPath);
return;
}
this.error(errors_1.USER_ERROR_MESSAGES.fixAmpliConfigToContinue(this.getShortPath(configPath), command), { exit: 1 });
}
}
getShortPath(absPath) {
const relativePath = path.relative(this.projectDir, absPath);
return relativePath.indexOf('..') === 0 ? absPath : relativePath;
}
getCommandUser() {
var _a;
const settings = this.getSettings();
const user = (_a = this.user) !== null && _a !== void 0 ? _a : settings.projectUser();
if (user) {
return user;
}
const users = settings.users();
if (users.length > 0) {
return users[users.length - 1];
}
return null;
}
getSetFlags() {
var _a;
const commandFlags = Object.values((_a = this.constructor.flags) !== null && _a !== void 0 ? _a : {});
if (!commandFlags) {
return [];
}
const flagNames = [];
this.argv.forEach(arg => {
let flag;
if (arg.startsWith('--')) {
flag = commandFlags.find(f => f.name === arg.substr(2));
}
else if (arg.startsWith('-')) {
flag = commandFlags.find(f => f.char === arg.substr(1));
}
if (flag) {
flagNames.push(flag.name);
}
});
return flagNames;
}
setBranchMappedToMain(automatically) {
this.branchMapping = { branchMappedToMain: true, branchMappedAutomatically: automatically };
}
async resolveProjectMergeConflicts(configPath, content) {
const { ours, theirs } = mergeConflicts_1.resolveMergeConflicts(content);
let ourConfig;
let theirConfig;
try {
ourConfig = JSON.parse(ours);
}
catch (e) {
return false;
}
try {
theirConfig = JSON.parse(theirs);
}
catch (e) {
return false;
}
const valueRepresentation = (value) => (lodash_1.isObject(value) ? JSON.stringify(value) : value);
const ourKeys = Object.keys(ourConfig);
const theirKeys = Object.keys(ourConfig);
const ourDifferentKeys = ourKeys.filter(key => !theirKeys.includes(key) || valueRepresentation(ourConfig[key]) !== valueRepresentation(theirConfig[key]));
const theirDifferentKeys = theirKeys.filter(key => !ourKeys.includes(key) || valueRepresentation(theirConfig[key]) !== valueRepresentation(ourConfig[key]));
this.println(`${icons_1.ICON_ERROR_W_TEXT} Merge conflicts found in ${chalk_1.default.bold(path.basename(configPath))} configuration file.`);
this.println();
this.println(chalk_1.default.bold('Yours:'));
ourDifferentKeys.map(key => this.println(` ${key}: ${valueRepresentation(ourConfig[key])}`));
this.println();
this.println(chalk_1.default.bold('Theirs:'));
theirDifferentKeys.map(key => this.println(` ${key}: ${valueRepresentation(theirConfig[key])}`));
this.println();
const promptConfigs = [
{ name: 'yours (recommended)', value: 'ours' },
{ name: 'theirs', value: 'theirs' },
];
const { config: selectedConfig } = await inquirer.prompt([{
name: 'config',
message: `Select one to continue (or Ctl+C to cancel):`,
type: 'autocomplete',
source: (_, input) => promptConfigs.filter(c => string_1.matchPattern(c.name, input)),
default: 'ours',
}]);
const config = selectedConfig === 'theirs' ? theirConfig : ourConfig;
const differentKeys = selectedConfig === 'theirs' ? theirDifferentKeys : ourDifferentKeys;
this.println();
differentKeys.map(key => this.println(`${key}: ${valueRepresentation(config[key])}`));
this.println();
const newContent = JSON.stringify(config, null, 2);
await fs.writeFile(configPath, newContent);
this.pullIsRecommended = true;
return true;
}
async runRecommendedPull(pullFlags) {
if (!this.pullIsRecommended) {
return;
}
this.pullIsRecommended = false;
this.println(`${icons_1.ICON_WARNING_W_TEXT} We recommend running ${chalk_1.default.bold('ampli pull')} to update to the latest tracking library.`);
const { pull } = await inquirer.prompt([{
name: 'pull',
message: `Update tracking library now?`,
type: 'confirm',
default: true,
}]);
if (!pull) {
return;
}
await new pull_1.default(Object.assign({ branch: undefined, version: undefined, path: undefined, omitApiKeys: undefined, [configure_1.DEPRECATED_RUNTIMES]: undefined }, pullFlags), {
source: undefined,
}, this.actionConfig('pull'), () => { }).run();
}
async createTempUserConfigWithOrg(userDir, orgId) {
const { current: userConfigPath } = settings_1.userConfigPaths(userDir);
if (await fs.exists(userConfigPath)) {
const rawUserConfig = await fs.readTextFile(userConfigPath);
const userConfig = JSON.parse(rawUserConfig);
for (const [, value] of Object.entries(userConfig)) {
const user = value.User;
if (user) {
if (Array.isArray(user.orgs)) {
user.orgs.forEach((org) => {
org.id = orgId;
org.name = `Org ${orgId}`;
});
}
}
}
await fs.writeFile(path.join(this.tempUserDir, path.basename(userConfigPath)), JSON.stringify(userConfig, undefined, 2));
}
}
}
exports.default = Base;
Base.debug = debug_1.default.extend('command');
Base.commonFlags = {
debug: command_1.flags.boolean({
hidden: true,
description: '(developer) enable debug logging to file',
default: false,
}),
showProgress: command_1.flags.boolean({
hidden: true,
description: '(developer) show progress',
default: true,
allowNo: true,
}),
userDir: command_1.flags.string({
hidden: true,
description: 'user directory',
default: '',
}),
projectDir: command_1.flags.string({
hidden: true,
description: 'project directory',
default: '.',
}),
renameConfigs: command_1.flags.boolean({
hidden: true,
description: 'rename/converts configs to ampli.json',
default: true,
allowNo: true,
}),
zone: command_1.flags.string(Object.assign(Object.assign({}, exports.zoneFlagOptions), { default: '', hidden: true })),
};
Base.commonFlagsWithToken = Object.assign({ token: command_1.flags.string({
char: 't',
description: 'personal API token to authenticate with',
}), withOrg: command_1.flags.string({
description: 'admin access to organization',
hidden: true,
}) }, Base.commonFlags);