@hubspot/cli 7.7.32-experimental.0 → 7.7.33-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 (77) hide show
  1. package/commands/getStarted.js +5 -4
  2. package/commands/project/__tests__/add.test.js +3 -5
  3. package/commands/project/__tests__/deploy.test.js +3 -2
  4. package/commands/project/add.js +2 -4
  5. package/commands/project/deploy.js +9 -61
  6. package/commands/project/dev/index.js +1 -1
  7. package/commands/project/dev/unifiedFlow.js +3 -0
  8. package/commands/project/upload.js +2 -2
  9. package/commands/project/validate.js +1 -1
  10. package/commands/project/watch.js +2 -2
  11. package/lang/en.d.ts +7 -3
  12. package/lang/en.js +8 -4
  13. package/lib/__tests__/hasFeature.test.js +145 -7
  14. package/lib/__tests__/importData.test.js +1 -1
  15. package/lib/app/migrate.js +9 -2
  16. package/lib/constants.d.ts +2 -0
  17. package/lib/constants.js +2 -0
  18. package/lib/errorHandlers/index.d.ts +4 -0
  19. package/lib/errorHandlers/index.js +1 -1
  20. package/lib/hasFeature.js +6 -0
  21. package/lib/importData.js +1 -1
  22. package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
  23. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  24. package/lib/projects/__tests__/deploy.test.js +164 -0
  25. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  26. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  27. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  28. package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
  29. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  30. package/lib/projects/add/legacyAddComponent.js +5 -1
  31. package/lib/projects/add/v3AddComponent.d.ts +1 -0
  32. package/lib/projects/add/v3AddComponent.js +2 -2
  33. package/lib/projects/create/__tests__/v3.test.js +97 -9
  34. package/lib/projects/create/index.js +2 -2
  35. package/lib/projects/create/legacy.js +1 -1
  36. package/lib/projects/create/v3.d.ts +2 -2
  37. package/lib/projects/create/v3.js +35 -12
  38. package/lib/projects/deploy.d.ts +13 -0
  39. package/lib/projects/deploy.js +63 -0
  40. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
  41. package/lib/projects/localDev/AppDevModeInterface.js +65 -36
  42. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  43. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  44. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  45. package/lib/projects/localDev/LocalDevState.js +9 -1
  46. package/lib/projects/localDev/helpers/project.js +1 -1
  47. package/lib/projects/platformVersion.d.ts +1 -0
  48. package/lib/projects/platformVersion.js +10 -0
  49. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  50. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  51. package/lib/projects/upload.js +1 -1
  52. package/lib/projects/urls.d.ts +1 -0
  53. package/lib/projects/urls.js +3 -0
  54. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  55. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  56. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  57. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  58. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  59. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  60. package/lib/prompts/installAppPrompt.d.ts +6 -1
  61. package/lib/prompts/installAppPrompt.js +6 -1
  62. package/lib/prompts/projectAddPrompt.js +1 -1
  63. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  64. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  65. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  66. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  67. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  68. package/mcp-server/tools/index.js +2 -0
  69. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  70. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  71. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -3
  72. package/mcp-server/tools/project/constants.d.ts +1 -1
  73. package/mcp-server/tools/project/constants.js +6 -4
  74. package/package.json +3 -3
  75. package/types/LocalDev.d.ts +2 -1
  76. package/types/Projects.d.ts +1 -0
  77. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
@@ -16,7 +16,8 @@ import { handleProjectUpload } from '../lib/projects/upload.js';
16
16
  import { PROJECT_CONFIG_FILE, GET_STARTED_OPTIONS, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, } from '../lib/constants.js';
17
17
  import { writeProjectConfig, getProjectConfig, validateProjectConfig, } from '../lib/projects/config.js';
18
18
  import { getProjectPackageJsonLocations, installPackages, } from '../lib/dependencyManagement.js';
19
- import { pollProjectBuildAndDeploy, useV3Api, } from '../lib/projects/buildAndDeploy.js';
19
+ import { pollProjectBuildAndDeploy } from '../lib/projects/pollProjectBuildAndDeploy.js';
20
+ import { useV3Api } from '../lib/projects/platformVersion.js';
20
21
  import { openLink } from '../lib/links.js';
