@hubspot/cli 7.7.32-experimental.0 → 7.7.34-experimental.0

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 (118) hide show
  1. package/commands/app/__tests__/install.test.js +47 -0
  2. package/commands/app/install.d.ts +8 -0
  3. package/commands/app/install.js +122 -0
  4. package/commands/app.js +6 -1
  5. package/commands/getStarted.js +7 -20
  6. package/commands/project/__tests__/add.test.js +3 -5
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.js +2 -4
  9. package/commands/project/deploy.js +9 -61
  10. package/commands/project/dev/index.js +1 -1
  11. package/commands/project/dev/unifiedFlow.js +4 -1
  12. package/commands/project/upload.d.ts +2 -2
  13. package/commands/project/upload.js +3 -3
  14. package/commands/project/validate.js +1 -1
  15. package/commands/project/watch.js +2 -2
  16. package/commands/testAccount/create.js +3 -0
  17. package/lang/en.d.ts +30 -4
  18. package/lang/en.js +31 -5
  19. package/lib/__tests__/hasFeature.test.js +145 -7
  20. package/lib/__tests__/importData.test.js +1 -1
  21. package/lib/app/migrate.js +9 -2
  22. package/lib/constants.d.ts +2 -0
  23. package/lib/constants.js +2 -0
  24. package/lib/errorHandlers/index.d.ts +4 -0
  25. package/lib/errorHandlers/index.js +1 -1
  26. package/lib/hasFeature.js +6 -0
  27. package/lib/importData.js +1 -1
  28. package/lib/mcp/setup.js +1 -1
  29. package/lib/projectProfiles.d.ts +1 -1
  30. package/lib/projectProfiles.js +10 -2
  31. package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
  32. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  33. package/lib/projects/__tests__/deploy.test.d.ts +1 -0
  34. package/lib/projects/__tests__/deploy.test.js +164 -0
  35. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  36. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  37. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  38. package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
  39. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  40. package/lib/projects/add/legacyAddComponent.js +5 -1
  41. package/lib/projects/add/v3AddComponent.d.ts +1 -0
  42. package/lib/projects/add/v3AddComponent.js +2 -2
  43. package/lib/projects/create/__tests__/v3.test.js +97 -9
  44. package/lib/projects/create/index.js +2 -2
  45. package/lib/projects/create/legacy.js +1 -1
  46. package/lib/projects/create/v3.d.ts +2 -2
  47. package/lib/projects/create/v3.js +35 -12
  48. package/lib/projects/deploy.d.ts +13 -0
  49. package/lib/projects/deploy.js +63 -0
  50. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
  51. package/lib/projects/localDev/AppDevModeInterface.js +65 -36
  52. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  53. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  54. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  55. package/lib/projects/localDev/LocalDevState.js +9 -1
  56. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  57. package/lib/projects/localDev/helpers/project.js +6 -7
  58. package/lib/projects/platformVersion.d.ts +1 -0
  59. package/lib/projects/platformVersion.js +10 -0
  60. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  61. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  62. package/lib/projects/structure.d.ts +2 -2
  63. package/lib/projects/upload.d.ts +2 -1
  64. package/lib/projects/upload.js +2 -1
  65. package/lib/projects/urls.d.ts +1 -0
  66. package/lib/projects/urls.js +3 -0
  67. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  68. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  69. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  70. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  71. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  72. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  73. package/lib/prompts/installAppPrompt.d.ts +6 -1
  74. package/lib/prompts/installAppPrompt.js +6 -1
  75. package/lib/prompts/projectAddPrompt.js +1 -1
  76. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  77. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  78. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  79. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  80. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  81. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  82. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  83. package/mcp-server/tools/cms/HsListTool.js +1 -1
  84. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -1
  85. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
  86. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
  87. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  88. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  89. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
  90. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  91. package/mcp-server/tools/index.js +2 -0
  92. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  93. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  94. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  95. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  96. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  97. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  98. package/mcp-server/tools/project/DocsSearchTool.js +2 -2
  99. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -4
  100. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  101. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  102. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  103. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  104. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  105. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  106. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  107. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
  108. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  109. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  110. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  111. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  112. package/mcp-server/tools/project/constants.d.ts +1 -1
  113. package/mcp-server/tools/project/constants.js +14 -6
  114. package/package.json +3 -3
  115. package/types/LocalDev.d.ts +2 -1
  116. package/types/Projects.d.ts +1 -0
  117. package/types/Yargs.d.ts +1 -1
  118. /package/{lib/projects/__tests__/buildAndDeploy.test.d.ts → commands/app/__tests__/install.test.d.ts} +0 -0
