@hubspot/cli 7.6.0-beta.6 → 7.6.0-beta.7

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.
Files changed (52) hide show
  1. package/README.md +0 -4
  2. package/commands/__tests__/testAccount.test.js +2 -0
  3. package/commands/project/__tests__/devUnifiedFlow.test.js +0 -21
  4. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  5. package/commands/project/dev/unifiedFlow.js +2 -5
  6. package/commands/testAccount/__tests__/importData.test.js +93 -0
  7. package/commands/testAccount/importData.d.ts +9 -0
  8. package/commands/testAccount/importData.js +61 -0
  9. package/commands/testAccount.js +2 -0
  10. package/lang/en.d.ts +38 -1
  11. package/lang/en.js +39 -2
  12. package/lib/__tests__/importData.test.d.ts +1 -0
  13. package/lib/__tests__/importData.test.js +89 -0
  14. package/lib/accountTypes.js +2 -3
  15. package/lib/app/__tests__/migrate.test.js +5 -5
  16. package/lib/app/migrate.js +2 -3
  17. package/lib/constants.d.ts +1 -0
  18. package/lib/constants.js +1 -0
  19. package/lib/hasFeature.d.ts +1 -0
  20. package/lib/hasFeature.js +7 -0
  21. package/lib/importData.d.ts +3 -0
  22. package/lib/importData.js +50 -0
  23. package/lib/projects/__tests__/AppDevModeInterface.test.js +2 -3
  24. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -25
  25. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  26. package/lib/projects/__tests__/localDevHelpers.test.js +2 -1
  27. package/lib/projects/localDev/AppDevModeInterface.js +1 -1
  28. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -3
  29. package/lib/projects/localDev/LocalDevLogger.js +2 -15
  30. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -1
  31. package/lib/projects/localDev/LocalDevProcess.js +3 -3
  32. package/lib/projects/localDev/LocalDevState.d.ts +6 -4
  33. package/lib/projects/localDev/LocalDevState.js +16 -10
  34. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  35. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  36. package/lib/projects/localDev/helpers.js +1 -1
  37. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  38. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  39. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  40. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  41. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  42. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  43. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  44. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  45. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  46. package/lib/ui/removeAnsiCodes.js +4 -0
  47. package/package.json +2 -2
  48. package/types/LocalDev.d.ts +0 -1
  49. package/lib/utils/__tests__/isDeepEqual.test.js +0 -269
  50. package/lib/utils/isDeepEqual.d.ts +0 -1
  51. package/lib/utils/isDeepEqual.js +0 -31
  52. /package/{lib/utils/__tests__/isDeepEqual.test.d.ts → commands/testAccount/__tests__/importData.test.d.ts} +0 -0
package/README.md CHANGED
@@ -4,10 +4,6 @@
4
4
 
5
5
  A CLI for HubSpot developers to enable local development and automation. [Learn more about building on HubSpot](https://developers.hubspot.com).
6
6
 
7
- ## Contributing
8
-
9
- For more information on developing, see the [Contributing Guide](CONTRIBUTING.md).
10
-
11
7
  ## Getting started
12
8
 
13
9
  For more information on using these tools, see [Local Development Tooling: Getting Started](https://developers.hubspot.com/docs/cms/guides/getting-started-with-local-development)
@@ -2,6 +2,7 @@ import yargs from 'yargs';
2
2
  import testAccountCreateCommand from '../testAccount/create.js';
3
3
  import testAccountCreateConfigCommand from '../testAccount/createConfig.js';
4
4
  import testAccountDeleteCommand from '../testAccount/delete.js';
5
+ import testAccountImportDataCommand from '../testAccount/importData.js';
5
6
  import testAccountCommands from '../testAccount.js';
6
7
  vi.mock('../testAccount/create');
7
8
  vi.mock('../testAccount/createConfig');
@@ -37,6 +38,7 @@ describe('commands/testAccount', () => {
37
38
  testAccountCreateCommand,
38
39
  testAccountCreateConfigCommand,
39
40
  testAccountDeleteCommand,
41
+ testAccountImportDataCommand,
40
42
  ];
41
43
  it('should demand the command takes one positional argument', () => {
42
44
  testAccountCommands.builder(yargs);
@@ -326,26 +326,6 @@ describe('unifiedProjectDevFlow', () => {
326
326
  projectName: mockProject.name,
327
327
  }));
328
328
  });
329
- it('should detect GitHub linked projects', async () => {
330
- const githubLinkedProject = {
331
- ...mockProject,
332
- sourceIntegration: { source: 'GITHUB' },
333
- };
334
- ensureProjectExists.mockResolvedValue({
335
- projectExists: true,
336
- project: githubLinkedProject,
337
- });
338
- await unifiedProjectDevFlow({
339
- args: mockArgs,
340
- targetProjectAccountId: mockTargetProjectAccountId,
341
- providedTargetTestingAccountId: mockProvidedTargetTestingAccountId,
342
- projectConfig: mockProjectConfig,
343
- projectDir: mockProjectDir,
344
- });
345
- expect(LocalDevProcess).toHaveBeenCalledWith(expect.objectContaining({
346
- isGithubLinked: true,
347
- }));
348
- });
349
329
  });