21
22
  import { getStaticAuthAppInstallUrl } from '../lib/app/urls.js';
22
23
  import { getEnv } from '@hubspot/local-dev-lib/config';
@@ -28,10 +29,12 @@ async function handler(args) {
28
29
  const { derivedAccountId } = args;
29
30
  const env = getEnv(derivedAccountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD;
30
31
  await trackCommandUsage('get-started', {}, derivedAccountId);
32
+ const accountName = uiAccountDescription(derivedAccountId);
31
33
  // TODO: Put this in constants.ts once we have a defined place for the template before INBOUND
32
34
  const templateSource = 'robrown-hubspot/hubspot-project-components-ua-app-objects-beta';
33
35
  uiInfoSection(commands.getStarted.startTitle, () => {
34
36
  uiLogger.log(commands.getStarted.startDescription);
37
+ uiLogger.log(commands.getStarted.guideOverview(accountName));
35
38
  });
36
39
  const { default: selectedOption } = await promptUser([
37
40
  {
@@ -163,7 +166,6 @@ async function handler(args) {
163
166
  uiLogger.log(' ');
164
167
  }
165
168
  // 6. Ask user if they want to upload the project
166
- const accountName = uiAccountDescription(derivedAccountId);
167
169
  const { shouldUpload } = await promptUser([
168
170
  {
169
171
  type: 'confirm',
@@ -192,12 +194,11 @@ async function handler(args) {
192
194
  process.exit(EXIT_CODES.ERROR);
193
195
  }
194
196
  validateProjectConfig(newProjectConfig, newProjectDir);
195
- const targetAccountId = derivedAccountId;
196
197
  uiLogger.log(' ');
197
198
  uiLogger.log(commands.getStarted.logs.uploadingProject);
198
199
  uiLogger.log(' ');
199
200
  const { result, uploadError } = await handleProjectUpload({
200
- accountId: targetAccountId,
201
+ accountId: derivedAccountId,
201
202
  projectConfig: newProjectConfig,
202
203
  projectDir: newProjectDir,
203
204
  callbackFunc: pollProjectBuildAndDeploy,
@@ -4,13 +4,13 @@ import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from
4
4
  import { v3AddComponent } from '../../../lib/projects/add/v3AddComponent.js';
5
5
  import { legacyAddComponent } from '../../../lib/projects/add/legacyAddComponent.js';
6
6
  import { getProjectConfig } from '../../../lib/projects/config.js';
7
- import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
7
+ import { useV3Api } from '../../../lib/projects/platformVersion.js';
8
8
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
9
9
  vi.mock('../../../lib/commonOpts');
10
10
  vi.mock('../../../lib/projects/add/v3AddComponent');
11
11
  vi.mock('../../../lib/projects/add/legacyAddComponent');
12
12
  vi.mock('../../../lib/projects/config');
13
- vi.mock('../../../lib/projects/buildAndDeploy');
13
+ vi.mock('../../../lib/projects/platformVersion');
14
14
  vi.mock('../../../lib/usageTracking');
15
15
  const mockedV3AddComponent = vi.mocked(v3AddComponent);
16
16
  const mockedLegacyAddComponent = vi.mocked(legacyAddComponent);
@@ -82,15 +82,13 @@ describe('commands/project/add', () => {
82
82
  it('should call v3AddComponent with accountId for v3 projects', async () => {
83
83
  mockedUseV3Api.mockReturnValue(true);
84
84
  await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
85
- expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
86
85
  expect(mockedV3AddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
87
86
  expect(mockedLegacyAddComponent).not.toHaveBeenCalled();
88
87
  });
89
88
  it('should call legacyAddComponent for non-v3 projects', async () => {
90
89
  mockedUseV3Api.mockReturnValue(false);
91
90
  await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
92
- expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
93
- expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig);
91
+ expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
94
92
  expect(mockedV3AddComponent).not.toHaveBeenCalled();
95
93
  });
96
94
  it('should exit with error when project config is not found', async () => {
@@ -8,7 +8,7 @@ import * as ui from '../../../lib/ui/index.js';
8
8
  import { addAccountOptions, addConfigOptions, addJSONOutputOptions, addUseEnvironmentOptions, } from '../../../lib/commonOpts.js';
9
9
  import * as projectUtils from '../../../lib/projects/config.js';
10
10
  import * as projectUrlUtils from '../../../lib/projects/urls.js';
11
- import { pollDeployStatus } from '../../../lib/projects/buildAndDeploy.js';
11
+ import { pollDeployStatus } from '../../../lib/projects/pollProjectBuildAndDeploy.js';
12
12
  import * as projectNamePrompt from '../../../lib/prompts/projectNamePrompt.js';
13
13
  import * as promptUtils from '../../../lib/prompts/promptUtils.js';
14
14
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
@@ -23,7 +23,8 @@ vi.mock('../../../lib/commonOpts');
23
23
  vi.mock('../../../lib/validation');
24
24
  vi.mock('../../../lib/projects/config');
25
25
  vi.mock('../../../lib/projects/urls');
26
- vi.mock('../../../lib/projects/buildAndDeploy');
26
+ vi.mock('../../../lib/projects/pollProjectBuildAndDeploy');
27
+ vi.mock('../../../lib/projects/platformVersion');
27
28
  vi.mock('../../../lib/prompts/projectNamePrompt');
28
29
  vi.mock('../../../lib/prompts/promptUtils');
29
30
  vi.mock('../../../lib/usageTracking');
@@ -1,11 +1,10 @@
1
1
  import { logError } from '../../lib/errorHandlers/index.js';
2
- import { trackCommandUsage } from '../../lib/usageTracking.js';
3
2
  import { getProjectConfig } from '../../lib/projects/config.js';
4
3
  import { uiBetaTag } from '../../lib/ui/index.js';
5
4
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
6
5
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
7
6
  import { commands } from '../../lang/en.js';
8
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
7
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
9
8
  import { legacyAddComponent } from '../../lib/projects/add/legacyAddComponent.js';
10
9
  import { v3AddComponent } from '../../lib/projects/add/v3AddComponent.js';
11
10
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../lib/constants.js';
@@ -15,7 +14,6 @@ const describe = uiBetaTag(commands.project.add.describe, false);
15
14
  async function handler(args) {
16
15
  try {
17
16
  const { derivedAccountId } = args;
18
- trackCommandUsage('project-add', undefined, derivedAccountId);
19
17
  const { projectConfig, projectDir } = await getProjectConfig();
20
18
  if (!projectDir || !projectConfig) {
21
19
  uiLogger.error(commands.project.add.error.locationInProject);
@@ -26,7 +24,7 @@ async function handler(args) {
26
24
  await v3AddComponent(args, projectDir, projectConfig, derivedAccountId);
27
25
  }
28
26
  else {
29
- await legacyAddComponent(args, projectDir, projectConfig);
27
+ await legacyAddComponent(args, projectDir, projectConfig, derivedAccountId);
30
28
  }
31
29
  }
32
30
  catch (e) {
@@ -1,11 +1,10 @@
1
- import { deployProject, fetchProject, } from '@hubspot/local-dev-lib/api/projects';
1
+ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
2
2
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3
3
  import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
4
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
4
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
5
5
  import { trackCommandUsage } from '../../lib/usageTracking.js';
6
6
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
7
7
  import { getProjectConfig } from '../../lib/projects/config.js';
8
- import { pollDeployStatus } from '../../lib/projects/buildAndDeploy.js';
9
8
  import { projectNamePrompt } from '../../lib/prompts/projectNamePrompt.js';
10
9
  import { promptUser } from '../../lib/prompts/promptUtils.js';
11
10
  import { uiBetaTag, uiLine } from '../../lib/ui/index.js';
@@ -13,54 +12,11 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
13
12
  import { uiLogger } from '../../lib/ui/logger.js';
14
13
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
15
14
  import { loadProfile, logProfileFooter, logProfileHeader, exitIfUsingProfiles, } from '../../lib/projectProfiles.js';
16
- import { PROJECT_ERROR_TYPES, PROJECT_DEPLOY_TEXT, } from '../../lib/constants.js';
15
+ import { PROJECT_DEPLOY_TEXT } from '../../lib/constants.js';
17
16
  import { commands } from '../../lang/en.js';
17
+ import { handleProjectDeploy, validateBuildIdForDeploy, logDeployErrors, } from '../../lib/projects/deploy.js';
18
18
  const command = 'deploy';
19
19
  const describe = uiBetaTag(commands.project.deploy.describe, false);
20
- function validateBuildId(buildId, deployedBuildId, latestBuildId, projectName, accountId) {
21
- if (Number(buildId) > latestBuildId) {
22
- return commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName);
23
- }
24
- if (Number(buildId) === deployedBuildId) {
25
- return commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName);
26
- }
27
- return true;
28
- }
29
- function logDeployErrors(errorData) {
30
- uiLogger.error(errorData.message);
31
- errorData.errors.forEach(err => {
32
- // This is how the pre-deploy check manifests itself in < 2025.2 projects
33
- if (err.subCategory === PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS) {
34
- uiLogger.log(commands.project.deploy.errors.deployContainsRemovals(err.context.COMPONENT_NAME));
35
- }
36
- else {
37
- uiLogger.log(err.message);
38
- }
39
- });
40
- }
41
- function handleBlockedDeploy(deployResp) {
42
- const deployCanBeForced = deployResp.issues.every(issue => issue.blockingMessages.every(message => message.isWarning));
43
- uiLogger.log('');
44
- if (deployCanBeForced) {
45
- uiLogger.warn(commands.project.deploy.errors.deployWarningsHeader);
46
- uiLogger.log('');
47
- }
48
- else {
49
- uiLogger.error(commands.project.deploy.errors.deployBlockedHeader);
50
- uiLogger.log('');
51
- }
52
- deployResp.issues.forEach(issue => {
53
- if (issue.blockingMessages.length > 0) {
54
- issue.blockingMessages.forEach(message => {
55
- uiLogger.log(commands.project.deploy.errors.deployIssueComponentWarning(issue.uid, issue.componentTypeName, message.message));
56
- });
57
- }
58
- else {
59
- uiLogger.log(commands.project.deploy.errors.deployIssueComponentGeneric(issue.uid, issue.componentTypeName));
60
- }
61
- uiLogger.log('');
62
- });
63
- }
64
20
  async function handler(args) {
65
21
  const { derivedAccountId, project: projectOption, buildId: buildIdOption, force: forceOption, deployLatestBuild: deployLatestBuildOption, json: formatOutputAsJson, } = args;
66
22
  const accountConfig = getAccountConfig(derivedAccountId);
@@ -105,7 +61,7 @@ async function handler(args) {
105
61
  return process.exit(EXIT_CODES.ERROR);
106
62
  }
107
63
  if (buildIdToDeploy) {
108
- const validationResult = validateBuildId(buildIdToDeploy, deployedBuildId, latestBuild.buildId, projectName, targetAccountId);
64
+ const validationResult = validateBuildIdForDeploy(buildIdToDeploy, deployedBuildId, latestBuild.buildId, projectName, targetAccountId);
109
65
  if (validationResult !== true) {
110
66
  uiLogger.error(validationResult.toString());
111
67
  return process.exit(EXIT_CODES.ERROR);
@@ -122,7 +78,7 @@ async function handler(args) {
122
78
  default: latestBuild.buildId === deployedBuildId
123
79
  ? undefined
124
80
  : latestBuild.buildId,
125
- validate: buildId => validateBuildId(buildId, deployedBuildId, latestBuild.buildId, projectName, targetAccountId),
81
+ validate: buildId => validateBuildIdForDeploy(buildId, deployedBuildId, latestBuild.buildId, projectName, targetAccountId),
126
82
  });
127
83
  buildIdToDeploy = deployBuildIdPromptResponse.buildId;
128
84
  }
@@ -131,21 +87,13 @@ async function handler(args) {
131
87
  uiLogger.error(commands.project.deploy.errors.noBuildId);
132
88
  return process.exit(EXIT_CODES.ERROR);
133
89
  }
134
- const { data: deployResp } = await deployProject(targetAccountId, projectName, buildIdToDeploy, useV3Api(projectConfig?.platformVersion), forceOption);
135
- if (!deployResp || deployResp.buildResultType !== 'DEPLOY_QUEUED') {
136
- if (deployResp?.buildResultType === 'DEPLOY_BLOCKED') {
137
- handleBlockedDeploy(deployResp);
138
- process.exit(EXIT_CODES.ERROR);
139
- }
140
- else {
141
- uiLogger.error(commands.project.deploy.errors.deploy);
142
- }
90
+ const deployResult = await handleProjectDeploy(targetAccountId, projectName, buildIdToDeploy, useV3Api(projectConfig?.platformVersion), forceOption);
91
+ if (!deployResult) {
143
92
  return process.exit(EXIT_CODES.ERROR);
144
93
  }
145
94
  else if (formatOutputAsJson) {
146
- jsonOutput.deployId = Number(deployResp.id);
95
+ jsonOutput.deployId = deployResult.deployId;
147
96
  }
148
- const deployResult = await pollDeployStatus(targetAccountId, projectName, Number(deployResp.id), buildIdToDeploy);
149
97
  if (deployResult.status === PROJECT_DEPLOY_TEXT.STATES.SUCCESS) {
150
98
  deploySuccess = true;
151
99
  }
@@ -5,7 +5,7 @@ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
5
5
  import { uiBetaTag, uiLine } from '../../../lib/ui/index.js';
6
6
  import { deprecatedProjectDevFlow } from './deprecatedFlow.js';
7
7
  import { unifiedProjectDevFlow } from './unifiedFlow.js';
8
- import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
8
+ import { useV3Api } from '../../../lib/projects/platformVersion.js';
9
9
  import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
10
10
  import { loadProfile, exitIfUsingProfiles, } from '../../../lib/projectProfiles.js';
11
11
  import { commands } from '../../../lang/en.js';
@@ -22,6 +22,7 @@ import LocalDevWebsocketServer from '../../../lib/projects/localDev/LocalDevWebs
22
22
  export async function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, }) {
23
23
  const env = getValidEnv(getEnv(targetProjectAccountId));
24
24
  let projectNodes;
25
+ let projectProfileData;
25
26
  // Get IR
26
27
  try {
27
28
  const intermediateRepresentation = await translateForLocalDev({
@@ -30,6 +31,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
30
31
  accountId: targetProjectAccountId,
31
32
  }, { profile: args.profile });
32
33
  projectNodes = intermediateRepresentation.intermediateNodesIndexedByUid;
34
+ projectProfileData = intermediateRepresentation.profileData;
33
35
  uiLogger.debug(util.inspect(projectNodes, false, null, true));
34
36
  }
35
37
  catch (e) {
@@ -116,6 +118,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
116
118
  // End setup, start local dev process
117
119
  const localDevProcess = new LocalDevProcess({
118
120
  initialProjectNodes: projectNodes,
121
+ initialProjectProfileData: projectProfileData,
119
122
  debug: args.debug,
120
123
  profile: args.profile,
121
124
  targetProjectAccountId,
@@ -2,14 +2,14 @@ import chalk from 'chalk';
2
2
  import { logger } from '@hubspot/local-dev-lib/logger';
3
3
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
4
4
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
5
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
5
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
6
6
  import { uiBetaTag, uiCommandReference } from '../../lib/ui/index.js';
7
7
  import { trackCommandUsage } from '../../lib/usageTracking.js';
8
8
  import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
9
9
  import { logFeedbackMessage } from '../../lib/projects/ui.js';
10
10
  import { handleProjectUpload } from '../../lib/projects/upload.js';
11
11
  import { loadAndValidateProfile } from '../../lib/projectProfiles.js';
12
- import { displayWarnLogs, pollProjectBuildAndDeploy, } from '../../lib/projects/buildAndDeploy.js';
12
+ import { displayWarnLogs, pollProjectBuildAndDeploy, } from '../../lib/projects/pollProjectBuildAndDeploy.js';
13
13
  import { i18n } from '../../lib/lang.js';
14
14
  import { PROJECT_ERROR_TYPES } from '../../lib/constants.js';
15
15
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
3
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
4
4
  import { trackCommandUsage } from '../../lib/usageTracking.js';
5
5
  import { uiLogger } from '../../lib/ui/logger.js';
6
6
  import { getProjectConfig, validateProjectConfig as validateProjectConfig, } from '../../lib/projects/config.js';
@@ -1,6 +1,6 @@
1
1
  import { cancelStagedBuild, fetchProjectBuilds, } from '@hubspot/local-dev-lib/api/projects';
2
2
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
3
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
3
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
4
4
  import { uiCommandReference, uiLink, uiBetaTag } from '../../lib/ui/index.js';
5
5
  import { i18n } from '../../lib/lang.js';
6
6
  import { createWatcher } from '../../lib/projects/watch.js';
@@ -11,7 +11,7 @@ import { trackCommandUsage } from '../../lib/usageTracking.js';
11
11
  import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
12
12
  import { logFeedbackMessage } from '../../lib/projects/ui.js';
13
13
  import { handleProjectUpload } from '../../lib/projects/upload.js';
14
- import { pollBuildStatus, pollDeployStatus, } from '../../lib/projects/buildAndDeploy.js';
14
+ import { pollBuildStatus, pollDeployStatus, } from '../../lib/projects/pollProjectBuildAndDeploy.js';
15
15
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
16
16
  import { handleKeypress, handleExit } from '../../lib/process.js';
17
17
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
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.";
@@ -3160,8 +3163,9 @@ Run ${string} to upgrade to version ${string}`;
3160
3163
  };
3161
3164
  };
3162
3165
  readonly installAppPrompt: {
3163
- readonly explanation: "Local development requires this app to be installed in the target test account";
3166
+ readonly explanation: "Local development requires this app to be installed in the target test account.";
3164
3167
  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.";
3168
+ readonly staticAuthExplanation: (projectAccountId: number, testingAccountId: number, projectName: string, appUid: string) => string;
3165
3169
  readonly prompt: "Open HubSpot to install this app?";
3166
3170
  readonly autoPrompt: "Install this app in your target test account?";
3167
3171
  readonly reinstallPrompt: "Open HubSpot to reinstall this app?";
@@ -3397,7 +3401,7 @@ Run ${string} to upgrade to version ${string}`;
3397
3401
  readonly themesAndAppsNotAllowed: "Support for migrating projects containing both themes and apps to the latest platform version is coming soon. Try again later.";
3398
3402
  readonly multipleApps: "Multiple apps found in project, this is not allowed in 2025.2";
3399
3403
  readonly alreadyExists: (projectName: string) => string;
3400
- readonly failedToMigrateThemes: "Failed to migrate project themes. Please verify that you have propoerly formatted themes before trying again.";
3404
+ readonly failedToMigrateThemes: "Failed to migrate project themes. Please verify that your themes are properly formatted before trying again.";
3401
3405
  readonly failedToUpdateProjectConfig: "Failed to update project config file. Please update the platformVersion in the project config file manually.";
3402
3406
  };
3403
3407
  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.',
@@ -3154,8 +3157,9 @@ export const lib = {
3154
3157
  },
3155
3158
  },
3156
3159
  installAppPrompt: {
3157
- explanation: 'Local development requires this app to be installed in the target test account',
3160
+ explanation: 'Local development requires this app to be installed in the target test account.',
3158
3161
  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.",
3162
+ 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
3163
  prompt: 'Open HubSpot to install this app?',
3160
3164
  autoPrompt: 'Install this app in your target test account?',
3161
3165
  reinstallPrompt: 'Open HubSpot to reinstall this app?',
@@ -3391,7 +3395,7 @@ export const lib = {
3391
3395
  themesAndAppsNotAllowed: 'Support for migrating projects containing both themes and apps to the latest platform version is coming soon. Try again later.',
3392
3396
  multipleApps: 'Multiple apps found in project, this is not allowed in 2025.2',
3393
3397
  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.',
3398
+ failedToMigrateThemes: 'Failed to migrate project themes. Please verify that your themes are properly formatted before trying again.',
3395
3399
  failedToUpdateProjectConfig: 'Failed to update project config file. Please update the platformVersion in the project config file manually.',
3396
3400
  },
3397
3401
  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'));