package/lang/en.d.ts CHANGED
@@ -23,7 +23,8 @@ export declare const commands: {
23
23
  };
24
24
  readonly startTitle: "Welcome to HubSpot Development!";
25
25
  readonly verboseDescribe: "A step-by-step command to get you started with a HubSpot project.";
26
- readonly startDescription: "You can use the HubSpot CLI to build apps, CMS themes, and more.";
26
+ readonly startDescription: "You can use the HubSpot CLI to build apps, CMS themes, and more.\n";
27
+ readonly guideOverview: (accountName: string) => string;
27
28
  readonly designManager: "To onboard with CMS, please visit the HubSpot Design Manager in your account and follow the checklist items.";
28
29
  readonly openDesignManager: "Click here to go to the HubSpot Design Manager";
29
30
  readonly openDesignManagerPrompt: "Open Design Manager in your browser?";
@@ -1188,9 +1189,11 @@ ${string}`;
1188
1189
  readonly success: (componentName: string, multiple?: boolean) => string;
1189
1190
  readonly error: {
1190
1191
  readonly failedToDownloadComponent: "Failed to download project. Please try again later.";
1192
+ readonly invalidComponentType: (componentType: string) => string;
1191
1193
  readonly maxExceeded: (maxCount: number) => string;
1192
1194
  readonly authTypeNotAllowed: (authType: string) => string;
1193
1195
  readonly distributionNotAllowed: (dist: string) => string;
1196
+ readonly portalDoesNotHaveAccessToThisFeature: (accountId: number) => string;
1194
1197
  readonly locationInProject: "This command must be run from within a project directory.";
1195
1198
  readonly failedToFetchComponentList: "Failed to fetch the list of available features. Please try again later.";
1196
1199
  readonly projectContainsPublicApp: "This project contains a public app. This command is currently only compatible with projects that contain private apps.";
@@ -1550,6 +1553,28 @@ ${string}`;
1550
1553
  readonly app: {
1551
1554
  readonly describe: "Commands for managing apps.";
1552
1555
  readonly subcommands: {
1556
+ readonly install: {
1557
+ readonly describe: "Install an OAuth app into a test account.";
1558
+ readonly options: {
1559
+ readonly appUid: "The uid of the app to install";
1560
+ readonly projectName: "The name of the project that contains the app";
1561
+ };
1562
+ readonly positionals: {
1563
+ readonly testAccountId: "The id of the test account to install the app into";
1564
+ };
1565
+ readonly errors: {
1566
+ readonly mustSpecifyProjectName: `You must specify a project name. Use the ${string} flag to specify the project name or run this command from within a project directory.`;
1567
+ readonly noAppUidFound: `No app uid found. Please specify the app uid with the ${string} flag or run this command from within a project that contains an app.`;
1568
+ readonly appMustBeOauth: "This command only supports installing oauth apps. Please specify an app with oauth auth type.";
1569
+ };
1570
+ readonly polling: {
1571
+ readonly start: "Installing app...";
1572
+ readonly success: "App installed successfully";
1573
+ readonly failure: "App installation failed";
1574
+ readonly error: "Error installing app";
1575
+ };
1576
+ readonly example: "Install the app with uid my-app-uid from the project named \"my-project\" into the target account with id 1234567890";
1577
+ };
1553
1578
  readonly secret: {
1554
1579
  readonly describe: "Commands for managing secrets.";
1555
1580
  readonly subcommands: {
@@ -2598,7 +2623,7 @@ export declare const lib: {
2598
2623
  readonly checking: "Checking if your deployed build is up to date...";
2599
2624
  readonly upToDate: "Deployed build is up to date.";
2600
2625
  readonly notUpToDate: "Your project contains undeployed local changes.";
2601
- readonly notUpToDateExplanation: `Run ${string} to upload these changes to HubSpot, then re-run ${string} to continue local development.`;
2626
+ readonly notUpToDateExplanation: (profile?: string) => string;
2602
2627
  };
2603
2628
  readonly createNewProjectForLocalDev: {
2604
2629
  readonly projectMustExistExplanation: (projectName: string, accountId: number) => string;
@@ -3160,8 +3185,9 @@ Run ${string} to upgrade to version ${string}`;
3160
3185
  };
3161
3186
  };
3162
3187
  readonly installAppPrompt: {
3163
- readonly explanation: "Local development requires this app to be installed in the target test account";
3188
+ readonly explanation: "Local development requires this app to be installed in the target test account.";
3164
3189
  readonly reinstallExplanation: "This app's required scopes have been updated since it was last installed on the target test account. To avoid issues with local development, we recommend reinstalling the app with the updated scopes.";
3190
+ readonly staticAuthExplanation: (projectAccountId: number, testingAccountId: number, projectName: string, appUid: string) => string;
3165
3191
  readonly prompt: "Open HubSpot to install this app?";
3166
3192
  readonly autoPrompt: "Install this app in your target test account?";
3167
3193
  readonly reinstallPrompt: "Open HubSpot to reinstall this app?";
@@ -3397,7 +3423,7 @@ Run ${string} to upgrade to version ${string}`;
3397
3423
  readonly themesAndAppsNotAllowed: "Support for migrating projects containing both themes and apps to the latest platform version is coming soon. Try again later.";
3398
3424
  readonly multipleApps: "Multiple apps found in project, this is not allowed in 2025.2";
3399
3425
  readonly alreadyExists: (projectName: string) => string;
3400
- readonly failedToMigrateThemes: "Failed to migrate project themes. Please verify that you have propoerly formatted themes before trying again.";
3426
+ readonly failedToMigrateThemes: "Failed to migrate project themes. Please verify that your themes are properly formatted before trying again.";
3401
3427
  readonly failedToUpdateProjectConfig: "Failed to update project config file. Please update the platformVersion in the project config file manually.";
3402
3428
  };
3403
3429
  readonly unmigratableReasons: {
package/lang/en.js CHANGED
@@ -4,7 +4,7 @@ import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
4
4
  import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
5
5
  import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
6
6
  import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
7
- import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, } from '../lib/projects/urls.js';
7
+ import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, getAppAllowlistUrl, } from '../lib/projects/urls.js';
8
8
  import { APP_DISTRIBUTION_TYPES, APP_AUTH_TYPES, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
9
9
  import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
10
10
  export const commands = {
@@ -31,7 +31,8 @@ export const commands = {
31
31
  },
32
32
  startTitle: 'Welcome to HubSpot Development!',
33
33
  verboseDescribe: 'A step-by-step command to get you started with a HubSpot project.',
34
- startDescription: 'You can use the HubSpot CLI to build apps, CMS themes, and more.',
34
+ startDescription: 'You can use the HubSpot CLI to build apps, CMS themes, and more.\n',
35
+ guideOverview: (accountName) => `This guide will walk you through deploying your first project to ${chalk.bold(accountName)}.\nTo target a different account, exit this guide ${chalk.bold('(ctrl + c)')} and run ${uiCommandReference('hs account use')} before trying again.`,
35
36
  designManager: 'To onboard with CMS, please visit the HubSpot Design Manager in your account and follow the checklist items.',
36
37
  openDesignManager: 'Click here to go to the HubSpot Design Manager',
37
38
  openDesignManagerPrompt: 'Open Design Manager in your browser?',
@@ -1187,9 +1188,11 @@ export const commands = {
1187
1188
  success: (componentName, multiple = false) => `${componentName || 'An app'} ${multiple ? 'were' : 'was'} successfully added to your ${componentName ? 'app' : 'project'}.`,
1188
1189
  error: {
1189
1190
  failedToDownloadComponent: 'Failed to download project. Please try again later.',
1191
+ invalidComponentType: (componentType) => `'${componentType}' is not a valid project component type.`,
1190
1192
  maxExceeded: (maxCount) => `This project has the maximum allowed(${maxCount})`,
1191
1193
  authTypeNotAllowed: (authType) => `Auth type '${authType}' not allowed.`,
1192
1194
  distributionNotAllowed: (dist) => `Distribution '${dist}' not allowed.`,
1195
+ portalDoesNotHaveAccessToThisFeature: (accountId) => `The account ${uiAccountDescription(accountId)} does not have access to this feature`,
1193
1196
  locationInProject: 'This command must be run from within a project directory.',
1194
1197
  failedToFetchComponentList: 'Failed to fetch the list of available features. Please try again later.',
1195
1198
  projectContainsPublicApp: 'This project contains a public app. This command is currently only compatible with projects that contain private apps.',
@@ -1547,6 +1550,28 @@ export const commands = {
1547
1550
  app: {
1548
1551
  describe: 'Commands for managing apps.',
1549
1552
  subcommands: {
1553
+ install: {
1554
+ describe: 'Install an OAuth app into a test account.',
1555
+ options: {
1556
+ appUid: 'The uid of the app to install',
1557
+ projectName: 'The name of the project that contains the app',
1558
+ },
1559
+ positionals: {
1560
+ testAccountId: 'The id of the test account to install the app into',
1561
+ },
1562
+ errors: {
1563
+ mustSpecifyProjectName: `You must specify a project name. Use the ${uiCommandReference('--project-name')} flag to specify the project name or run this command from within a project directory.`,
1564
+ noAppUidFound: `No app uid found. Please specify the app uid with the ${uiCommandReference('--app-uid')} flag or run this command from within a project that contains an app.`,
1565
+ appMustBeOauth: 'This command only supports installing oauth apps. Please specify an app with oauth auth type.',
1566
+ },
1567
+ polling: {
1568
+ start: 'Installing app...',
1569
+ success: 'App installed successfully',
1570
+ failure: 'App installation failed',
1571
+ error: 'Error installing app',
1572
+ },
1573
+ example: 'Install the app with uid my-app-uid from the project named "my-project" into the target account with id 1234567890',
1574
+ },
1550
1575
  secret: {
1551
1576
  describe: 'Commands for managing secrets.',
1552
1577
  subcommands: {
@@ -2595,7 +2620,7 @@ export const lib = {
2595
2620
  checking: 'Checking if your deployed build is up to date...',
2596
2621
  upToDate: 'Deployed build is up to date.',
2597
2622
  notUpToDate: `Your project contains undeployed local changes.`,
2598
- notUpToDateExplanation: `Run ${uiCommandReference('hs project upload')} to upload these changes to HubSpot, then re-run ${uiCommandReference('hs project dev')} to continue local development.`,
2623
+ notUpToDateExplanation: (profile) => `Run ${uiCommandReference(`hs project upload ${profile ? `--profile ${profile}` : ''}`)} to upload these changes to HubSpot, then re-run ${uiCommandReference(`hs project dev ${profile ? `--profile ${profile}` : ''}`)} to continue local development.`,
2599
2624
  },
2600
2625
  createNewProjectForLocalDev: {
2601
2626
  projectMustExistExplanation: (projectName, accountId) => `The project ${projectName} does not exist in the target account ${uiAccountDescription(accountId)}. This command requires the project to exist in the target account.`,
@@ -3154,8 +3179,9 @@ export const lib = {
3154
3179
  },
3155
3180
  },
3156
3181
  installAppPrompt: {
3157
- explanation: 'Local development requires this app to be installed in the target test account',
3182
+ explanation: 'Local development requires this app to be installed in the target test account.',
3158
3183
  reinstallExplanation: "This app's required scopes have been updated since it was last installed on the target test account. To avoid issues with local development, we recommend reinstalling the app with the updated scopes.",
3184
+ staticAuthExplanation: (projectAccountId, testingAccountId, projectName, appUid) => `To install this static auth app, your testing account ${uiAccountDescription(testingAccountId)} must be on ${uiLink("this app's allowlist", getAppAllowlistUrl(projectAccountId, projectName, appUid))}.`,
3159
3185
  prompt: 'Open HubSpot to install this app?',
3160
3186
  autoPrompt: 'Install this app in your target test account?',
3161
3187
  reinstallPrompt: 'Open HubSpot to reinstall this app?',
@@ -3391,7 +3417,7 @@ export const lib = {
3391
3417
  themesAndAppsNotAllowed: 'Support for migrating projects containing both themes and apps to the latest platform version is coming soon. Try again later.',
3392
3418
  multipleApps: 'Multiple apps found in project, this is not allowed in 2025.2',
3393
3419
  alreadyExists: (projectName) => `A project with name ${projectName} already exists. Please choose another name.`,
3394
- failedToMigrateThemes: 'Failed to migrate project themes. Please verify that you have propoerly formatted themes before trying again.',
3420
+ failedToMigrateThemes: 'Failed to migrate project themes. Please verify that your themes are properly formatted before trying again.',
3395
3421
  failedToUpdateProjectConfig: 'Failed to update project config file. Please update the platformVersion in the project config file manually.',
3396
3422
  },
3397
3423
  unmigratableReasons: {
@@ -1,35 +1,173 @@
1
1
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
2
- import { hasFeature } from '../hasFeature.js';
2
+ import { http } from '@hubspot/local-dev-lib/http';
3
+ import { hasFeature, hasUnfiedAppsAccess } from '../hasFeature.js';
4
+ import { FEATURES } from '../constants.js';
3
5
  vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
6
+ vi.mock('@hubspot/local-dev-lib/http');
4
7
  const mockedFetchEnabledFeatures = fetchEnabledFeatures;
8
+ const mockedHttp = http;
5
9
  describe('lib/hasFeature', () => {
6
10
  describe('hasFeature()', () => {
7
11
  const accountId = 123;
8
- beforeEach(() => {
12
+ afterEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ it('should return true if the feature is enabled', async () => {
9
16
  mockedFetchEnabledFeatures.mockResolvedValueOnce({
10
17
  data: {
11
18
  enabledFeatures: {
12
19
  'feature-1': true,
13
- 'feature-2': false,
14
- 'feature-3': true,
15
20
  },
16
21
  },
17
22
  });
18
- });
19
- it('should return true if the feature is enabled', async () => {
20
23
  // @ts-expect-error test data
21
24
  const result = await hasFeature(accountId, 'feature-1');
22
25
  expect(result).toBe(true);
23
26
  });
24
- it('should return false if the feature is not enabled', async () => {
27
+ it('should return false if the feature is disabled', async () => {
28
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
29
+ data: {
30
+ enabledFeatures: {
31
+ 'feature-2': false,
32
+ },
33
+ },
34
+ });
25
35
  // @ts-expect-error test data
26
36
  const result = await hasFeature(accountId, 'feature-2');
27
37
  expect(result).toBe(false);
28
38
  });
29
39
  it('should return false if the feature is not present', async () => {
40
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
41
+ data: {
42
+ enabledFeatures: {},
43
+ },
44
+ });
30
45
  // @ts-expect-error test data
31
46
  const result = await hasFeature(accountId, 'feature-4');
32
47
  expect(result).toBe(false);
33
48
  });
49
+ it('should return true for APPS_HOME feature when not present in enabled features (defaults on)', async () => {
50
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
51
+ data: {
52
+ enabledFeatures: {},
53
+ },
54
+ });
55
+ const result = await hasFeature(accountId, FEATURES.APPS_HOME);
56
+ expect(result).toBe(true);
57
+ });
58
+ it('should respect explicit setting for APPS_HOME feature even when it defaults on', async () => {
59
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
60
+ data: {
61
+ enabledFeatures: {
62
+ [FEATURES.APPS_HOME]: false,
63
+ },
64
+ },
65
+ });
66
+ const result = await hasFeature(accountId, FEATURES.APPS_HOME);
67
+ expect(result).toBe(false);
68
+ });
69
+ it('should handle truthy values correctly', async () => {
70
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
71
+ data: {
72
+ enabledFeatures: {
73
+ 'feature-truthy': 'yes',
74
+ },
75
+ },
76
+ });
77
+ // @ts-expect-error test data
78
+ const truthyResult = await hasFeature(accountId, 'feature-truthy');
79
+ expect(truthyResult).toBe(true);
80
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
81
+ data: {
82
+ enabledFeatures: {
83
+ 'feature-number': 1,
84
+ },
85
+ },
86
+ });
87
+ // @ts-expect-error test data
88
+ const numberResult = await hasFeature(accountId, 'feature-number');
89
+ expect(numberResult).toBe(true);
90
+ });
91
+ it('should handle falsy values correctly', async () => {
92
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
93
+ data: {
94
+ enabledFeatures: {
95
+ 'feature-null': null,
96
+ },
97
+ },
98
+ });
99
+ // @ts-expect-error test data
100
+ const nullResult = await hasFeature(accountId, 'feature-null');
101
+ expect(nullResult).toBe(false);
102
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
103
+ data: {
104
+ enabledFeatures: {
105
+ 'feature-zero': 0,
106
+ },
107
+ },
108
+ });
109
+ // @ts-expect-error test data
110
+ const zeroResult = await hasFeature(accountId, 'feature-zero');
111
+ expect(zeroResult).toBe(false);
112
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
113
+ data: {
114
+ enabledFeatures: {
115
+ 'feature-empty': '',
116
+ },
117
+ },
118
+ });
119
+ // @ts-expect-error test data
120
+ const emptyResult = await hasFeature(accountId, 'feature-empty');
121
+ expect(emptyResult).toBe(false);
122
+ });
123
+ it('should propagate errors from fetchEnabledFeatures', async () => {
124
+ const error = new Error('API error');
125
+ mockedFetchEnabledFeatures.mockRejectedValueOnce(error);
126
+ await expect(hasFeature(accountId, FEATURES.UNIFIED_APPS)).rejects.toThrow('API error');
127
+ });
128
+ });
129
+ describe('hasUnfiedAppsAccess()', () => {
130
+ const accountId = 123;
131
+ afterEach(() => {
132
+ vi.clearAllMocks();
133
+ });
134
+ it('should return true when API returns true', async () => {
135
+ // @ts-expect-error Don't want to mock the full response object
136
+ mockedHttp.get.mockResolvedValueOnce({ data: true });
137
+ const result = await hasUnfiedAppsAccess(accountId);
138
+ expect(result).toBe(true);
139
+ expect(mockedHttp.get).toHaveBeenCalledWith(accountId, {
140
+ url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
141
+ });
142
+ });
143
+ it('should return false when API returns false', async () => {
144
+ // @ts-expect-error Don't want to mock the full response object
145
+ mockedHttp.get.mockResolvedValueOnce({ data: false });
146
+ const result = await hasUnfiedAppsAccess(accountId);
147
+ expect(result).toBe(false);
148
+ });
149
+ it('should handle truthy values correctly', async () => {
150
+ // @ts-expect-error Don't want to mock the full response object
151
+ mockedHttp.get.mockResolvedValueOnce({ data: 'yes' });
152
+ const result = await hasUnfiedAppsAccess(accountId);
153
+ expect(result).toBe(true);
154
+ });
155
+ it('should handle falsy values correctly', async () => {
156
+ // @ts-expect-error Don't want to mock the full response object
157
+ mockedHttp.get.mockResolvedValueOnce({ data: null });
158
+ const result = await hasUnfiedAppsAccess(accountId);
159
+ expect(result).toBe(false);
160
+ });
161
+ it('should handle undefined response data', async () => {
162
+ // @ts-expect-error Don't want to mock the full response object
163
+ mockedHttp.get.mockResolvedValueOnce({ data: undefined });
164
+ const result = await hasUnfiedAppsAccess(accountId);
165
+ expect(result).toBe(false);
166
+ });
167
+ it('should propagate errors from http.get', async () => {
168
+ const error = new Error('Network error');
169
+ mockedHttp.get.mockRejectedValueOnce(error);
170
+ await expect(hasUnfiedAppsAccess(accountId)).rejects.toThrow('Network error');
171
+ });
34
172
  });
35
173
  });