350
330
  describe('local dev process setup', () => {
351
331
  it('should initialize LocalDevProcess with correct parameters', async () => {
@@ -359,7 +339,6 @@ describe('unifiedProjectDevFlow', () => {
359
339
  expect(LocalDevProcess).toHaveBeenCalledWith({
360
340
  initialProjectNodes: mockProjectNodes,
361
341
  debug: mockArgs.debug,
362
- isGithubLinked: false,
363
342
  profile: mockArgs.profile,
364
343
  targetProjectAccountId: mockTargetProjectAccountId,
365
344
  targetTestingAccountId: mockProvidedTargetTestingAccountId,
@@ -10,5 +10,5 @@ type UnifiedProjectDevFlowArgs = {
10
10
  projectDir: string;
11
11
  profileConfig?: HsProfileFile;
12
12
  };
13
- export declare function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, profileConfig, }: UnifiedProjectDevFlowArgs): Promise<void>;
13
+ export declare function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, }: UnifiedProjectDevFlowArgs): Promise<void>;
14
14
  export {};
@@ -19,7 +19,7 @@ import { uiLine } from '../../../lib/ui/index.js';
19
19
  import { uiLogger } from '../../../lib/ui/logger.js';
20
20
  import { commands } from '../../../lang/en.js';
21
21
  import LocalDevWebsocketServer from '../../../lib/projects/localDev/LocalDevWebsocketServer.js';
22
- export async function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, profileConfig, }) {
22
+ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, }) {
23
23
  const env = getValidEnv(getEnv(targetProjectAccountId));
24
24
  let projectNodes;
25
25
  // Get IR
@@ -53,7 +53,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
53
53
  const accounts = getConfigAccounts();
54
54
  const accountIsCombined = await isUnifiedAccount(targetProjectAccountConfig);
55
55
  const targetProjectAccountIsTestAccountOrSandbox = isTestAccountOrSandbox(targetProjectAccountConfig);
56
- if (!accountIsCombined && !profileConfig) {
56
+ if (!accountIsCombined) {
57
57
  uiLogger.error(commands.project.dev.errors.accountNotCombined);
58
58
  process.exit(EXIT_CODES.ERROR);
59
59
  }
@@ -105,11 +105,9 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
105
105
  allowCreate: false,
106
106
  noLogs: true,
107
107
  });
108
- let isGithubLinked = false;
109
108
  let project = uploadedProject;
110
109
  SpinniesManager.init();
111
110
  if (projectExists && project) {
112
- isGithubLinked = Boolean(project.sourceIntegration && project.sourceIntegration.source === 'GITHUB');
113
111
  await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes);
114
112
  }
