@hubspot/cli 7.9.0-beta.1 → 7.9.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/project/__tests__/deploy.test.js +4 -3
- package/lang/en.d.ts +10 -10
- package/lang/en.js +13 -13
- package/lib/__tests__/npm.test.js +1 -1
- package/lib/__tests__/sandboxSync.test.js +1 -1
- package/lib/__tests__/usageTracking.test.js +2 -2
- package/lib/configMigrate.js +3 -3
- package/lib/doctor/DiagnosticInfoBuilder.js +1 -1
- package/lib/doctor/Doctor.js +1 -1
- package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +4 -2
- package/lib/doctor/__tests__/Doctor.test.js +1 -1
- package/lib/jsonLoader.d.ts +14 -0
- package/lib/jsonLoader.js +60 -0
- package/lib/middleware/__test__/requestMiddleware.test.js +1 -1
- package/lib/middleware/autoUpdateMiddleware.js +1 -1
- package/lib/middleware/commandTargetingUtils.js +1 -0
- package/lib/middleware/fireAlarmMiddleware.js +1 -1
- package/lib/middleware/notificationsMiddleware.js +1 -1
- package/lib/middleware/requestMiddleware.js +1 -1
- package/lib/npm.js +1 -1
- package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -0
- package/lib/projects/create/__tests__/v2.test.js +20 -14
- package/lib/projects/create/v2.js +8 -13
- package/lib/projects/localDev/LocalDevLogger.js +2 -2
- package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
- package/lib/projects/localDev/LocalDevWebsocketServer.js +1 -1
- package/lib/prompts/promptUtils.d.ts +8 -0
- package/lib/prompts/promptUtils.js +7 -1
- package/lib/prompts/selectProjectTemplatePrompt.js +4 -0
- package/lib/sandboxSync.js +1 -1
- package/lib/usageTracking.js +2 -2
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +2 -2
- package/mcp-server/tools/cms/HsCreateModuleTool.js +2 -2
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +2 -2
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -9
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +7 -4
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +7 -3
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +7 -4
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +5 -1
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +8 -3
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +8 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +4 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -5
- package/mcp-server/tools/project/CreateProjectTool.js +2 -2
- package/mcp-server/tools/project/DeployProjectTool.d.ts +4 -1
- package/mcp-server/tools/project/DeployProjectTool.js +4 -3
- package/mcp-server/tools/project/DocFetchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocFetchTool.js +7 -6
- package/mcp-server/tools/project/DocsSearchTool.js +5 -5
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +4 -1
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +7 -5
- package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +8 -2
- package/mcp-server/tools/project/GetApplicationInfoTool.js +7 -6
- package/mcp-server/tools/project/GetConfigValuesTool.js +4 -4
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +4 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +6 -14
- package/mcp-server/tools/project/UploadProjectTools.d.ts +4 -1
- package/mcp-server/tools/project/UploadProjectTools.js +4 -3
- package/mcp-server/tools/project/ValidateProjectTool.d.ts +4 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +5 -4
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +6 -1
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +7 -3
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +7 -2
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +8 -3
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +6 -2
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +8 -3
- package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +9 -5
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +6 -2
- package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +43 -13
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +8 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +8 -2
- package/mcp-server/utils/__tests__/content.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/content.test.js +166 -0
- package/mcp-server/utils/__tests__/feedbackTracking.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/feedbackTracking.test.js +121 -0
- package/mcp-server/utils/content.d.ts +1 -1
- package/mcp-server/utils/content.js +8 -1
- package/mcp-server/utils/feedbackTracking.d.ts +1 -0
- package/mcp-server/utils/feedbackTracking.js +41 -0
- package/package.json +2 -2
- package/commands/project/__tests__/fixtures/exampleProject.json +0 -33
|
@@ -12,8 +12,9 @@ import * as projectNamePrompt from '../../../lib/prompts/projectNamePrompt.js';
|
|
|
12
12
|
import * as promptUtils from '../../../lib/prompts/promptUtils.js';
|
|
13
13
|
import { trackCommandUsage } from '../../../lib/usageTracking.js';
|
|
14
14
|
import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
|
|
15
|
-
import
|
|
15
|
+
import { loadJson } from '../../../lib/jsonLoader.js';
|
|
16
16
|
import { mockHubSpotHttpResponse, mockHubSpotHttpError, } from '../../../lib/testUtils.js';
|
|
17
|
+
const exampleProject = loadJson(import.meta.url, './fixtures/exampleProject.json');
|
|
17
18
|
import projectDeployCommand from '../deploy.js';
|
|
18
19
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
19
20
|
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
@@ -204,7 +205,7 @@ describe('commands/project/deploy', () => {
|
|
|
204
205
|
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
205
206
|
});
|
|
206
207
|
it('should log an error and exit when buildId option is not a valid build', async () => {
|
|
207
|
-
args.buildId = exampleProject.latestBuild
|
|
208
|
+
args.buildId = (exampleProject.latestBuild?.buildId ?? 0) + 1;
|
|
208
209
|
await projectDeployCommand.handler(args);
|
|
209
210
|
expect(uiLogger.error).toHaveBeenCalledTimes(1);
|
|
210
211
|
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(`Build ${args.buildId} does not exist for project`));
|
|
@@ -222,7 +223,7 @@ describe('commands/project/deploy', () => {
|
|
|
222
223
|
it('should prompt for build id if no option is provided', async () => {
|
|
223
224
|
delete args.buildId;
|
|
224
225
|
promptUserSpy.mockResolvedValue({
|
|
225
|
-
buildId: exampleProject.latestBuild
|
|
226
|
+
buildId: exampleProject.latestBuild?.buildId ?? 0,
|
|
226
227
|
});
|
|
227
228
|
await projectDeployCommand.handler(args);
|
|
228
229
|
expect(promptUserSpy).toHaveBeenCalledTimes(1);
|
package/lang/en.d.ts
CHANGED
|
@@ -1382,7 +1382,7 @@ Local development of unified apps is currently only compatible with accounts tha
|
|
|
1382
1382
|
${string}`;
|
|
1383
1383
|
};
|
|
1384
1384
|
readonly prompts: {
|
|
1385
|
-
readonly parentComponents: "[--project-base]
|
|
1385
|
+
readonly parentComponents: "[--project-base] Choose what to include in your project:";
|
|
1386
1386
|
readonly emptyProject: "Empty Project";
|
|
1387
1387
|
readonly app: "App";
|
|
1388
1388
|
};
|
|
@@ -1532,7 +1532,7 @@ ${string}`;
|
|
|
1532
1532
|
readonly maxExceeded: (maxCount: number) => string;
|
|
1533
1533
|
readonly authTypeNotAllowed: (authType: string) => string;
|
|
1534
1534
|
readonly distributionNotAllowed: (dist: string) => string;
|
|
1535
|
-
readonly portalDoesNotHaveAccessToThisFeature: (
|
|
1535
|
+
readonly portalDoesNotHaveAccessToThisFeature: () => string;
|
|
1536
1536
|
readonly locationInProject: "This command must be run from within a project directory.";
|
|
1537
1537
|
readonly failedToFetchComponentList: "Failed to fetch the list of available features. Please try again later.";
|
|
1538
1538
|
readonly projectContainsPublicApp: "This project contains a public app. This command is currently only compatible with projects that contain private apps.";
|
|
@@ -2771,7 +2771,7 @@ export declare const lib: {
|
|
|
2771
2771
|
readonly failedToInitialize: "Missing required arguments to initialize Local Dev";
|
|
2772
2772
|
readonly noDeployedBuild: (projectName: string, accountIdentifier: string, uploadCommand: string) => string;
|
|
2773
2773
|
readonly noComponents: "There are no components in this project.";
|
|
2774
|
-
readonly
|
|
2774
|
+
readonly headerMessage: "HubSpot projects local development";
|
|
2775
2775
|
readonly learnMoreLocalDevServer: string;
|
|
2776
2776
|
readonly running: (projectName: string, accountIdentifier: string) => string;
|
|
2777
2777
|
readonly quitHelper: `Press ${string} to stop the local dev server`;
|
|
@@ -2956,8 +2956,8 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
2956
2956
|
readonly prompt: {
|
|
2957
2957
|
readonly marketPlaceDistribution: "On the HubSpot marketplace";
|
|
2958
2958
|
readonly privateDistribution: "Privately";
|
|
2959
|
-
readonly distribution: "[--distribution]
|
|
2960
|
-
readonly auth: "[--auth]
|
|
2959
|
+
readonly distribution: "[--distribution] Choose how to distribute your application:";
|
|
2960
|
+
readonly auth: "[--auth] Choose your authentication type:";
|
|
2961
2961
|
readonly staticAuth: "Static Auth";
|
|
2962
2962
|
readonly oauth: "OAuth";
|
|
2963
2963
|
};
|
|
@@ -3364,8 +3364,8 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
3364
3364
|
};
|
|
3365
3365
|
};
|
|
3366
3366
|
readonly projectNameAndDestPrompt: {
|
|
3367
|
-
readonly enterName: "[--name]
|
|
3368
|
-
readonly enterDest: "[--dest]
|
|
3367
|
+
readonly enterName: "[--name] Enter your project name:";
|
|
3368
|
+
readonly enterDest: "[--dest] Choose where to create the project:";
|
|
3369
3369
|
readonly errors: {
|
|
3370
3370
|
readonly nameRequired: "A project name is required";
|
|
3371
3371
|
readonly destRequired: "A project dest is required";
|
|
@@ -3375,7 +3375,7 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
3375
3375
|
};
|
|
3376
3376
|
readonly selectProjectTemplatePrompt: {
|
|
3377
3377
|
readonly selectTemplate: "[--template] Choose a project template: ";
|
|
3378
|
-
readonly features: "[--features]
|
|
3378
|
+
readonly features: "[--features] Choose which features to add:";
|
|
3379
3379
|
readonly errors: {
|
|
3380
3380
|
readonly invalidTemplate: (template: string) => string;
|
|
3381
3381
|
readonly projectTemplateRequired: "Project template is required when projectTemplates is provided";
|
|
@@ -3409,8 +3409,8 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
3409
3409
|
};
|
|
3410
3410
|
};
|
|
3411
3411
|
readonly projectAddPrompt: {
|
|
3412
|
-
readonly selectType: "[--type]
|
|
3413
|
-
readonly selectFeatures: "[--features]
|
|
3412
|
+
readonly selectType: "[--type] Choose which features to add: ";
|
|
3413
|
+
readonly selectFeatures: "[--features] Choose which features to add: ";
|
|
3414
3414
|
readonly enterName: "[--name] Give your component a name: ";
|
|
3415
3415
|
readonly errors: {
|
|
3416
3416
|
readonly nameRequired: "A component name is required";
|
package/lang/en.js
CHANGED
|
@@ -1382,7 +1382,7 @@ export const commands = {
|
|
|
1382
1382
|
welcomeMessage: `\n${chalk.bold('Welcome to HubSpot Developer Projects!')}`,
|
|
1383
1383
|
},
|
|
1384
1384
|
prompts: {
|
|
1385
|
-
parentComponents: '[--project-base]
|
|
1385
|
+
parentComponents: '[--project-base] Choose what to include in your project:',
|
|
1386
1386
|
emptyProject: 'Empty Project',
|
|
1387
1387
|
app: 'App',
|
|
1388
1388
|
},
|
|
@@ -1526,9 +1526,9 @@ export const commands = {
|
|
|
1526
1526
|
failedToDownloadComponent: 'Failed to download project. Please try again later.',
|
|
1527
1527
|
invalidComponentType: (componentType) => `'${componentType}' is not a valid project component type.`,
|
|
1528
1528
|
maxExceeded: (maxCount) => `This project has the maximum allowed(${maxCount})`,
|
|
1529
|
-
authTypeNotAllowed: (authType) => `
|
|
1530
|
-
distributionNotAllowed: (dist) => `
|
|
1531
|
-
portalDoesNotHaveAccessToThisFeature: (
|
|
1529
|
+
authTypeNotAllowed: (authType) => `Requires auth type '${authType}'.`,
|
|
1530
|
+
distributionNotAllowed: (dist) => `Requires distribution '${dist}'.`,
|
|
1531
|
+
portalDoesNotHaveAccessToThisFeature: () => "This account doesn't have access to this feature.",
|
|
1532
1532
|
locationInProject: 'This command must be run from within a project directory.',
|
|
1533
1533
|
failedToFetchComponentList: 'Failed to fetch the list of available features. Please try again later.',
|
|
1534
1534
|
projectContainsPublicApp: 'This project contains a public app. This command is currently only compatible with projects that contain private apps.',
|
|
@@ -2765,8 +2765,8 @@ export const lib = {
|
|
|
2765
2765
|
failedToInitialize: 'Missing required arguments to initialize Local Dev',
|
|
2766
2766
|
noDeployedBuild: (projectName, accountIdentifier, uploadCommand) => `Your project ${chalk.bold(projectName)} exists in ${accountIdentifier}, but has no deployed build. Projects must be successfully deployed to be developed locally. Address any build and deploy errors your project may have, then run ${uploadCommand} to upload and deploy your project.`,
|
|
2767
2767
|
noComponents: 'There are no components in this project.',
|
|
2768
|
-
|
|
2769
|
-
learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/
|
|
2768
|
+
headerMessage: 'HubSpot projects local development',
|
|
2769
|
+
learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands'),
|
|
2770
2770
|
running: (projectName, accountIdentifier) => chalk.hex(UI_COLORS.SORBET)(`Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`),
|
|
2771
2771
|
quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`,
|
|
2772
2772
|
viewProjectLink: (name, accountId) => uiLink('View project in HubSpot', getProjectDetailUrl(name, accountId) || ''),
|
|
@@ -2949,8 +2949,8 @@ export const lib = {
|
|
|
2949
2949
|
prompt: {
|
|
2950
2950
|
marketPlaceDistribution: 'On the HubSpot marketplace',
|
|
2951
2951
|
privateDistribution: 'Privately',
|
|
2952
|
-
distribution: '[--distribution]
|
|
2953
|
-
auth: '[--auth]
|
|
2952
|
+
distribution: '[--distribution] Choose how to distribute your application:',
|
|
2953
|
+
auth: '[--auth] Choose your authentication type:',
|
|
2954
2954
|
staticAuth: 'Static Auth',
|
|
2955
2955
|
oauth: 'OAuth',
|
|
2956
2956
|
},
|
|
@@ -3355,8 +3355,8 @@ export const lib = {
|
|
|
3355
3355
|
},
|
|
3356
3356
|
},
|
|
3357
3357
|
projectNameAndDestPrompt: {
|
|
3358
|
-
enterName: '[--name]
|
|
3359
|
-
enterDest: '[--dest]
|
|
3358
|
+
enterName: '[--name] Enter your project name:',
|
|
3359
|
+
enterDest: '[--dest] Choose where to create the project:',
|
|
3360
3360
|
errors: {
|
|
3361
3361
|
nameRequired: 'A project name is required',
|
|
3362
3362
|
destRequired: 'A project dest is required',
|
|
@@ -3366,7 +3366,7 @@ export const lib = {
|
|
|
3366
3366
|
},
|
|
3367
3367
|
selectProjectTemplatePrompt: {
|
|
3368
3368
|
selectTemplate: '[--template] Choose a project template: ',
|
|
3369
|
-
features: '[--features]
|
|
3369
|
+
features: '[--features] Choose which features to add:',
|
|
3370
3370
|
errors: {
|
|
3371
3371
|
invalidTemplate: (template) => `[--template] Could not find template "${template}". Please choose an available template:`,
|
|
3372
3372
|
projectTemplateRequired: 'Project template is required when projectTemplates is provided',
|
|
@@ -3400,8 +3400,8 @@ export const lib = {
|
|
|
3400
3400
|
},
|
|
3401
3401
|
},
|
|
3402
3402
|
projectAddPrompt: {
|
|
3403
|
-
selectType: '[--type]
|
|
3404
|
-
selectFeatures: '[--features]
|
|
3403
|
+
selectType: '[--type] Choose which features to add: ',
|
|
3404
|
+
selectFeatures: '[--features] Choose which features to add: ',
|
|
3405
3405
|
enterName: '[--name] Give your component a name: ',
|
|
3406
3406
|
errors: {
|
|
3407
3407
|
nameRequired: 'A component name is required',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import util from 'util';
|
|
2
2
|
import { isGloballyInstalled, getLatestCliVersion, DEFAULT_PACKAGE_MANAGER, } from '../npm.js';
|
|
3
|
-
import pkg from '
|
|
3
|
+
import { pkg } from '../jsonLoader.js';
|
|
4
4
|
vi.mock('../../ui/logger.js');
|
|
5
5
|
vi.mock('../ui/SpinniesManager');
|
|
6
6
|
describe('lib/npm', () => {
|
|
@@ -59,7 +59,7 @@ describe('lib/sandboxSync', () => {
|
|
|
59
59
|
it('throws error when account IDs are missing', async () => {
|
|
60
60
|
mockedGetAccountId.mockReset();
|
|
61
61
|
mockedGetAccountId.mockReturnValue(null);
|
|
62
|
-
const errorRegex = new RegExp(`
|
|
62
|
+
const errorRegex = new RegExp(`because your account has been removed from`);
|
|
63
63
|
await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toThrow(errorRegex);
|
|
64
64
|
});
|
|
65
65
|
it('handles sync in progress error', async () => {
|
|
@@ -3,8 +3,8 @@ import { isTrackingAllowed, getAccountConfig, } from '@hubspot/local-dev-lib/con
|
|
|
3
3
|
import { API_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
|
|
4
4
|
import { uiLogger } from '../ui/logger.js';
|
|
5
5
|
import { trackCommandUsage, trackHelpUsage, trackConvertFieldsUsage, trackAuthAction, trackCommandMetadataUsage, } from '../usageTracking.js';
|
|
6
|
-
import
|
|
7
|
-
const version =
|
|
6
|
+
import { pkg } from '../jsonLoader.js';
|
|
7
|
+
const version = pkg.version;
|
|
8
8
|
vi.mock('@hubspot/local-dev-lib/trackUsage');
|
|
9
9
|
vi.mock('@hubspot/local-dev-lib/config');
|
|
10
10
|
vi.mock('../ui/logger.js');
|
package/lib/configMigrate.js
CHANGED
|
@@ -24,7 +24,7 @@ async function promptNewAccountName(account, globalConfig, renamedAccounts) {
|
|
|
24
24
|
if (value === account.name) {
|
|
25
25
|
return lib.configMigrate.handleAccountNameConflicts.errors.sameName;
|
|
26
26
|
}
|
|
27
|
-
const existingAccount = globalConfig.accounts
|
|
27
|
+
const existingAccount = globalConfig.accounts?.some(acc => acc.name === value);
|
|
28
28
|
const renamedAccount = renamedAccounts.some(acc => acc.name === value);
|
|
29
29
|
if (existingAccount || renamedAccount) {
|
|
30
30
|
return lib.configMigrate.handleAccountNameConflicts.errors.nameAlreadyInConfig(value);
|
|
@@ -85,8 +85,8 @@ async function handleAccountNameConflicts(globalConfig, deprecatedConfig, force)
|
|
|
85
85
|
const accountsWithConflictsToRemove = new Set();
|
|
86
86
|
const renamedAccounts = [];
|
|
87
87
|
const accountsNotYetInGlobal = deprecatedConfig.portals.filter(portal => portal.portalId &&
|
|
88
|
-
!globalConfig.accounts
|
|
89
|
-
const accountsWithConflicts = accountsNotYetInGlobal.filter(localAccount => globalConfig.accounts
|
|
88
|
+
!globalConfig.accounts?.some(acc => acc.accountId === portal.portalId));
|
|
89
|
+
const accountsWithConflicts = accountsNotYetInGlobal.filter(localAccount => globalConfig.accounts?.some(globalAccount => globalAccount.name === localAccount.name));
|
|
90
90
|
if (accountsWithConflicts.length > 0) {
|
|
91
91
|
uiLogger.log('');
|
|
92
92
|
uiLogger.warn(lib.configMigrate.handleAccountNameConflicts.warnings.accountNameConflictMessage(accountsWithConflicts.length));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getProjectConfig } from '../projects/config.js';
|
|
2
2
|
import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import pkg from '
|
|
4
|
+
import { pkg } from '../jsonLoader.js';
|
|
5
5
|
import { uiLogger } from '../ui/logger.js';
|
|
6
6
|
import { getAccountId, getDefaultAccountOverrideFilePath, isConfigFlagEnabled, } from '@hubspot/local-dev-lib/config';
|
|
7
7
|
import { getAccountConfig, getConfigPath } from '@hubspot/local-dev-lib/config';
|
package/lib/doctor/Doctor.js
CHANGED
|
@@ -13,7 +13,7 @@ import { PORT_MANAGER_SERVER_PORT } from '@hubspot/local-dev-lib/constants/ports
|
|
|
13
13
|
import { accessTokenForPersonalAccessKey, authorizedScopesForPortalAndUser, scopesOnAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
14
14
|
import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
|
|
15
15
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
16
|
-
import pkg from '
|
|
16
|
+
import { pkg } from '../jsonLoader.js';
|
|
17
17
|
import { lib } from '../../lang/en.js';
|
|
18
18
|
import { uiLink } from '../ui/index.js';
|
|
19
19
|
const minMajorNodeVersion = 18;
|
|
@@ -6,11 +6,13 @@ vi.mock('@hubspot/local-dev-lib/personalAccessKey');
|
|
|
6
6
|
vi.mock('../../projects/config');
|
|
7
7
|
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
8
8
|
vi.mock('util');
|
|
9
|
-
vi.mock('
|
|
9
|
+
vi.mock('../../jsonLoader.js', () => {
|
|
10
10
|
return {
|
|
11
|
-
|
|
11
|
+
pkg: {
|
|
12
|
+
name: '@hubspot/cli',
|
|
12
13
|
version: '1.0.0',
|
|
13
14
|
},
|
|
15
|
+
loadJson: vi.fn(),
|
|
14
16
|
};
|
|
15
17
|
});
|
|
16
18
|
import { DiagnosticInfoBuilder, } from '../DiagnosticInfoBuilder.js';
|
|
@@ -359,7 +359,7 @@ describe('lib/doctor/Doctor', () => {
|
|
|
359
359
|
expect(doctor.diagnosis?.addProjectSection).toHaveBeenCalledWith({
|
|
360
360
|
type: 'warning',
|
|
361
361
|
message: 'Port 8080 is in use',
|
|
362
|
-
secondaryMessaging: expect.stringMatching(/Make sure it is available before running
|
|
362
|
+
secondaryMessaging: expect.stringMatching(/Make sure it is available before running/),
|
|
363
363
|
});
|
|
364
364
|
});
|
|
365
365
|
it('should add success section if port is available', async () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic JSON loader that works in both test (lib/) and production (dist/lib/) environments.
|
|
3
|
+
* Automatically resolves paths relative to the caller's location.
|
|
4
|
+
*
|
|
5
|
+
* @param importMetaUrl - Pass import.meta.url from the calling file
|
|
6
|
+
* @param relativePath - Path to JSON file relative to the caller (e.g., '../package.json', './fixtures/data.json')
|
|
7
|
+
* @returns The loaded JSON object
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadJson<T = unknown>(importMetaUrl: string, relativePath: string): T;
|
|
10
|
+
export declare const pkg: {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// NOTE: Can be switched back to standard import with min node version 23
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
/**
|
|
7
|
+
* Generic JSON loader that works in both test (lib/) and production (dist/lib/) environments.
|
|
8
|
+
* Automatically resolves paths relative to the caller's location.
|
|
9
|
+
*
|
|
10
|
+
* @param importMetaUrl - Pass import.meta.url from the calling file
|
|
11
|
+
* @param relativePath - Path to JSON file relative to the caller (e.g., '../package.json', './fixtures/data.json')
|
|
12
|
+
* @returns The loaded JSON object
|
|
13
|
+
*/
|
|
14
|
+
export function loadJson(importMetaUrl, relativePath) {
|
|
15
|
+
const callerDir = path.dirname(fileURLToPath(importMetaUrl));
|
|
16
|
+
const resolvedPath = path.resolve(callerDir, relativePath);
|
|
17
|
+
// If the resolved path exists, use it directly
|
|
18
|
+
if (existsSync(resolvedPath)) {
|
|
19
|
+
return createRequire(importMetaUrl)(resolvedPath);
|
|
20
|
+
}
|
|
21
|
+
// If not found, try adjusting for dist/ directory
|
|
22
|
+
// This handles the case where we're in dist/lib/ but the JSON is at project root
|
|
23
|
+
const pathParts = resolvedPath.split(path.sep);
|
|
24
|
+
const distIndex = pathParts.indexOf('dist');
|
|
25
|
+
if (distIndex !== -1) {
|
|
26
|
+
// Remove 'dist' from the path and try again
|
|
27
|
+
pathParts.splice(distIndex, 1);
|
|
28
|
+
const adjustedPath = pathParts.join(path.sep);
|
|
29
|
+
if (existsSync(adjustedPath)) {
|
|
30
|
+
return createRequire(importMetaUrl)(adjustedPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`JSON file not found: ${relativePath} (resolved to ${resolvedPath})`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Helper to find package.json by walking up the directory tree.
|
|
37
|
+
* Works regardless of whether we're in lib/ or dist/lib/.
|
|
38
|
+
*/
|
|
39
|
+
function findPackageJsonPath(startDir) {
|
|
40
|
+
let currentDir = startDir;
|
|
41
|
+
while (true) {
|
|
42
|
+
const pkgPath = path.join(currentDir, 'package.json');
|
|
43
|
+
if (existsSync(pkgPath)) {
|
|
44
|
+
return pkgPath;
|
|
45
|
+
}
|
|
46
|
+
const parentDir = path.dirname(currentDir);
|
|
47
|
+
if (parentDir === currentDir) {
|
|
48
|
+
// Reached root without finding package.json (e.g., in test environments)
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
currentDir = parentDir;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Load package.json once when this module is imported for convenience
|
|
55
|
+
// In test environments where this can't be found, tests should mock this module
|
|
56
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const pkgPath = findPackageJsonPath(__dirname);
|
|
58
|
+
export const pkg = pkgPath
|
|
59
|
+
? createRequire(import.meta.url)(pkgPath)
|
|
60
|
+
: { name: 'unknown', version: 'unknown' };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addUserAgentHeader } from '@hubspot/local-dev-lib/http';
|
|
2
2
|
import { setRequestHeaders } from '../requestMiddleware.js';
|
|
3
|
-
import pkg from '
|
|
3
|
+
import { pkg } from '../../jsonLoader.js';
|
|
4
4
|
vi.mock('@hubspot/local-dev-lib/http', () => ({
|
|
5
5
|
addUserAgentHeader: vi.fn(),
|
|
6
6
|
}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import updateNotifier from 'update-notifier';
|
|
2
2
|
import { isConfigFlagEnabled } from '@hubspot/local-dev-lib/config';
|
|
3
|
-
import pkg from '
|
|
3
|
+
import { pkg } from '../jsonLoader.js';
|
|
4
4
|
import { UI_COLORS } from '../ui/index.js';
|
|
5
5
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
6
6
|
import { lib } from '../../lang/en.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
|
|
3
3
|
import { debugError } from '../errorHandlers/index.js';
|
|
4
|
-
import pkg from '
|
|
4
|
+
import { pkg } from '../jsonLoader.js';
|
|
5
5
|
import { logInBox } from '../ui/boxen.js';
|
|
6
6
|
import { renderInline } from '../../ui/index.js';
|
|
7
7
|
import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import updateNotifier from 'update-notifier';
|
|
2
|
-
import pkg from '
|
|
2
|
+
import { pkg } from '../jsonLoader.js';
|
|
3
3
|
import { UI_COLORS } from '../ui/index.js';
|
|
4
4
|
import { lib } from '../../lang/en.js';
|
|
5
5
|
const notifier = updateNotifier({
|
package/lib/npm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec as execAsync } from 'node:child_process';
|
|
2
2
|
import util from 'util';
|
|
3
3
|
import { uiLogger } from './ui/logger.js';
|
|
4
|
-
import pkg from '
|
|
4
|
+
import { pkg } from './jsonLoader.js';
|
|
5
5
|
export const DEFAULT_PACKAGE_MANAGER = 'npm';
|
|
6
6
|
export async function isGloballyInstalled(packageName) {
|
|
7
7
|
const exec = util.promisify(execAsync);
|
|
@@ -21,6 +21,7 @@ import { confirmPrompt } from '../../prompts/promptUtils.js';
|
|
|
21
21
|
import { getOauthAppInstallUrl, getStaticAuthAppInstallUrl, } from '../../app/urls.js';
|
|
22
22
|
import { isDeveloperTestAccount, isSandbox } from '../../accountTypes.js';
|
|
23
23
|
import { logError } from '../../errorHandlers/index.js';
|
|
24
|
+
import { isServerRunningAtUrl } from '../../http.js';
|
|
24
25
|
import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, APP_INSTALLATION_STATES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
|
|
25
26
|
import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
|
|
26
27
|
vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
|
|
@@ -36,6 +37,7 @@ vi.mock('../../errorHandlers/index');
|
|
|
36
37
|
vi.mock('../localDev/LocalDevState');
|
|
37
38
|
vi.mock('../localDev/LocalDevLogger');
|
|
38
39
|
vi.mock('../../ui/SpinniesManager');
|
|
40
|
+
vi.mock('../../http');
|
|
39
41
|
describe('AppDevModeInterface', () => {
|
|
40
42
|
let appDevModeInterface;
|
|
41
43
|
let mockLocalDevState;
|
|
@@ -139,6 +141,7 @@ describe('AppDevModeInterface', () => {
|
|
|
139
141
|
});
|
|
140
142
|
confirmPrompt.mockResolvedValue(true);
|
|
141
143
|
installStaticAuthAppOnTestAccount.mockResolvedValue(undefined);
|
|
144
|
+
isServerRunningAtUrl.mockResolvedValue(true);
|
|
142
145
|
// Mock process.exit
|
|
143
146
|
vi.spyOn(global.process, 'exit').mockImplementation((code) => {
|
|
144
147
|
throw new Error(`Process.exit called with code ${code}`);
|
|
@@ -37,12 +37,12 @@ describe('lib/projects/create/v2', () => {
|
|
|
37
37
|
};
|
|
38
38
|
it('returns enabled components when they meet all requirements', async () => {
|
|
39
39
|
const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', 123, mockProjectMetadataForChoices);
|
|
40
|
-
expect(choices).toHaveLength(
|
|
40
|
+
expect(choices).toHaveLength(2);
|
|
41
41
|
expect(choices[0]).toEqual({
|
|
42
42
|
name: 'Module Component [module]',
|
|
43
43
|
value: mockComponents[0],
|
|
44
44
|
});
|
|
45
|
-
expect(choices[
|
|
45
|
+
expect(choices[1]).toEqual({
|
|
46
46
|
name: expect.stringContaining('Card Component'),
|
|
47
47
|
value: mockComponents[1],
|
|
48
48
|
disabled: expect.stringContaining('maximum'),
|
|
@@ -50,20 +50,20 @@ describe('lib/projects/create/v2', () => {
|
|
|
50
50
|
});
|
|
51
51
|
it('disables components when auth type is not supported', async () => {
|
|
52
52
|
const choices = await calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', 123, mockProjectMetadataForChoices);
|
|
53
|
-
// All components should be disabled
|
|
54
|
-
expect(choices[
|
|
53
|
+
// All components should be disabled
|
|
54
|
+
expect(choices[0]).toEqual({
|
|
55
55
|
name: expect.stringContaining('Module Component'),
|
|
56
56
|
value: mockComponents[0],
|
|
57
|
-
disabled: expect.stringContaining('
|
|
57
|
+
disabled: expect.stringContaining('oauth'),
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
it('disables components when distribution is not supported', async () => {
|
|
61
61
|
const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', 123, mockProjectMetadataForChoices);
|
|
62
|
-
// All components should be disabled
|
|
63
|
-
expect(choices[
|
|
62
|
+
// All components should be disabled
|
|
63
|
+
expect(choices[0]).toEqual({
|
|
64
64
|
name: expect.stringContaining('Module Component'),
|
|
65
65
|
value: mockComponents[0],
|
|
66
|
-
disabled: expect.stringContaining('
|
|
66
|
+
disabled: expect.stringContaining('private'),
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
it('handles components without auth type or distribution restrictions', async () => {
|
|
@@ -125,8 +125,8 @@ describe('lib/projects/create/v2', () => {
|
|
|
125
125
|
},
|
|
126
126
|
};
|
|
127
127
|
const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, projectMetadataAtMaxWorkflowAction);
|
|
128
|
-
expect(choices).toHaveLength(
|
|
129
|
-
expect(choices[
|
|
128
|
+
expect(choices).toHaveLength(1);
|
|
129
|
+
expect(choices[0]).toEqual({
|
|
130
130
|
name: expect.stringContaining('Workflow Action Tool'),
|
|
131
131
|
value: componentWithCliSelector[0],
|
|
132
132
|
disabled: expect.stringContaining('maximum'),
|
|
@@ -183,12 +183,18 @@ describe('lib/projects/create/v2', () => {
|
|
|
183
183
|
supportedDistributions: ['private'],
|
|
184
184
|
},
|
|
185
185
|
];
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const projectMetadataWithWorkflowAction = {
|
|
187
|
+
hsMetaFiles: [],
|
|
188
|
+
components: {
|
|
189
|
+
'workflow-action': { count: 0, maxCount: 3, hsMetaFiles: [] },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, projectMetadataWithWorkflowAction);
|
|
193
|
+
expect(choices).toHaveLength(1);
|
|
194
|
+
expect(choices[0]).toEqual({
|
|
189
195
|
name: expect.stringContaining('Workflow Action Tool'),
|
|
190
196
|
value: gatedComponent[0],
|
|
191
|
-
disabled: expect.stringContaining('
|
|
197
|
+
disabled: expect.stringContaining("doesn't have access to this feature"),
|
|
192
198
|
});
|
|
193
199
|
expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
|
|
194
200
|
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { Separator } from '@inquirer/prompts';
|
|
2
1
|
import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_PROJECT, PROJECT_WITH_APP, FEATURES, } from '../../constants.js';
|
|
3
2
|
import { commands, lib } from '../../../lang/en.js';
|
|
4
3
|
import { listPrompt } from '../../prompts/promptUtils.js';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
4
|
import { isV2Project } from '../platformVersion.js';
|
|
7
5
|
import path from 'path';
|
|
8
6
|
import { getConfigForPlatformVersion } from './legacy.js';
|
|
@@ -75,25 +73,27 @@ export async function calculateComponentTemplateChoices(components, authType, di
|
|
|
75
73
|
if (Array.isArray(supportedAuthTypes) &&
|
|
76
74
|
authType &&
|
|
77
75
|
!supportedAuthTypes.includes(authType.toLowerCase())) {
|
|
78
|
-
|
|
76
|
+
const supportedAuthTypesString = supportedAuthTypes.join(', ');
|
|
77
|
+
disabledReasons.push(commands.project.add.error.authTypeNotAllowed(supportedAuthTypesString));
|
|
79
78
|
}
|
|
80
79
|
if (Array.isArray(supportedDistributions) &&
|
|
81
80
|
distribution &&
|
|
82
81
|
!supportedDistributions.includes(distribution.toLowerCase())) {
|
|
83
|
-
|
|
82
|
+
const supportedDistributionsString = supportedDistributions.join(', ');
|
|
83
|
+
disabledReasons.push(commands.project.add.error.distributionNotAllowed(supportedDistributionsString));
|
|
84
84
|
}
|
|
85
85
|
const templateGate = componentTypeToGateMap[template.cliSelector || template.type];
|
|
86
86
|
if (templateGate) {
|
|
87
87
|
const isUngated = await hasFeature(accountId, templateGate);
|
|
88
88
|
if (!isUngated) {
|
|
89
|
-
disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature(
|
|
89
|
+
disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature());
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
if (disabledReasons.length > 0) {
|
|
93
93
|
disabledComponents.push({
|
|
94
|
-
name:
|
|
94
|
+
name: `${template.label} [${template.cliSelector || template.type}]`,
|
|
95
95
|
value: template,
|
|
96
|
-
disabled: disabledReasons.join(' ')
|
|
96
|
+
disabled: `– ${disabledReasons.join(' ')}`,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
@@ -104,12 +104,7 @@ export async function calculateComponentTemplateChoices(components, authType, di
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
return disabledComponents.length
|
|
107
|
-
? [
|
|
108
|
-
...enabledComponents,
|
|
109
|
-
new Separator(),
|
|
110
|
-
...disabledComponents,
|
|
111
|
-
new Separator(),
|
|
112
|
-
]
|
|
107
|
+
? [...enabledComponents, ...disabledComponents]
|
|
113
108
|
: [...enabledComponents];
|
|
114
109
|
}
|
|
115
110
|
export async function v2ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, accountId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAccountId, hasLocalStateFlag } from '@hubspot/local-dev-lib/config';
|
|
2
2
|
import { getConfigDefaultAccount } from '@hubspot/local-dev-lib/config';
|
|
3
3
|
import { uiLogger } from '../../ui/logger.js';
|
|
4
|
-
import {
|
|
4
|
+
import { uiLine, uiAccountDescription, uiCommandReference, } from '../../ui/index.js';
|
|
5
5
|
import { lib } from '../../../lang/en.js';
|
|
6
6
|
import SpinniesManager from '../../ui/SpinniesManager.js';
|
|
7
7
|
import { logError } from '../../errorHandlers/index.js';
|
|
@@ -75,7 +75,7 @@ class LocalDevLogger {
|
|
|
75
75
|
if (!this.state.debug) {
|
|
76
76
|
console.clear();
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
uiLogger.log(lib.LocalDevManager.headerMessage);
|
|
79
79
|
uiLogger.log(lib.LocalDevManager.learnMoreLocalDevServer);
|
|
80
80
|
uiLogger.log('');
|
|
81
81
|
uiLogger.log(lib.LocalDevManager.running(this.state.projectConfig.name, uiAccountDescription(this.state.targetProjectAccountId)));
|
|
@@ -11,7 +11,7 @@ import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
|
11
11
|
import { getAccountHomeUrl } from '../urls.js';
|
|
12
12
|
import { componentIsApp, componentIsPublicApp, CONFIG_FILES, getAppCardConfigs, getComponentUid, } from '../structure.js';
|
|
13
13
|
import { ComponentTypes, } from '../../../types/Projects.js';
|
|
14
|
-
import { UI_COLORS, uiCommandReference, uiAccountDescription,
|
|
14
|
+
import { UI_COLORS, uiCommandReference, uiAccountDescription, uiLink, uiLine, } from '../../ui/index.js';
|
|
15
15
|
import { logError } from '../../errorHandlers/index.js';
|
|
16
16
|
import { installAppBrowserPrompt } from '../../prompts/installAppPrompt.js';
|
|
17
17
|
import { confirmPrompt } from '../../prompts/promptUtils.js';
|
|
@@ -131,8 +131,8 @@ class LocalDevManager {
|
|
|
131
131
|
else if (!this.debug) {
|
|
132
132
|
console.clear();
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
uiLogger.log(uiLink(lib.LocalDevManager.learnMoreLocalDevServer, 'https://developers.hubspot.com/docs/
|
|
134
|
+
uiLogger.log(lib.LocalDevManager.headerMessage);
|
|
135
|
+
uiLogger.log(uiLink(lib.LocalDevManager.learnMoreLocalDevServer, 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands'));
|
|
136
136
|
uiLogger.log('');
|
|
137
137
|
uiLogger.log(chalk.hex(UI_COLORS.SORBET)(lib.LocalDevManager.running(this.projectConfig.name, uiAccountDescription(this.targetAccountId))));
|
|
138
138
|
uiLogger.log(lib.LocalDevManager.viewProjectLink(this.projectConfig.name, this.targetProjectAccountId));
|
|
@@ -6,7 +6,7 @@ import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, CONFIG
|
|
|
6
6
|
import { lib } from '../../../lang/en.js';
|
|
7
7
|
import { removeAnsiCodes } from '../../ui/removeAnsiCodes.js';
|
|
8
8
|
import { isDeployWebsocketMessage, isViewedWelcomeScreenWebsocketMessage, isUploadWebsocketMessage, isAppInstallFailureWebsocketMessage, isAppInstallSuccessWebsocketMessage, isAppInstallInitiatedWebsocketMessage, } from './localDevWebsocketServerUtils.js';
|
|
9
|
-
import pkg from '
|
|
9
|
+
import { pkg } from '../../jsonLoader.js';
|
|
10
10
|
const LOCAL_DEV_WEBSOCKET_SERVER_VERSION = 2;
|
|
11
11
|
const LOG_PREFIX = '[LocalDevWebsocketServer]';
|
|
12
12
|
const DOMAINS = ['hubspot.com', 'hubspotqa.com'];
|