@@ -43,7 +43,7 @@ describe('lib/importData', () => {
43
43
  data: { id: '123' },
44
44
  });
45
45
  await handleImportData(targetAccountId, dataFileNames, importRequest);
46
- expect(mockUiLogger.success).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
46
+ expect(mockUiLogger.info).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
47
47
  });
48
48
  it('should log the correct error message', async () => {
49
49
  mockCreateImport.mockRejectedValue(new Error('test-error'));
@@ -22,7 +22,7 @@ import { hasUnfiedAppsAccess } from '../hasFeature.js';
22
22
  import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
23
23
  import { uiLogger } from '../ui/logger.js';
24
24
  import { debugError } from '../errorHandlers/index.js';
25
- import { useV3Api } from '../projects/buildAndDeploy.js';
25
+ import { useV3Api } from '../projects/platformVersion.js';
26
26
  export function getUnmigratableReason(reasonCode, projectName, accountId) {
27
27
  switch (reasonCode) {
28
28
  case UNMIGRATABLE_REASONS.UP_TO_DATE:
@@ -236,13 +236,20 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
236
236
  throw new Error(lib.migrate.errors.project.invalidConfig);
237
237
  }
238
238
  const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
239
+ let migrated = false;
240
+ let failureReason;
239
241
  try {
240
- await migrateThemes(projectConfig.projectDir, projectSrcDir);
242
+ const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
243
+ migrated = migrationResult.migrated;
244
+ failureReason = migrationResult.failureReason;
241
245
  }
242
246
  catch (error) {
243
247
  debugError(error);
244
248
  throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
245
249
  }
250
+ if (!migrated) {
251
+ throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
252
+ }
246
253
  const newProjectConfig = { ...projectConfig.projectConfig };
247
254
  newProjectConfig.platformVersion = platformVersion;
248
255
  const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
@@ -80,6 +80,8 @@ export declare const FEATURES: {
80
80
  readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
81
81
  readonly SANDBOXES_V2: "sandboxes:v2:enabled";
82
82
  readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
83
+ readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
84
+ readonly APPS_HOME: "UIE:AppHome";
83
85
  };
84
86
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
85
87
  UPLOAD_SUCCESS: string;
package/lib/constants.js CHANGED
@@ -72,6 +72,8 @@ export const FEATURES = {
72
72
  UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
73
73
  SANDBOXES_V2: 'sandboxes:v2:enabled',
74
74
  SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
75
+ APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
76
+ APPS_HOME: 'UIE:AppHome',
75
77
  };
76
78
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
77
79
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -12,3 +12,7 @@ export declare class ApiErrorContext {
12
12
  projectName?: string;
13
13
  });
14
14
  }
15
+ export declare function isErrorWithMessageOrReason(error: unknown): error is {
16
+ message?: string;
17
+ reason?: string;
18
+ };
@@ -85,7 +85,7 @@ export class ApiErrorContext {
85
85
  this.projectName = props.projectName || '';
86
86
  }
87
87
  }