115
113
  else {
@@ -120,7 +118,6 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
120
118
  const localDevProcess = new LocalDevProcess({
121
119
  initialProjectNodes: projectNodes,
122
120
  debug: args.debug,
123
- isGithubLinked,
124
121
  profile: args.profile,
125
122
  targetProjectAccountId,
126
123
  targetTestingAccountId: targetTestingAccountId,
@@ -0,0 +1,93 @@
1
+ import yargs from 'yargs';
2
+ import { addAccountOptions, addConfigOptions, addUseEnvironmentOptions, } from '../../../lib/commonOpts.js';
3
+ import testAccountImportDataCommand from '../importData.js';
4
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
5
+ import { handleImportData, handleTargetTestAccountSelectionFlow, } from '../../../lib/importData.js';
6
+ import { trackCommandUsage } from '../../../lib/usageTracking.js';
7
+ import { getImportDataRequest } from '@hubspot/local-dev-lib/crm';
8
+ import { logError } from '../../../lib/errorHandlers/index.js';
9
+ import { importDataFilePathPrompt } from '../../../lib/prompts/importDataFilePathPrompt.js';
10
+ import { confirmImportDataPrompt } from '../../../lib/prompts/confirmImportDataPrompt.js';
11
+ vi.mock('../../../lib/commonOpts');
12
+ vi.mock('../../../lib/importData');
13
+ vi.mock('../../../lib/usageTracking');
14
+ vi.mock('@hubspot/local-dev-lib/crm');
15
+ vi.mock('../../../lib/errorHandlers/index');
16
+ vi.mock('../../../lib/prompts/importDataFilePathPrompt');
17
+ vi.mock('../../../lib/prompts/confirmImportDataPrompt');
18
+ describe('commands/testAccount/importData', () => {
19
+ const yargsMock = yargs;
20
+ const mockExit = vi
21
+ .spyOn(process, 'exit')
22
+ .mockImplementation(() => undefined);
23
+ const mockHandleImportData = vi.mocked(handleImportData);
24
+ const mockHandleTargetTestAccountSelectionFlow = vi.mocked(handleTargetTestAccountSelectionFlow);
25
+ const mockTrackCommandUsage = vi.mocked(trackCommandUsage);
26
+ const mockGetImportDataRequest = vi.mocked(getImportDataRequest);
27
+ const mockLogError = vi.mocked(logError);
28
+ const mockImportDataFilePathPrompt = vi.mocked(importDataFilePathPrompt);
29
+ const mockConfirmImportDataPrompt = vi.mocked(confirmImportDataPrompt);
30
+ beforeEach(() => {
31
+ mockExit.mockReset();
32
+ mockHandleImportData.mockReset();
33
+ mockHandleTargetTestAccountSelectionFlow.mockReset();
34
+ mockTrackCommandUsage.mockReset();
35
+ mockGetImportDataRequest.mockReset();
36
+ mockLogError.mockReset();
37
+ mockImportDataFilePathPrompt.mockReset();
38
+ mockConfirmImportDataPrompt.mockReset();
39
+ });
40
+ describe('command', () => {
41
+ it('should have the correct command structure', () => {
42
+ expect(testAccountImportDataCommand.command).toEqual('import-data');
43
+ });
44
+ });
45
+ // describe('describe', () => {
46
+ // it('should provide a description', () => {
47
+ // expect(testAccountImportDataCommand.describe).toBeDefined();
48
+ // });
49
+ // });
50
+ describe('builder', () => {
51
+ it('should support the correct options', () => {
52
+ testAccountImportDataCommand.builder(yargsMock);
53
+ expect(yargsMock.example).toHaveBeenCalledTimes(1);
54
+ expect(yargsMock.options).toHaveBeenCalledTimes(1);
55
+ expect(addAccountOptions).toHaveBeenCalledTimes(1);
56
+ expect(addAccountOptions).toHaveBeenCalledWith(yargsMock);
57
+ expect(addConfigOptions).toHaveBeenCalledTimes(1);
58
+ expect(addConfigOptions).toHaveBeenCalledWith(yargsMock);
59
+ expect(addUseEnvironmentOptions).toHaveBeenCalledTimes(1);
60
+ expect(addUseEnvironmentOptions).toHaveBeenCalledWith(yargsMock);
61
+ });
62
+ });
63
+ describe('handler', () => {
64
+ it('should complete the flow given the correct args', async () => {
65
+ const targetAccountId = 123456789;
66
+ const mockArgs = {
67
+ d: false,
68
+ debug: false,
69
+ _: [],
70
+ $0: 'hs',
71
+ derivedAccountId: targetAccountId,
72
+ userProvidedAccount: 'test-account',
73
+ filePath: 'test-file.json',
74
+ skipConfirm: true,
75
+ };
76
+ mockHandleTargetTestAccountSelectionFlow.mockResolvedValue(targetAccountId);
77
+ mockGetImportDataRequest.mockReturnValue({
78
+ importRequest: {},
79
+ dataFileNames: ['test-file.json'],
80
+ });
81
+ mockHandleImportData.mockResolvedValue();
82
+ await testAccountImportDataCommand.handler(mockArgs);
83
+ expect(mockHandleTargetTestAccountSelectionFlow).toHaveBeenCalledWith(123456789, 'test-account');
84
+ expect(mockGetImportDataRequest).toHaveBeenCalledWith('test-file.json');
85
+ expect(mockHandleImportData).toHaveBeenCalledTimes(1);
86
+ expect(mockTrackCommandUsage).toHaveBeenCalledTimes(1);
87
+ expect(mockLogError).not.toHaveBeenCalled();
88
+ expect(mockImportDataFilePathPrompt).not.toHaveBeenCalled();
89
+ expect(mockConfirmImportDataPrompt).not.toHaveBeenCalled();
90
+ expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.SUCCESS);
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,9 @@
1
+ import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ export declare const command = "import-data";
3
+ export declare const describe: undefined;
4
+ type CrmImportDataArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
5
+ filePath: string | undefined;
6
+ skipConfirm: boolean | undefined;
7
+ };
8
+ declare const crmImportDataCommand: YargsCommandModule<unknown, CrmImportDataArgs>;
9
+ export default crmImportDataCommand;
@@ -0,0 +1,61 @@
1
+ import { getImportDataRequest } from '@hubspot/local-dev-lib/crm';
2
+ import { logError } from '../../lib/errorHandlers/index.js';
3
+ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
4
+ import { trackCommandUsage } from '../../lib/usageTracking.js';
5
+ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
6
+ import { importDataFilePathPrompt } from '../../lib/prompts/importDataFilePathPrompt.js';
7
+ import { handleImportData, handleTargetTestAccountSelectionFlow, } from '../../lib/importData.js';
8
+ import { confirmImportDataPrompt } from '../../lib/prompts/confirmImportDataPrompt.js';
9
+ import { commands } from '../../lang/en.js';
10
+ export const command = 'import-data';
11
+ export const describe = undefined; // commands.testAccount.subcommands.importData.describe;
12
+ async function handler(args) {
13
+ const { derivedAccountId, userProvidedAccount, filePath: providedFilePath, skipConfirm, } = args;
14
+ trackCommandUsage('crm-import-data', {}, derivedAccountId);
15
+ let targetAccountId;
16
+ try {
17
+ targetAccountId = await handleTargetTestAccountSelectionFlow(derivedAccountId, userProvidedAccount);
18
+ const filePath = providedFilePath || (await importDataFilePathPrompt());
19
+ const { importRequest, dataFileNames } = getImportDataRequest(filePath);
20
+ const confirmImportData = skipConfirm ||
21
+ (await confirmImportDataPrompt(targetAccountId, dataFileNames));
22
+ if (!confirmImportData) {
23
+ process.exit(EXIT_CODES.SUCCESS);
24
+ }
25
+ await handleImportData(targetAccountId, dataFileNames, importRequest);
26
+ }
27
+ catch (error) {
28
+ logError(error);
29
+ process.exit(EXIT_CODES.ERROR);
30
+ }
31
+ process.exit(EXIT_CODES.SUCCESS);
32
+ }
33
+ function crmImportDataBuilder(yargs) {
34
+ yargs.example([['$0 test-account import-data']]);
35
+ yargs.options({
36
+ 'file-path': {
37
+ type: 'string',
38
+ describe: commands.testAccount.subcommands.importData.options.filePath.describe,
39
+ positional: false,
40
+ },
41
+ 'skip-confirm': {
42
+ type: 'boolean',
43
+ describe: commands.testAccount.subcommands.importData.options.skipConfirm
44
+ .describe,
45
+ },
46
+ });
47
+ return yargs;
48
+ }
49
+ const builder = makeYargsBuilder(crmImportDataBuilder, command, describe, {
50
+ useGlobalOptions: true,
51
+ useConfigOptions: true,
52
+ useAccountOptions: true,
53
+ useEnvironmentOptions: true,
54
+ });
55
+ const crmImportDataCommand = {
56
+ command,
57
+ describe,
58
+ builder,
59
+ handler,
60
+ };
61
+ export default crmImportDataCommand;
@@ -1,5 +1,6 @@
1
1
  import createTestAccountCommand from './testAccount/create.js';
2
2
  import createTestAccountConfigCommand from './testAccount/createConfig.js';
3
+ import importDataCommand from './testAccount/importData.js';
3
4
  import deleteTestAccountCommand from './testAccount/delete.js';
4
5
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
5
6
  import { commands } from '../lang/en.js';
@@ -10,6 +11,7 @@ function testAccountBuilder(yargs) {
10
11
  .command(createTestAccountCommand)
11
12
  .command(createTestAccountConfigCommand)
12
13
  .command(deleteTestAccountCommand)
14
+ .command(importDataCommand)
13
15
  .demandCommand(1, '');
14
16
  return yargs;
15
17
  }
package/lang/en.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts';
1
2
  export declare const commands: {
2
3
  readonly generalErrors: {
3
4
  readonly srcIsProject: (src: string, command: string) => string;
@@ -976,7 +977,7 @@ Profiles enable you to reference variables in your component configuration files
976
977
  readonly logs: {
977
978
  readonly betaMessage: "HubSpot projects local development";
978
979
  readonly placeholderAccountSelection: "Using default account as target account (for now)";
979
- readonly learnMoreLocalDevServer: "Learn more about the projects local dev server";
980
+ readonly learnMoreLocalDevServer: string;
980
981
  readonly accountTypeInformation: "Testing in a developer test account is strongly recommended, but you can use a sandbox account if your plan allows you to create one.";
981
982
  readonly learnMoreMessage: `
982
983
  Visit our ${string} to learn more.`;
@@ -1803,6 +1804,19 @@ ${string}`;
1803
1804
  };
1804
1805
  readonly testAccount: {
1805
1806
  readonly describe: "Commands for working with test accounts.";
1807
+ readonly subcommands: {
1808
+ readonly importData: {
1809
+ readonly describe: "Import data into the CRM";
1810
+ readonly options: {
1811
+ readonly skipConfirm: {
1812
+ readonly describe: "Skip the confirmation prompt";
1813
+ };
1814
+ readonly filePath: {
1815
+ readonly describe: "The path to the JSON file containing the import schema";
1816
+ };
1817
+ };
1818
+ };
1819
+ };
1806
1820
  readonly create: {
1807
1821
  readonly describe: "Create a test account from a config file";
1808
1822
  readonly configPathPrompt: "[--config-path] Enter the path to the test account config: ";
@@ -2690,6 +2704,16 @@ Run ${string} to upgrade to version ${string}`;
2690
2704
  readonly boxen: {
2691
2705
  readonly failedToLoad: "Failed to load boxen util.";
2692
2706
  };
2707
+ readonly importData: {
2708
+ readonly errors: {
2709
+ readonly incorrectAccountType: (derivedAccountId: number) => string;
2710
+ readonly failedToImportData: "Failed to import data into portal.";
2711
+ readonly notDeveloperTestAccount: "The account is not a developer test account.";
2712
+ readonly noAccountConfig: (accountId: number) => string;
2713
+ };
2714
+ readonly inProgress: (portalId: number, fileNames: string[]) => string;
2715
+ readonly viewImportLink: (baseUrl: string, accountId: number, importId: string) => string;
2716
+ };
2693
2717
  readonly ui: {
2694
2718
  readonly betaTag: string;
2695
2719
  readonly betaWarning: {
@@ -2849,6 +2873,19 @@ Run ${string} to upgrade to version ${string}`;
2849
2873
  };
2850
2874
  };
2851
2875
  readonly prompts: {
2876
+ readonly importDataFilePathPrompt: {
2877
+ readonly promptContext: `To view the JSON schema for data imports, visit ${string}`;
2878
+ readonly promptMessage: "[--file-path] Select the JSON file that will be used to import your data.";
2879
+ };
2880
+ readonly confirmImportDataPrompt: {
2881
+ readonly message: (dataFileNames: string[], cliAccount: CLIAccount | null) => string;
2882
+ };
2883
+ readonly importDataTestAccountSelectPrompt: {
2884
+ readonly errors: {
2885
+ readonly noAccountsFound: "No accounts found.";
2886
+ readonly noChildTestAccountsFound: (parentAccountId: number) => string;
2887
+ };
2888
+ };
2852
2889
  readonly projectDevTargetAccountPrompt: {
2853
2890
  readonly createNewSandboxOption: "<Test on a new development sandbox>";
2854
2891
  readonly createNewDeveloperTestAccountOption: "<Test on a new developer test account>";
package/lang/en.js CHANGED
@@ -5,6 +5,7 @@ import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUB
5
5
  import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
6
6
  import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, } from '../lib/projects/urls.js';
7
7
  import { PROJECT_CONFIG_FILE, PROJECT_WITH_APP } from '../lib/constants.js';
8
+ import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
8
9
  export const commands = {
9
10
  generalErrors: {
10
11
  srcIsProject: (src, command) => `"${src}" is in a project folder. Did you mean "hs project ${command}"?`,
@@ -976,7 +977,7 @@ export const commands = {
976
977
  logs: {
977
978
  betaMessage: 'HubSpot projects local development',
978
979
  placeholderAccountSelection: 'Using default account as target account (for now)',
979
- learnMoreLocalDevServer: 'Learn more about the projects local dev server',
980
+ learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
980
981
  accountTypeInformation: 'Testing in a developer test account is strongly recommended, but you can use a sandbox account if your plan allows you to create one.',
981
982
  learnMoreMessage: `\nVisit our ${uiLink('docs on Developer Test and Sandbox accounts', 'https://developers.hubspot.com/docs/getting-started/account-types')} to learn more.`,
982
983
  },
@@ -1798,6 +1799,19 @@ export const commands = {
1798
1799
  },
1799
1800
  testAccount: {
1800
1801
  describe: 'Commands for working with test accounts.',
1802
+ subcommands: {
1803
+ importData: {
1804
+ describe: 'Import data into the CRM',
1805
+ options: {
1806
+ skipConfirm: {
1807
+ describe: 'Skip the confirmation prompt',
1808
+ },
1809
+ filePath: {
1810
+ describe: 'The path to the JSON file containing the import schema',
1811
+ },
1812
+ },
1813
+ },
1814
+ },
1801
1815
  create: {
1802
1816
  describe: 'Create a test account from a config file',
1803
1817
  configPathPrompt: '[--config-path] Enter the path to the test account config: ',
@@ -2498,7 +2512,7 @@ export const lib = {
2498
2512
  },
2499
2513
  },
2500
2514
  AppDevModeInterface: {
2501
- defaultMarketplaceAppWarning: (installCount) => `\n\nYour marketplace app is currently installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`,
2515
+ defaultMarketplaceAppWarning: (installCount) => `Your marketplace app is currently installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`,
2502
2516
  autoInstallDeclined: 'You must install your app on your target test account to proceed with local development.',
2503
2517
  autoInstallSuccess: (appName, targetTestAccountId) => `Successfully installed app ${appName} on account ${uiAccountDescription(targetTestAccountId)}\n`,
2504
2518
  autoInstallError: (appName, targetTestAccountId) => `Error installing app ${appName} on account ${uiAccountDescription(targetTestAccountId)}. You may still be able to install your app in your browser.`,
@@ -2682,6 +2696,16 @@ export const lib = {
2682
2696
  boxen: {
2683
2697
  failedToLoad: 'Failed to load boxen util.',
2684
2698
  },
2699
+ importData: {
2700
+ errors: {
2701
+ incorrectAccountType: (derivedAccountId) => `The account ${uiAccountDescription(derivedAccountId)} is not a standard account, developer test account, or app developer account.`,
2702
+ failedToImportData: 'Failed to import data into portal.',
2703
+ notDeveloperTestAccount: 'The account is not a developer test account.',
2704
+ noAccountConfig: (accountId) => `No account config found for ${uiAccountDescription(accountId)}`,
2705
+ },
2706
+ inProgress: (portalId, fileNames) => `Importing data into ${uiAccountDescription(portalId)} from [${fileNames.join(', ')}]`,
2707
+ viewImportLink: (baseUrl, accountId, importId) => `Data import currently processing. You can view the status of your import ${uiLink('here', `${baseUrl}/import/${accountId}/post/${importId}`)}`,
2708
+ },
2685
2709
  ui: {
2686
2710
  betaTag: chalk.bold('[BETA]'),
2687
2711
  betaWarning: {
@@ -2841,6 +2865,19 @@ export const lib = {
2841
2865
  },
2842
2866
  },
2843
2867
  prompts: {
2868
+ importDataFilePathPrompt: {
2869
+ promptContext: `To view the JSON schema for data imports, visit ${uiLink('the docs', 'https://developers.hubspot.com/docs/guides/api/crm/imports')}`,
2870
+ promptMessage: '[--file-path] Select the JSON file that will be used to import your data.',
2871
+ },
2872
+ confirmImportDataPrompt: {
2873
+ message: (dataFileNames, cliAccount) => `You are importing [${dataFileNames.join(', ')}] into ${uiAccountDescription(getAccountIdentifier(cliAccount))}. Continue?`,
2874
+ },
2875
+ importDataTestAccountSelectPrompt: {
2876
+ errors: {
2877
+ noAccountsFound: 'No accounts found.',
2878
+ noChildTestAccountsFound: (parentAccountId) => `No developer test accounts found under the parent account ${uiAccountDescription(parentAccountId)}`,
2879
+ },
2880
+ },
2844
2881
  projectDevTargetAccountPrompt: {
2845
2882
  createNewSandboxOption: '<Test on a new development sandbox>',
2846
2883
  createNewDeveloperTestAccountOption: '<Test on a new developer test account>',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { uiLogger } from '../ui/logger.js';
2
+ import { createImport } from '@hubspot/local-dev-lib/api/crm';
3
+ import { getAccountConfig, getAccountId } from '@hubspot/local-dev-lib/config';
4
+ import { handleImportData, handleTargetTestAccountSelectionFlow, } from '../importData.js';
5
+ import { lib } from '../../lang/en.js';
6
+ import { isDeveloperTestAccount, isStandardAccount, isAppDeveloperAccount, } from '../accountTypes.js';
7
+ import { importDataTestAccountSelectPrompt } from '../prompts/importDataTestAccountSelectPrompt.js';
8
+ vi.mock('../ui/logger');
9
+ vi.mock('@hubspot/local-dev-lib/api/crm');
10
+ vi.mock('@hubspot/local-dev-lib/config');
11
+ vi.mock('../accountTypes');
12
+ vi.mock('../prompts/importDataTestAccountSelectPrompt');
13
+ describe('lib/importData', () => {
14
+ const mockUiLogger = vi.mocked(uiLogger);
15
+ const mockCreateImport = vi.mocked(createImport);
16
+ const mockGetAccountConfig = vi.mocked(getAccountConfig);
17
+ const mockGetAccountId = vi.mocked(getAccountId);
18
+ const mockIsDeveloperTestAccount = vi.mocked(isDeveloperTestAccount);
19
+ const mockIsStandardAccount = vi.mocked(isStandardAccount);
20
+ const mockIsAppDeveloperAccount = vi.mocked(isAppDeveloperAccount);
21
+ const mockImportDataTestAccountSelectPrompt = vi.mocked(importDataTestAccountSelectPrompt);
22
+ beforeEach(() => {
23
+ mockUiLogger.info.mockReset();
24
+ mockUiLogger.success.mockReset();
25
+ mockUiLogger.error.mockReset();
26
+ mockCreateImport.mockReset();
27
+ mockGetAccountConfig.mockReset();
28
+ mockGetAccountId.mockReset();
29
+ mockIsDeveloperTestAccount.mockReset();
30
+ mockIsStandardAccount.mockReset();
31
+ mockIsAppDeveloperAccount.mockReset();
32
+ mockImportDataTestAccountSelectPrompt.mockReset();
33
+ });
34
+ describe('handleImportData', () => {
35
+ const targetAccountId = 123456789;
36
+ const dataFileNames = ['test-file.json'];
37
+ const importRequest = {
38
+ name: 'test-import',
39
+ };
40
+ it('should log the correct success message', async () => {
41
+ // @ts-expect-error - mockCreateImport is not typed correctly
42
+ mockCreateImport.mockResolvedValue({
43
+ data: { id: '123' },
44
+ });
45
+ await handleImportData(targetAccountId, dataFileNames, importRequest);
46
+ expect(mockUiLogger.success).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
47
+ });
48
+ it('should log the correct error message', async () => {
49
+ mockCreateImport.mockRejectedValue(new Error('test-error'));
50
+ // weird because we catch the error, log a specific message, and then throw it again
51
+ await expect(handleImportData(targetAccountId, dataFileNames, importRequest)).rejects.toThrow('test-error');
52
+ expect(mockUiLogger.error).toHaveBeenCalledWith(lib.importData.errors.failedToImportData);
53
+ });
54
+ });
55
+ describe('handleTargetTestAccountSelectionFlow', () => {
56
+ const userProvidedAccountId = '1234';
57
+ const derivedAccountId = 123456789;
58
+ it('should error if the userProvidedAccountId is not the right account type', async () => {
59
+ mockGetAccountConfig.mockReturnValue({});
60
+ mockGetAccountId.mockReturnValue(1234);
61
+ mockIsDeveloperTestAccount.mockReturnValue(false);
62
+ await expect(handleTargetTestAccountSelectionFlow(derivedAccountId, userProvidedAccountId)).rejects.toThrow(lib.importData.errors.notDeveloperTestAccount);
63
+ });
64
+ it('should error if the derivedAccountId belongs to the wrong account type', async () => {
65
+ mockGetAccountConfig.mockReturnValue({});
66
+ mockIsDeveloperTestAccount.mockReturnValue(false);
67
+ mockIsStandardAccount.mockReturnValue(false);
68
+ mockIsAppDeveloperAccount.mockReturnValue(false);
69
+ await expect(handleTargetTestAccountSelectionFlow(derivedAccountId, undefined)).rejects.toThrow(lib.importData.errors.incorrectAccountType(derivedAccountId));
70
+ });
71
+ it('should return the derivedAccountId if it is a developer test account', async () => {
72
+ mockGetAccountConfig.mockReturnValue({});
73
+ mockIsDeveloperTestAccount.mockReturnValue(true);
74
+ const result = await handleTargetTestAccountSelectionFlow(derivedAccountId, undefined);
75
+ expect(result).toBe(derivedAccountId);
76
+ });
77
+ it('should return the result of the importDataTestAccountSelectPrompt if the derivedAccountId is a standard or app developer account', async () => {
78
+ mockGetAccountConfig.mockReturnValue({});
79
+ mockIsDeveloperTestAccount.mockReturnValue(false);
80
+ mockIsStandardAccount.mockReturnValue(true);
81
+ mockIsAppDeveloperAccount.mockReturnValue(true);
82
+ mockImportDataTestAccountSelectPrompt.mockResolvedValue({
83
+ selectedAccountId: 890223,
84
+ });
85
+ const result = await handleTargetTestAccountSelectionFlow(derivedAccountId, undefined);
86
+ expect(result).toBe(890223);
87
+ });
88
+ });
89
+ });
@@ -1,6 +1,5 @@
1
1
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
2
- import { hasFeature } from './hasFeature.js';
3
- import { FEATURES } from './constants.js';
2
+ import { hasUnfiedAppsAccess } from './hasFeature.js';
4
3
  import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
5
4
  function isAccountType(accountConfig, accountType) {
6
5
  return Boolean(accountConfig.accountType && accountType.includes(accountConfig.accountType));
@@ -40,5 +39,5 @@ export async function isUnifiedAccount(account) {
40
39
  if (!accountId) {
41
40
  return false;
42
41
  }
43
- return hasFeature(accountId, FEATURES.UNIFIED_APPS);
42
+ return hasUnfiedAppsAccess(accountId);
44
43
  }
@@ -11,7 +11,7 @@ import { ensureProjectExists } from '../../projects/ensureProjectExists.js';
11
11
  import { poll } from '../../polling.js';
12
12
  import { CLI_UNMIGRATABLE_REASONS, continueMigration, initializeMigration, listAppsForMigration, listThemesForMigration, } from '../../../api/migrate.js';
13
13
  import { lib } from '../../../lang/en.js';
14
- import { hasFeature } from '../../hasFeature.js';
14
+ import { hasUnfiedAppsAccess } from '../../hasFeature.js';
15
15
  import { getUnmigratableReason, generateFilterAppsByProjectNameFunction, buildErrorMessageFromMigrationStatus, fetchMigrationApps, promptForAppToMigrate, selectAppToMigrate, handleMigrationSetup, beginMigration, pollMigrationStatus, finalizeMigration, downloadProjectFiles, migrateApp2025_2, logInvalidAccountError, validateMigrationAppsAndThemes, } from '../migrate.js';
16
16
  vi.mock('@hubspot/local-dev-lib/logger');
17
17
  vi.mock('@hubspot/local-dev-lib/path');
@@ -43,7 +43,7 @@ const mockedListAppsForMigration = listAppsForMigration;
43
43
  const mockedListThemesForMigration = listThemesForMigration;
44
44
  const mockedInitializeMigration = initializeMigration;
45
45
  const mockedContinueMigration = continueMigration;
46
- const mockedHasFeature = hasFeature;
46
+ const mockedHasUnfiedAppsAccess = hasUnfiedAppsAccess;
47
47
  const mockedFs = fs;
48
48
  const createMockMigratableApp = (id, name, projectName) => ({
49
49
  appId: id,
@@ -82,7 +82,7 @@ describe('lib/app/migrate', () => {
82
82
  mockedGetCwd.mockReturnValue(MOCK_CWD);
83
83
  mockedSanitizeFileName.mockImplementation(name => name);
84
84
  mockedValidateUid.mockReturnValue(undefined);
85
- mockedHasFeature.mockResolvedValue(true);
85
+ mockedHasUnfiedAppsAccess.mockResolvedValue(true);
86
86
  mockedFs.renameSync.mockImplementation(() => { });
87
87
  });
88
88
  describe('getUnmigratableReason', () => {
@@ -472,10 +472,10 @@ describe('lib/app/migrate', () => {
472
472
  unstable: false,
473
473
  };
474
474
  beforeEach(() => {
475
- mockedHasFeature.mockResolvedValue(true);
475
+ mockedHasUnfiedAppsAccess.mockResolvedValue(true);
476
476
  });
477
477
  it('should throw an error when account is not ungated for unified apps', async () => {
478
- mockedHasFeature.mockResolvedValueOnce(false);
478
+ mockedHasUnfiedAppsAccess.mockResolvedValueOnce(false);
479
479
  await expect(migrateApp2025_2(ACCOUNT_ID, options)).rejects.toThrowError(/isn't enrolled in the required product beta to access this command./);
480
480
  });
481
481
  it('should throw an error when projectConfig is invalid', async () => {
@@ -16,8 +16,7 @@ import { DEFAULT_POLLING_STATUS_LOOKUP, poll } from '../polling.js';
16
16
  import { checkMigrationStatusV2, CLI_UNMIGRATABLE_REASONS, continueMigration, initializeMigration, isMigrationStatus, listAppsForMigration, listThemesForMigration, } from '../../api/migrate.js';
17
17
  import fs from 'fs';
18
18
  import { lib } from '../../lang/en.js';
19
- import { hasFeature } from '../hasFeature.js';
20
- import { FEATURES } from '../constants.js';
19
+ import { hasUnfiedAppsAccess } from '../hasFeature.js';
21
20
  import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
22
21
  import { uiLogger } from '../ui/logger.js';
23
22
  export function getUnmigratableReason(reasonCode, projectName, accountId) {
@@ -340,7 +339,7 @@ export async function downloadProjectFiles(derivedAccountId, projectName, buildI
340
339
  }
341
340
  export async function migrateApp2025_2(derivedAccountId, options, projectConfig) {
342
341
  SpinniesManager.init();
343
- const ungatedForUnifiedApps = await hasFeature(derivedAccountId, FEATURES.UNIFIED_APPS);
342
+ const ungatedForUnifiedApps = await hasUnfiedAppsAccess(derivedAccountId);
344
343
  if (!ungatedForUnifiedApps) {
345
344
  throw new Error(lib.migrate.errors.notUngatedForUnifiedApps(uiAccountDescription(derivedAccountId)));
346
345
  }
@@ -87,6 +87,7 @@ export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
87
87
  UPDATE_PROJECT_NODES: string;
88
88
  UPDATE_APP_DATA: string;
89
89
  UPDATE_PROJECT_DATA: string;
90
+ UPDATE_UPLOAD_WARNINGS: string;
90
91
  };
91
92
  export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
92
93
  UPLOAD: string;
package/lib/constants.js CHANGED
@@ -79,6 +79,7 @@ export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
79
79
  UPDATE_PROJECT_NODES: 'server:updateProjectNodes',
80
80
  UPDATE_APP_DATA: 'server:updateAppData',
81
81
  UPDATE_PROJECT_DATA: 'server:updateProjectData',
82
+ UPDATE_UPLOAD_WARNINGS: 'server:updateUploadWarnings',
82
83
  };
83
84
  export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
84
85
  UPLOAD: 'client:upload',
@@ -1,3 +1,4 @@
1
1
  import { FEATURES } from './constants.js';
2
2
  import { ValueOf } from '@hubspot/local-dev-lib/types/Utils';
3
3
  export declare function hasFeature(accountId: number, feature: ValueOf<typeof FEATURES>): Promise<boolean>;
4
+ export declare function hasUnfiedAppsAccess(accountId: number): Promise<boolean>;