diff --git a/bin/cli.js b/bin/cli.js index b7b33fd24..3a9061e97 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -13,6 +13,10 @@ const { getConfigPath, validateConfig, } = require('@hubspot/local-dev-lib/config'); +const { + DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID, + DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND, +} = require('@hubspot/local-dev-lib/constants/config'); const { logError } = require('../lib/errorHandlers/index'); const { setLogLevel, getCommandName } = require('../lib/commonOpts'); const { validateAccount } = require('../lib/validation'); @@ -202,7 +206,27 @@ const injectAccountIdMiddleware = async options => { if (options.useEnv && process.env.HUBSPOT_ACCOUNT_ID) { options.derivedAccountId = parseInt(process.env.HUBSPOT_ACCOUNT_ID, 10); } else { - options.derivedAccountId = getAccountId(account); + try { + options.derivedAccountId = getAccountId(account); + } catch (error) { + logError(error); + if (error.cause === DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID) { + logger.log( + i18n(`${i18nKey}.injectAccountIdMiddleware.invalidAccountId`, { + overrideCommand: uiCommandReference('hs account create-override'), + }) + ); + } + if (error.cause === DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND) { + logger.log( + i18n(`${i18nKey}.injectAccountIdMiddleware.accountNotFound`, { + configPath: getConfigPath(), + authCommand: uiCommandReference('hs account auth'), + }) + ); + } + process.exit(EXIT_CODES.ERROR); + } } }; diff --git a/commands/__tests__/accounts.test.ts b/commands/__tests__/accounts.test.ts index 6983e7eef..354f7fa30 100644 --- a/commands/__tests__/accounts.test.ts +++ b/commands/__tests__/accounts.test.ts @@ -6,6 +6,7 @@ import use from '../account/use'; import * as info from '../account/info'; import remove from '../account/remove'; import clean from '../account/clean'; +import * as createOverride from '../account/createOverride'; jest.mock('yargs'); jest.mock('../account/list'); @@ -14,6 +15,7 @@ jest.mock('../account/use'); jest.mock('../account/info'); jest.mock('../account/remove'); jest.mock('../account/clean'); +jest.mock('../account/createOverride'); jest.mock('../../lib/commonOpts'); yargs.command.mockReturnValue(yargs); yargs.demandCommand.mockReturnValue(yargs); @@ -42,6 +44,7 @@ describe('commands/account', () => { ['info', info], ['remove', remove], ['clean', clean], + ['createOverride', createOverride], ]; it('should demand the command takes one positional argument', () => { diff --git a/commands/account.ts b/commands/account.ts index 42f9685ce..d29406aa4 100644 --- a/commands/account.ts +++ b/commands/account.ts @@ -7,6 +7,7 @@ const use = require('./account/use'); const info = require('./account/info'); const remove = require('./account/remove'); const clean = require('./account/clean'); +const createOverride = require('./account/createOverride'); const i18nKey = 'commands.account'; @@ -23,6 +24,7 @@ exports.builder = yargs => { .command(info) .command(remove) .command(clean) + .command(createOverride) .demandCommand(1, ''); return yargs; diff --git a/commands/account/createOverride.ts b/commands/account/createOverride.ts new file mode 100644 index 000000000..c5165e3cc --- /dev/null +++ b/commands/account/createOverride.ts @@ -0,0 +1,77 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { Argv, ArgumentsCamelCase } from 'yargs'; +import { getCwd } from '@hubspot/local-dev-lib/path'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME } from '@hubspot/local-dev-lib/constants/config'; +import { getConfigPath, getAccountId } from '@hubspot/local-dev-lib/config'; +import { addConfigOptions } from '../../lib/commonOpts'; +import { i18n } from '../../lib/lang'; +import { EXIT_CODES } from '../../lib/enums/exitCodes'; +import { selectAccountFromConfig } from '../../lib/prompts/accountsPrompt'; +import { logError } from '../../lib/errorHandlers/index'; +import { CommonArgs, ConfigArgs } from '../../types/Yargs'; + +const i18nKey = 'commands.account.subcommands.createOverride'; + +export const describe = null; // i18n(`${i18nKey}.describe`); + +export const command = 'create-override [account]'; + +type AccountCreateOverrideArgs = CommonArgs & + ConfigArgs & { + account: string | number; + }; + +export async function handler( + args: ArgumentsCamelCase +): Promise { + let overrideDefaultAccount = args.account; + + if (!overrideDefaultAccount) { + overrideDefaultAccount = await selectAccountFromConfig(); + } else if (!getAccountId(overrideDefaultAccount)) { + logger.error( + i18n(`${i18nKey}.errors.accountNotFound`, { + configPath: getConfigPath() || '', + }) + ); + overrideDefaultAccount = await selectAccountFromConfig(); + } + const accountId = getAccountId(overrideDefaultAccount); + + try { + const overrideFilePath = path.join( + getCwd(), + DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME + ); + await fs.writeFile(overrideFilePath, accountId!.toString(), 'utf8'); + logger.success(i18n(`${i18nKey}.success`, { overrideFilePath })); + process.exit(EXIT_CODES.SUCCESS); + } catch (e: unknown) { + logError(e); + process.exit(EXIT_CODES.ERROR); + } +} + +export function builder(yargs: Argv): Argv { + addConfigOptions(yargs); + + yargs.positional('account', { + describe: i18n(`${i18nKey}.options.account.describe`), + type: 'string', + }); + yargs.example([ + ['$0 account create-override', i18n(`${i18nKey}.examples.default`)], + [ + '$0 account create-override 12345678', + i18n(`${i18nKey}.examples.idBased`), + ], + [ + '$0 account create-override MyAccount', + i18n(`${i18nKey}.examples.nameBased`), + ], + ]); + + return yargs as Argv; +} diff --git a/commands/account/list.ts b/commands/account/list.ts index d2a6cd4ec..335a0619f 100644 --- a/commands/account/list.ts +++ b/commands/account/list.ts @@ -4,6 +4,7 @@ const { getConfigPath, getConfigDefaultAccount, getConfigAccounts, + getDefaultAccountOverrideFilePath, } = require('@hubspot/local-dev-lib/config'); const { getAccountIdentifier, @@ -85,6 +86,7 @@ exports.handler = async options => { trackCommandUsage('accounts-list', null, derivedAccountId); const configPath = getConfigPath(); + const overrideFilePath = getDefaultAccountOverrideFilePath(); const accountsList = getConfigAccounts(); const mappedPortalData = sortAndMapPortals(accountsList); const portalData = getPortalData(mappedPortalData); @@ -97,6 +99,9 @@ exports.handler = async options => { ); logger.log(i18n(`${i18nKey}.configPath`, { configPath })); + if (overrideFilePath) { + logger.log(i18n(`${i18nKey}.overrideFilePath`, { overrideFilePath })); + } logger.log( i18n(`${i18nKey}.defaultAccount`, { account: getConfigDefaultAccount(), diff --git a/lang/en.lyaml b/lang/en.lyaml index af8b01c28..323fc5fa1 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -11,6 +11,9 @@ en: portalEnvVarDeprecated: "The HUBSPOT_PORTAL_ID environment variable is deprecated. Please use HUBSPOT_ACCOUNT_ID instead." loadConfigMiddleware: configFileExists: "A configuration file already exists at {{ configPath }}. To specify a new configuration file, delete the existing one and try again." + injectAccountIdMiddleware: + invalidAccountId: "In the default override file (.hsaccount), the account ID must be a number. Please delete the current file and generate a new one using {{ overrideCommand }}." + accountNotFound: "The account in the default override file (.hsaccount) wasn't found in your configured accounts. You can authorize this account using {{ authCommand }}." completion: describe: "Enable bash completion shortcuts for commands. Concat the generated script to your .bashrc, .bash_profile, or .zshrc file." examples: @@ -18,11 +21,24 @@ en: account: describe: "Commands for managing configured accounts." subcommands: + createOverride: + describe: "Create a new default account override file (.hs-account) in the current working directory." + success: "Default account override file created at {{ overrideFilePath }}" + errors: + accountNotFound: "The specified account could not be found in the config file {{ configPath }}" + options: + account: + describe: "Name or ID of the account to create an override file for." + examples: + default: "Create a new default account override file (.hs-account) in the current working directory" + idBased: "Create a new default account override file (.hs-account) in the current working directory, using the account with accountId \"1234567\"" + nameBased: "Create a new default account override file (.hs-account) in the current working directory, using the account with name \"MyAccount\"" list: accounts: "{{#bold}}Accounts{{/bold}}:" defaultAccount: "{{#bold}}Default account{{/bold}}: {{ account }}" describe: "List names of accounts defined in config." configPath: "{{#bold}}Config path{{/bold}}: {{ configPath }}" + overrideFilePath: "{{#bold}}Default account override file path{{/bold}}: {{ overrideFilePath }}" labels: accountId: "Account ID" authType: "Auth Type" @@ -1511,6 +1527,9 @@ en: doctor: runningDiagnostics: "Running diagnostics..." diagnosticsComplete: "Diagnostics complete" + defaultAccountOverrideFileChecks: + overrideActive: "Default account override file active: {{ defaultAccountOverrideFile }}" + overrideAccountId: "Active account ID: {{ overrideAccountId }}" accountChecks: active: "Default account active" inactive: "Default account isn't active" @@ -1559,6 +1578,8 @@ en: defaultAccountSubHeader: "Default Account: {{accountDetails}}" noConfigFile: "CLI configuration not found" noConfigFileSecondary: "Run {{command}} and follow the prompts to create your CLI configuration file and connect it to your HubSpot account" + defaultAccountOverrideFile: + header: "Default account override file path:" projectConfig: header: "Project configuration" projectDirSubHeader: "Project dir: {{#bold}}{{ projectDir }}{{/bold}}" diff --git a/lib/doctor/Diagnosis.ts b/lib/doctor/Diagnosis.ts index 3196d8f6d..59bf2fbff 100644 --- a/lib/doctor/Diagnosis.ts +++ b/lib/doctor/Diagnosis.ts @@ -30,6 +30,7 @@ interface DiagnosisCategories { cli: DiagnosisCategory; project: DiagnosisCategory; cliConfig: DiagnosisCategory; + defaultAccountOverrideFile: DiagnosisCategory; } const i18nKey = `lib.doctor.diagnosis`; @@ -59,6 +60,10 @@ export class Diagnosis { header: i18n(`${i18nKey}.cliConfig.header`), sections: [], }, + defaultAccountOverrideFile: { + header: i18n(`${i18nKey}.defaultAccountOverrideFile.header`), + sections: [], + }, project: { header: i18n(`${i18nKey}.projectConfig.header`), subheaders: [ @@ -109,6 +114,10 @@ export class Diagnosis { this.diagnosis.cliConfig.sections.push(section); } + addDefaultAccountOverrideFileSection(section: Section): void { + this.diagnosis.defaultAccountOverrideFile.sections.push(section); + } + toString(): string { const output = []; for (const value of Object.values(this.diagnosis)) { diff --git a/lib/doctor/DiagnosticInfoBuilder.ts b/lib/doctor/DiagnosticInfoBuilder.ts index d63c05e89..06583a55f 100644 --- a/lib/doctor/DiagnosticInfoBuilder.ts +++ b/lib/doctor/DiagnosticInfoBuilder.ts @@ -10,7 +10,10 @@ import { AuthType, } from '@hubspot/local-dev-lib/types/Accounts'; import { Project } from '@hubspot/local-dev-lib/types/Project'; -import { getAccountId } from '@hubspot/local-dev-lib/config'; +import { + getAccountId, + getDefaultAccountOverrideFilePath, +} from '@hubspot/local-dev-lib/config'; import { getAccountConfig, getConfigPath } from '@hubspot/local-dev-lib/config'; import { getAccessToken } from '@hubspot/local-dev-lib/personalAccessKey'; import { walk } from '@hubspot/local-dev-lib/fs'; @@ -36,6 +39,7 @@ export interface DiagnosticInfo extends FilesInfo { path?: string; versions: { [hubspotCli]: string; node: string; npm: string | null }; config: string | null; + defaultAccountOverrideFile: string | null | undefined; project: { details?: Project; config?: ProjectConfig; @@ -110,6 +114,7 @@ export class DiagnosticInfoBuilder { arch, path: mainModule?.path, config: getConfigPath(), + defaultAccountOverrideFile: getDefaultAccountOverrideFilePath(), versions: { [hubspotCli]: pkg.version, node, diff --git a/lib/doctor/Doctor.ts b/lib/doctor/Doctor.ts index dbddff33f..7b7fa97bf 100644 --- a/lib/doctor/Doctor.ts +++ b/lib/doctor/Doctor.ts @@ -1,5 +1,8 @@ import { logger } from '@hubspot/local-dev-lib/logger'; -import { getAccountId } from '@hubspot/local-dev-lib/config'; +import { + getAccountId, + getCWDAccountOverride, +} from '@hubspot/local-dev-lib/config'; import SpinniesManager from '../ui/SpinniesManager'; import { @@ -72,6 +75,8 @@ export class Doctor { ...(this.projectConfig?.projectConfig ? this.performProjectChecks() : []), ]); + this.performDefaultAccountOverrideFileChecks(); + SpinniesManager.succeed('runningDiagnostics', { text: i18n(`${i18nKey}.diagnosticsComplete`), succeedColor: 'white', @@ -117,6 +122,25 @@ export class Doctor { return [this.checkIfAccessTokenValid()]; } + private performDefaultAccountOverrideFileChecks(): void { + const localI18nKey = `${i18nKey}.defaultAccountOverrideFileChecks`; + if (this.diagnosticInfo?.defaultAccountOverrideFile) { + this.diagnosis?.addDefaultAccountOverrideFileSection({ + type: 'warning', + message: i18n(`${localI18nKey}.overrideActive`, { + defaultAccountOverrideFile: + this.diagnosticInfo.defaultAccountOverrideFile, + }), + }); + this.diagnosis?.addDefaultAccountOverrideFileSection({ + type: 'warning', + message: i18n(`${localI18nKey}.overrideAccountId`, { + overrideAccountId: getCWDAccountOverride(), + }), + }); + } + } + private async checkIfAccessTokenValid(): Promise { const localI18nKey = `${i18nKey}.accountChecks`; try { diff --git a/lib/doctor/__tests__/Diagnosis.test.ts b/lib/doctor/__tests__/Diagnosis.test.ts index a9985c4a4..070416ffd 100644 --- a/lib/doctor/__tests__/Diagnosis.test.ts +++ b/lib/doctor/__tests__/Diagnosis.test.ts @@ -16,6 +16,7 @@ describe('lib/doctor/Diagnosis', () => { account: {}, arch: process.arch, config: 'path/to/config.json', + defaultAccountOverrideFile: 'path/to/default/account/override/.hsaccount', configFiles: [], envFiles: [], files: [], diff --git a/lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts b/lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts index ac741a807..f8bff0c26 100644 --- a/lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts +++ b/lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts @@ -19,6 +19,7 @@ import { getAccountId as _getAccountId, getAccountConfig as _getAccountConfig, getConfigPath as _getConfigPath, + getDefaultAccountOverrideFilePath as _getDefaultAccountOverrideFilePath, } from '@hubspot/local-dev-lib/config'; import { getAccessToken as _getAccessToken } from '@hubspot/local-dev-lib/personalAccessKey'; import { walk as _walk } from '@hubspot/local-dev-lib/fs'; @@ -38,6 +39,10 @@ const getAccountConfig = _getAccountConfig as jest.MockedFunction< const getConfigPath = _getConfigPath as jest.MockedFunction< typeof _getConfigPath >; +const getDefaultAccountOverrideFilePath = + _getDefaultAccountOverrideFilePath as jest.MockedFunction< + typeof _getDefaultAccountOverrideFilePath + >; const getAccountId = _getAccountId as jest.MockedFunction; const getProjectConfig = _getProjectConfig as jest.MockedFunction< typeof _getProjectConfig @@ -116,6 +121,8 @@ describe('lib/doctor/DiagnosticInfo', () => { const npmVersion = 'v8.17.0'; const configPath = '/path/to/config'; + const defaultAccountOverrideFile = + 'path/to/default/account/override/.hsaccount'; beforeEach(() => { builder = new DiagnosticInfoBuilder(processInfo); @@ -157,6 +164,9 @@ describe('lib/doctor/DiagnosticInfo', () => { } as unknown as HubSpotPromise); getAccessToken.mockResolvedValue(accessToken); getConfigPath.mockReturnValue(configPath); + getDefaultAccountOverrideFilePath.mockReturnValue( + defaultAccountOverrideFile + ); utilPromisify.mockReturnValue(jest.fn().mockResolvedValue(npmVersion)); }); diff --git a/lib/doctor/__tests__/__snapshots__/DiagnosticInfoBuilder.test.ts.snap b/lib/doctor/__tests__/__snapshots__/DiagnosticInfoBuilder.test.ts.snap index 4bef91f57..7f6937c89 100644 --- a/lib/doctor/__tests__/__snapshots__/DiagnosticInfoBuilder.test.ts.snap +++ b/lib/doctor/__tests__/__snapshots__/DiagnosticInfoBuilder.test.ts.snap @@ -18,6 +18,7 @@ exports[`lib/doctor/DiagnosticInfo generateDiagnosticInfo should gather the requ "src/app/public-app.json", "src/app/app.functions/serverless.json", ], + "defaultAccountOverrideFile": "path/to/default/account/override/.hsaccount", "envFiles": [ "src/app/app.functions/.env", ],