Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add support for default account override #1349

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
26 changes: 25 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
};

Expand Down
3 changes: 3 additions & 0 deletions commands/__tests__/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
Expand Down Expand Up @@ -42,6 +44,7 @@ describe('commands/account', () => {
['info', info],
['remove', remove],
['clean', clean],
['createOverride', createOverride],
];

it('should demand the command takes one positional argument', () => {
Expand Down
2 changes: 2 additions & 0 deletions commands/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,6 +24,7 @@ exports.builder = yargs => {
.command(info)
.command(remove)
.command(clean)
.command(createOverride)
.demandCommand(1, '');

return yargs;
Expand Down
77 changes: 77 additions & 0 deletions commands/account/createOverride.ts
Original file line number Diff line number Diff line change
@@ -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<AccountCreateOverrideArgs>
): Promise<void> {
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 }));
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
process.exit(EXIT_CODES.SUCCESS);
} catch (e: unknown) {
kemmerle marked this conversation as resolved.
Show resolved Hide resolved
logError(e);
process.exit(EXIT_CODES.ERROR);
}
}

export function builder(yargs: Argv): Argv<AccountCreateOverrideArgs> {
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<AccountCreateOverrideArgs>;
}
5 changes: 5 additions & 0 deletions commands/account/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
getConfigPath,
getConfigDefaultAccount,
getConfigAccounts,
getDefaultAccountOverrideFilePath,
} = require('@hubspot/local-dev-lib/config');
const {
getAccountIdentifier,
Expand Down Expand Up @@ -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);
Expand All @@ -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(),
Expand Down
21 changes: 21 additions & 0 deletions lang/en.lyaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,34 @@ 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:
default: "Generate shell completion scripts for the zsh shell"
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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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}}"
Expand Down
9 changes: 9 additions & 0 deletions lib/doctor/Diagnosis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface DiagnosisCategories {
cli: DiagnosisCategory;
project: DiagnosisCategory;
cliConfig: DiagnosisCategory;
defaultAccountOverrideFile: DiagnosisCategory;
}

const i18nKey = `lib.doctor.diagnosis`;
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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)) {
Expand Down
7 changes: 6 additions & 1 deletion lib/doctor/DiagnosticInfoBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -110,6 +114,7 @@ export class DiagnosticInfoBuilder {
arch,
path: mainModule?.path,
config: getConfigPath(),
defaultAccountOverrideFile: getDefaultAccountOverrideFilePath(),
versions: {
[hubspotCli]: pkg.version,
node,
Expand Down
26 changes: 25 additions & 1 deletion lib/doctor/Doctor.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -72,6 +75,8 @@ export class Doctor {
...(this.projectConfig?.projectConfig ? this.performProjectChecks() : []),
]);

this.performDefaultAccountOverrideFileChecks();

SpinniesManager.succeed('runningDiagnostics', {
text: i18n(`${i18nKey}.diagnosticsComplete`),
succeedColor: 'white',
Expand Down Expand Up @@ -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(),
}),
});
}
}
Comment on lines +127 to +142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to do any checks on the validity of the account? Should these be warnings by default?


private async checkIfAccessTokenValid(): Promise<void> {
const localI18nKey = `${i18nKey}.accountChecks`;
try {
Expand Down
1 change: 1 addition & 0 deletions lib/doctor/__tests__/Diagnosis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
10 changes: 10 additions & 0 deletions lib/doctor/__tests__/DiagnosticInfoBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<typeof _getAccountId>;
const getProjectConfig = _getProjectConfig as jest.MockedFunction<
typeof _getProjectConfig
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -157,6 +164,9 @@ describe('lib/doctor/DiagnosticInfo', () => {
} as unknown as HubSpotPromise<Project>);
getAccessToken.mockResolvedValue(accessToken);
getConfigPath.mockReturnValue(configPath);
getDefaultAccountOverrideFilePath.mockReturnValue(
defaultAccountOverrideFile
);
utilPromisify.mockReturnValue(jest.fn().mockResolvedValue(npmVersion));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down