88
- function isErrorWithMessageOrReason(error) {
88
+ export function isErrorWithMessageOrReason(error) {
89
89
  return (typeof error === 'object' &&
90
90
  error !== null &&
91
91
  ('message' in error || 'reason' in error));
package/lib/hasFeature.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { http } from '@hubspot/local-dev-lib/http';
2
2
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
3
+ import { FEATURES } from './constants.js';
4
+ const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
3
5
  export async function hasFeature(accountId, feature) {
4
6
  const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
7
+ if (enabledFeatures[feature] === undefined &&
8
+ FEATURES_THAT_DEFAULT_ON.includes(feature)) {
9
+ return true;
10
+ }
5
11
  return Boolean(enabledFeatures[feature]);
6
12
  }
7
13
  export async function hasUnfiedAppsAccess(accountId) {
package/lib/importData.js CHANGED
@@ -10,7 +10,7 @@ export async function handleImportData(targetAccountId, dataFileNames, importReq
10
10
  const baseUrl = getHubSpotWebsiteOrigin(getEnv());
11
11
  const response = await createImport(targetAccountId, importRequest, dataFileNames);
12
12
  const importId = response.data.id;
13
- uiLogger.success(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
13
+ uiLogger.info(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
14
14
  }
15
15
  catch (error) {
16
16
  uiLogger.error(lib.importData.errors.failedToImportData);
package/lib/mcp/setup.js CHANGED
@@ -8,7 +8,7 @@ import path from 'path';
8
8
  import os from 'os';
9
9
  import fs from 'fs-extra';
10
10
  import { existsSync } from 'fs';
11
- const mcpServerName = 'hubspot-cli-mcp';
11
+ const mcpServerName = 'HubSpot';
12
12
  const claudeCode = 'claude';
13
13
  const windsurf = 'windsurf';
14
14
  const cursor = 'cursor';
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
5
  export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
6
6
  export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
7
- export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined, useEnv?: boolean): Promise<number | undefined>;
@@ -38,7 +38,12 @@ export function loadProfile(projectConfig, projectDir, profileName) {
38
38
  uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
39
39
  return;
40
40
  }
41
- return profile;
41
+ return {
42
+ ...profile,
43
+ accountId: process.env.HUBSPOT_ACCOUNT_ID
44
+ ? Number(process.env.HUBSPOT_ACCOUNT_ID)
45
+ : profile.accountId,
46
+ };
42
47
  }
43
48
  catch (e) {
44
49
  uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
@@ -54,7 +59,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
54
59
  }
55
60
  }
56
61
  }
57
- export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
62
+ export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile, useEnv = false) {
58
63
  if (argsProfile) {
59
64
  logProfileHeader(argsProfile);
60
65
  const profile = loadProfile(projectConfig, projectDir, argsProfile);
@@ -63,6 +68,9 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
63
68
  process.exit(EXIT_CODES.ERROR);
64
69
  }
65
70
  logProfileFooter(profile, true);
71
+ if (useEnv) {
72
+ return Number(process.env.HUBSPOT_ACCOUNT_ID);
73
+ }
66
74
  return profile.accountId;
67
75
  }
68
76
  else {
@@ -240,16 +240,21 @@ describe('AppDevModeInterface', () => {
240
240
  await newAppDevModeInterface.setup({});
241
241
  expect(process.exit).toHaveBeenCalledWith(0);
242
242
  });
243
- it('should auto-install static auth app on test account', async () => {
244
- fetchAppInstallationData.mockResolvedValue({
245
- data: {
246
- isInstalledWithScopeGroups: false,
247
- previouslyAuthorizedScopeGroups: [],
248
- },
249
- });
250
- await appDevModeInterface.setup({});
251
- expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
252
- });
243
+ // @TODO: Restore test account auto install functionality
244
+ // it('should auto-install static auth app on test account', async () => {
245
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
246
+ // data: {
247
+ // isInstalledWithScopeGroups: false,
248
+ // previouslyAuthorizedScopeGroups: [],
249
+ // },
250
+ // });
251
+ // await appDevModeInterface.setup({});
252
+ // expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
253
+ // 123,
254
+ // 67890,
255
+ // [1, 2, 3]
256
+ // );
257
+ // });
253
258
  it('should open browser for OAuth app installation', async () => {
254
259
  const oauthAppNode = {
255
260
  ...mockAppNode,
@@ -288,7 +293,12 @@ describe('AppDevModeInterface', () => {
288
293
  },
289
294
  });
290
295
  await appDevModeInterface.setup({});
291
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
296
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
297
+ appUid: 'test-app-uid',
298
+ projectAccountId: 12345,
299
+ projectName: 'test-project',
300
+ testingAccountId: 67890,
301
+ });
292
302
  });
