UNPKG

@amplitude/ampli

Version:

Amplitude CLI

533 lines (532 loc) 22.3 kB
"use strict"; 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);