293
303
  it('should handle errors during setup', async () => {
294
304
  const error = new Error('Setup failed');
@@ -318,39 +328,46 @@ describe('AppDevModeInterface', () => {
318
328
  await appDevModeInterface.setup({});
319
329
  expect(process.exit).toHaveBeenCalledWith(1);
320
330
  });
321
- it('should exit if user declines auto-install', async () => {
322
- // Set up conditions for automatic installation
323
- getAccountConfig.mockReturnValue({
324
- parentAccountId: 12345, // matches targetProjectAccountId
325
- });
326
- isDeveloperTestAccount.mockReturnValue(true);
327
- fetchAppInstallationData.mockResolvedValue({
328
- data: {
329
- isInstalledWithScopeGroups: false,
330
- previouslyAuthorizedScopeGroups: [],
331
- },
332
- });
333
- installAppAutoPrompt.mockResolvedValue(false);
334
- // Create a new instance to trigger the exit during setup
335
- const newAppDevModeInterface = new AppDevModeInterface({
336
- localDevState: mockLocalDevState,
337
- localDevLogger: mockLocalDevLogger,
338
- });
339
- // The setup method catches the error, so we check that process.exit was called
340
- await newAppDevModeInterface.setup({});
341
- expect(process.exit).toHaveBeenCalledWith(0);
342
- });
343
- it('should fallback to browser install if auto-install fails', async () => {
344
- fetchAppInstallationData.mockResolvedValue({
345
- data: {
346
- isInstalledWithScopeGroups: false,
347
- previouslyAuthorizedScopeGroups: [],
348
- },
349
- });
350
- installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
351
- await appDevModeInterface.setup({});
352
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
353
- });
331
+ // @TODO: Restore test account auto install functionality
332
+ // it('should exit if user declines auto-install', async () => {
333
+ // // Set up conditions for automatic installation
334
+ // (getAccountConfig as Mock).mockReturnValue({
335
+ // parentAccountId: 12345, // matches targetProjectAccountId
336
+ // });
337
+ // (isDeveloperTestAccount as Mock).mockReturnValue(true);
338
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
339
+ // data: {
340
+ // isInstalledWithScopeGroups: false,
341
+ // previouslyAuthorizedScopeGroups: [],
342
+ // },
343
+ // });
344
+ // (installAppAutoPrompt as Mock).mockResolvedValue(false);
345
+ // // Create a new instance to trigger the exit during setup
346
+ // const newAppDevModeInterface = new AppDevModeInterface({
347
+ // localDevState: mockLocalDevState,
348
+ // localDevLogger: mockLocalDevLogger,
349
+ // });
350
+ // // The setup method catches the error, so we check that process.exit was called
351
+ // await newAppDevModeInterface.setup({});
352
+ // expect(process.exit).toHaveBeenCalledWith(0);
353
+ // });
354
+ // @TODO: Restore test account auto install functionality
355
+ // it('should fallback to browser install if auto-install fails', async () => {
356
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
357
+ // data: {
358
+ // isInstalledWithScopeGroups: false,
359
+ // previouslyAuthorizedScopeGroups: [],
360
+ // },
361
+ // });
362
+ // (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
363
+ // new Error('Install failed')
364
+ // );
365
+ // await appDevModeInterface.setup({});
366
+ // expect(installAppBrowserPrompt).toHaveBeenCalledWith(
367
+ // 'http://static-install-url',
368
+ // false
369
+ // );
370
+ // });
354
371
  });
355
372
  describe('start()', () => {
356
373
  it('should return early if no app node exists', async () => {