@hubspot/cli 7.7.27-experimental.2 → 7.7.28-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 (131) hide show
  1. package/README.md +0 -4
  2. package/api/__tests__/migrate.test.js +5 -5
  3. package/api/migrate.d.ts +10 -4
  4. package/api/migrate.js +2 -2
  5. package/commands/__tests__/create.test.js +20 -0
  6. package/commands/__tests__/testAccount.test.js +2 -0
  7. package/commands/app/__tests__/migrate.test.js +1 -0
  8. package/commands/create/function.js +2 -2
  9. package/commands/create/module.js +2 -2
  10. package/commands/create/template.js +2 -2
  11. package/commands/create.js +47 -0
  12. package/commands/getStarted.js +66 -4
  13. package/commands/mcp/setup.d.ts +0 -1
  14. package/commands/mcp/setup.js +3 -11
  15. package/commands/project/__tests__/create.test.js +57 -0
  16. package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
  17. package/commands/project/create.js +6 -1
  18. package/commands/project/deploy.js +31 -1
  19. package/commands/project/dev/deprecatedFlow.js +2 -1
  20. package/commands/project/dev/index.js +32 -12
  21. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  22. package/commands/project/dev/unifiedFlow.js +10 -16
  23. package/commands/project/profile/delete.js +26 -14
  24. package/commands/project/upload.d.ts +2 -2
  25. package/commands/project/upload.js +1 -1
  26. package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
  27. package/commands/testAccount/__tests__/importData.test.js +93 -0
  28. package/commands/testAccount/create.js +23 -13
  29. package/commands/testAccount/importData.d.ts +9 -0
  30. package/commands/testAccount/importData.js +61 -0
  31. package/commands/testAccount.js +2 -0
  32. package/lang/en.d.ts +160 -46
  33. package/lang/en.js +175 -59
  34. package/lang/en.lyaml +35 -14
  35. package/lib/__tests__/importData.test.d.ts +1 -0
  36. package/lib/__tests__/importData.test.js +89 -0
  37. package/lib/accountTypes.js +2 -3
  38. package/lib/app/__tests__/migrate.test.js +81 -36
  39. package/lib/app/migrate.d.ts +17 -4
  40. package/lib/app/migrate.js +97 -19
  41. package/lib/constants.d.ts +1 -0
  42. package/lib/constants.js +1 -0
  43. package/lib/hasFeature.d.ts +1 -0
  44. package/lib/hasFeature.js +7 -0
  45. package/lib/importData.d.ts +3 -0
  46. package/lib/importData.js +50 -0
  47. package/lib/mcp/setup.d.ts +0 -2
  48. package/lib/mcp/setup.js +0 -24
  49. package/lib/process.js +15 -4
  50. package/lib/projectProfiles.d.ts +1 -1
  51. package/lib/projectProfiles.js +10 -2
  52. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
  53. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
  54. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  55. package/lib/projects/__tests__/components.test.js +164 -7
  56. package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
  57. package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
  58. package/lib/projects/add/v3AddComponent.js +16 -4
  59. package/lib/projects/components.d.ts +1 -0
  60. package/lib/projects/components.js +27 -1
  61. package/lib/projects/localDev/AppDevModeInterface.js +35 -3
  62. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
  63. package/lib/projects/localDev/LocalDevLogger.js +2 -19
  64. package/lib/projects/localDev/LocalDevManager.js +1 -1
  65. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
  66. package/lib/projects/localDev/LocalDevProcess.js +3 -26
  67. package/lib/projects/localDev/LocalDevState.d.ts +6 -7
  68. package/lib/projects/localDev/LocalDevState.js +16 -15
  69. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  70. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  71. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
  72. package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
  73. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  74. package/lib/projects/localDev/helpers/project.js +173 -0
  75. package/lib/projects/urls.d.ts +1 -0
  76. package/lib/projects/urls.js +4 -0
  77. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  78. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  79. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  80. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  81. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  82. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  83. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  84. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  85. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  86. package/lib/prompts/createFunctionPrompt.js +36 -7
  87. package/lib/prompts/createModulePrompt.d.ts +2 -1
  88. package/lib/prompts/createModulePrompt.js +48 -1
  89. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  90. package/lib/prompts/createTemplatePrompt.js +9 -1
  91. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  92. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  93. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  94. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  95. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  96. package/lib/prompts/promptUtils.d.ts +7 -1
  97. package/lib/prompts/promptUtils.js +14 -1
  98. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  99. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  100. package/lib/ui/index.js +3 -6
  101. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  102. package/lib/ui/removeAnsiCodes.js +4 -0
  103. package/mcp-server/server.js +2 -1
  104. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  105. package/mcp-server/tools/cms/HsListTool.js +58 -0
  106. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  107. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  108. package/mcp-server/tools/index.d.ts +1 -0
  109. package/mcp-server/tools/index.js +8 -0
  110. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  111. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  112. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  113. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  114. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  115. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  116. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  117. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  118. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  119. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  120. package/mcp-server/tools/project/constants.d.ts +2 -0
  121. package/mcp-server/tools/project/constants.js +6 -0
  122. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  123. package/mcp-server/utils/toolUsageTracking.js +2 -1
  124. package/package.json +9 -6
  125. package/types/Cms.d.ts +16 -0
  126. package/types/Cms.js +25 -1
  127. package/types/LocalDev.d.ts +0 -3
  128. package/types/Prompts.d.ts +1 -0
  129. package/types/Yargs.d.ts +1 -1
  130. package/ui/index.d.ts +1 -0
  131. package/ui/index.js +6 -0
@@ -1,41 +1,44 @@
1
- import { HUBSPOT_ACCOUNT_TYPES, HUBSPOT_ACCOUNT_TYPE_STRINGS, } from '@hubspot/local-dev-lib/constants/config';
2
- import { isMissingScopeError, isSpecifiedError, } from '@hubspot/local-dev-lib/errors/index';
1
+ import { HUBSPOT_ACCOUNT_TYPE_STRINGS } from '@hubspot/local-dev-lib/constants/config';
2
+ import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3
+ import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
4
+ import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
3
5
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
4
- import { getAccountConfig, getEnv } from '@hubspot/local-dev-lib/config';
5
- import { createProject } from '@hubspot/local-dev-lib/api/projects';
6
- import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
6
+ import { isMissingScopeError } from '@hubspot/local-dev-lib/errors/index';
7
7
  import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
8
- import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
9
8
  import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
10
- import { confirmDefaultAccountPrompt, selectSandboxTargetAccountPrompt, selectDeveloperTestTargetAccountPrompt, confirmUseExistingDeveloperTestAccountPrompt, } from '../../prompts/projectDevTargetAccountPrompt.js';
11
- import { confirmPrompt, listPrompt } from '../../prompts/promptUtils.js';
12
- import { validateSandboxUsageLimits, getAvailableSyncTypes, } from '../../sandboxes.js';
13
- import { syncSandbox } from '../../sandboxSync.js';
14
- import { validateDevTestAccountUsageLimits } from '../../developerTestAccounts.js';
15
- import { uiLine, uiAccountDescription } from '../../ui/index.js';
16
- import SpinniesManager from '../../ui/SpinniesManager.js';
17
- import { EXIT_CODES } from '../../enums/exitCodes.js';
18
- import { trackCommandMetadataUsage } from '../../usageTracking.js';
19
- import { isAppDeveloperAccount, isDeveloperTestAccount, isUnifiedAccount, } from '../../accountTypes.js';
20
- import { handleProjectUpload } from '../../projects/upload.js';
21
- import { pollProjectBuildAndDeploy } from '../../projects/buildAndDeploy.js';
22
- import { PROJECT_ERROR_TYPES, PROJECT_BUILD_TEXT, PROJECT_DEPLOY_TEXT, } from '../../constants.js';
23
- import { logError, ApiErrorContext, debugError, } from '../../errorHandlers/index.js';
24
- import { buildSandbox, buildDeveloperTestAccount, saveAccountToConfig, } from '../../buildAccount.js';
25
- import { hubspotAccountNamePrompt } from '../../prompts/accountNamePrompt.js';
26
- import { lib } from '../../../lang/en.js';
27
- import { uiLogger } from '../../ui/logger.js';
9
+ import { uiLogger } from '../../../ui/logger.js';
10
+ import { lib } from '../../../../lang/en.js';
11
+ import { EXIT_CODES } from '../../../enums/exitCodes.js';
12
+ import { confirmDefaultAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
13
+ import { isUnifiedAccount } from '../../../accountTypes.js';
14
+ import { isAppDeveloperAccount } from '../../../accountTypes.js';
15
+ import { isDeveloperTestAccount } from '../../../accountTypes.js';
16
+ import { uiAccountDescription } from '../../../ui/index.js';
17
+ import { uiLine } from '../../../ui/index.js';
18
+ import { selectDeveloperTestTargetAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
19
+ import { selectSandboxTargetAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
20
+ import { validateSandboxUsageLimits } from '../../../sandboxes.js';
21
+ import { logError } from '../../../errorHandlers/index.js';
22
+ import { syncSandbox } from '../../../sandboxSync.js';
23
+ import { getAvailableSyncTypes } from '../../../sandboxes.js';
24
+ import { hubspotAccountNamePrompt } from '../../../prompts/accountNamePrompt.js';
25
+ import { trackCommandMetadataUsage } from '../../../usageTracking.js';
26
+ import { validateDevTestAccountUsageLimits } from '../../../developerTestAccounts.js';
27
+ import { buildSandbox, buildDeveloperTestAccount, saveAccountToConfig, } from '../../../buildAccount.js';
28
+ import { debugError } from '../../../errorHandlers/index.js';
29
+ import { listPrompt } from '../../../prompts/promptUtils.js';
30
+ import { confirmUseExistingDeveloperTestAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
28
31
  // If the user passed in the --account flag, confirm they want to use that account as
29
32
  // their target account, otherwise exit
30
33
  export async function confirmDefaultAccountIsTarget(accountConfig) {
31
34
  if (!accountConfig.name || !accountConfig.accountType) {
32
- uiLogger.error(lib.localDevHelpers.confirmDefaultAccountIsTarget.configError);
35
+ uiLogger.error(lib.localDevHelpers.account.confirmDefaultAccountIsTarget.configError);
33
36
  process.exit(EXIT_CODES.ERROR);
34
37
  }
35
38
  uiLogger.log('');
36
39
  const useDefaultAccount = await confirmDefaultAccountPrompt(accountConfig.name, HUBSPOT_ACCOUNT_TYPE_STRINGS[accountConfig.accountType]);
37
40
  if (!useDefaultAccount) {
38
- uiLogger.log(lib.localDevHelpers.confirmDefaultAccountIsTarget
41
+ uiLogger.log(lib.localDevHelpers.account.confirmDefaultAccountIsTarget
39
42
  .declineDefaultAccountExplanation);
40
43
  process.exit(EXIT_CODES.SUCCESS);
41
44
  }
@@ -47,18 +50,18 @@ export async function checkIfDefaultAccountIsSupported(accountConfig, hasPublicA
47
50
  !(isAppDeveloperAccount(accountConfig) ||
48
51
  isDeveloperTestAccount(accountConfig) ||
49
52
  defaultAccountIsUnified)) {
50
- uiLogger.error(lib.localDevHelpers.checkIfDefaultAccountIsSupported.publicApp);
53
+ uiLogger.error(lib.localDevHelpers.account.checkIfDefaultAccountIsSupported.publicApp);
51
54
  process.exit(EXIT_CODES.SUCCESS);
52
55
  }
53
56
  else if (!hasPublicApps && isAppDeveloperAccount(accountConfig)) {
54
- uiLogger.error(lib.localDevHelpers.checkIfDefaultAccountIsSupported.privateApp);
57
+ uiLogger.error(lib.localDevHelpers.account.checkIfDefaultAccountIsSupported.privateApp);
55
58
  process.exit(EXIT_CODES.SUCCESS);
56
59
  }
57
60
  }
58
61
  export function checkIfParentAccountIsAuthed(accountConfig) {
59
62
  if (!accountConfig.parentAccountId ||
60
63
  !getAccountConfig(accountConfig.parentAccountId)) {
61
- uiLogger.error(lib.localDevHelpers.checkIfParentAccountIsAuthed.notAuthedError(accountConfig.parentAccountId || '', uiAccountDescription(getAccountIdentifier(accountConfig))));
64
+ uiLogger.error(lib.localDevHelpers.account.checkIfParentAccountIsAuthed.notAuthedError(accountConfig.parentAccountId || '', uiAccountDescription(getAccountIdentifier(accountConfig))));
62
65
  process.exit(EXIT_CODES.SUCCESS);
63
66
  }
64
67
  }
@@ -66,13 +69,14 @@ export function checkIfParentAccountIsAuthed(accountConfig) {
66
69
  export function checkIfAccountFlagIsSupported(accountConfig, hasPublicApps) {
67
70
  if (hasPublicApps) {
68
71
  if (!isDeveloperTestAccount(accountConfig)) {
69
- uiLogger.error(lib.localDevHelpers.validateAccountOption.invalidPublicAppAccount);
72
+ uiLogger.error(lib.localDevHelpers.account.validateAccountOption
73
+ .invalidPublicAppAccount);
70
74
  process.exit(EXIT_CODES.SUCCESS);
71
75
  }
72
76
  checkIfParentAccountIsAuthed(accountConfig);
73
77
  }
74
78
  else if (isAppDeveloperAccount(accountConfig)) {
75
- uiLogger.error(lib.localDevHelpers.validateAccountOption.invalidPrivateAppAccount);
79
+ uiLogger.error(lib.localDevHelpers.account.validateAccountOption.invalidPrivateAppAccount);
76
80
  process.exit(EXIT_CODES.SUCCESS);
77
81
  }
78
82
  }
@@ -81,11 +85,11 @@ export async function suggestRecommendedNestedAccount(accounts, accountConfig, h
81
85
  uiLogger.log('');
82
86
  uiLine();
83
87
  if (hasPublicApps) {
84
- uiLogger.log(lib.localDevHelpers.validateAccountOption
88
+ uiLogger.log(lib.localDevHelpers.account.validateAccountOption
85
89
  .publicAppNonDeveloperTestAccountWarning);
86
90
  }
87
91
  else {
88
- uiLogger.log(lib.localDevHelpers.validateAccountOption.nonSandboxWarning);
92
+ uiLogger.log(lib.localDevHelpers.account.validateAccountOption.nonSandboxWarning);
89
93
  }
90
94
  uiLine();
91
95
  uiLogger.log('');
@@ -177,7 +181,7 @@ export async function useExistingDevTestAccount(env, account) {
177
181
  const useExistingDevTestAcct = await confirmUseExistingDeveloperTestAccountPrompt(account);
178
182
  if (!useExistingDevTestAcct) {
179
183
  uiLogger.log('');
180
- uiLogger.log(lib.localDevHelpers.confirmDefaultAccountIsTarget
184
+ uiLogger.log(lib.localDevHelpers.account.confirmDefaultAccountIsTarget
181
185
  .declineDefaultAccountExplanation);
182
186
  uiLogger.log('');
183
187
  process.exit(EXIT_CODES.SUCCESS);
@@ -185,110 +189,6 @@ export async function useExistingDevTestAccount(env, account) {
185
189
  const devTestAcctConfigName = await saveAccountToConfig(account.id, account.accountName, env);
186
190
  uiLogger.success(lib.developerTestAccount.create.success.configFileUpdated(devTestAcctConfigName, PERSONAL_ACCESS_KEY_AUTH_METHOD.name));
187
191
  }
188
- // Prompt the user to create a new project if one doesn't exist on their target account
189
- export async function createNewProjectForLocalDev(projectConfig, targetAccountId, shouldCreateWithoutConfirmation, hasPublicApps) {
190
- // Create the project without prompting if this is a newly created sandbox
191
- let shouldCreateProject = shouldCreateWithoutConfirmation;
192
- if (!shouldCreateProject) {
193
- const explanationLangFunction = hasPublicApps
194
- ? lib.localDevHelpers.createNewProjectForLocalDev
195
- .publicAppProjectMustExistExplanation
196
- : lib.localDevHelpers.createNewProjectForLocalDev
197
- .projectMustExistExplanation;
198
- const explanationString = explanationLangFunction(projectConfig.name, targetAccountId);
199
- uiLogger.log('');
200
- uiLine();
201
- uiLogger.log(explanationString);
202
- uiLine();
203
- shouldCreateProject = await confirmPrompt(lib.localDevHelpers.createNewProjectForLocalDev.createProject(projectConfig.name, uiAccountDescription(targetAccountId)));
204
- }
205
- if (shouldCreateProject) {
206
- SpinniesManager.add('createProject', {
207
- text: lib.localDevHelpers.createNewProjectForLocalDev.creatingProject(projectConfig.name, uiAccountDescription(targetAccountId)),
208
- });
209
- try {
210
- const { data: project } = await createProject(targetAccountId, projectConfig.name);
211
- SpinniesManager.succeed('createProject', {
212
- text: lib.localDevHelpers.createNewProjectForLocalDev.createdProject(projectConfig.name, uiAccountDescription(targetAccountId)),
213
- succeedColor: 'white',
214
- });
215
- return project;
216
- }
217
- catch (err) {
218
- SpinniesManager.fail('createProject');
219
- uiLogger.log(lib.localDevHelpers.createNewProjectForLocalDev.failedToCreateProject);
220
- process.exit(EXIT_CODES.ERROR);
221
- }
222
- }
223
- else {
224
- // We cannot continue if the project does not exist in the target account
225
- uiLogger.log('');
226
- uiLogger.log(lib.localDevHelpers.createNewProjectForLocalDev.choseNotToCreateProject);
227
- process.exit(EXIT_CODES.SUCCESS);
228
- }
229
- }
230
- function projectUploadCallback(accountId, projectConfig, tempFile, buildId) {
231
- if (!buildId) {
232
- uiLogger.error(lib.localDevHelpers.createInitialBuildForNewProject.genericError);
233
- process.exit(EXIT_CODES.ERROR);
234
- }
235
- return pollProjectBuildAndDeploy(accountId, projectConfig, tempFile, buildId, true);
236
- }
237
- // Create an initial build if the project was newly created in the account
238
- // Return the newly deployed build
239
- export async function createInitialBuildForNewProject(projectConfig, projectDir, targetAccountId, sendIR, profile) {
240
- const { result: initialUploadResult, uploadError } = await handleProjectUpload({
241
- accountId: targetAccountId,
242
- projectConfig,
243
- projectDir,
244
- callbackFunc: projectUploadCallback,
245
- uploadMessage: lib.localDevHelpers.createInitialBuildForNewProject
246
- .initialUploadMessage,
247
- forceCreate: true,
248
- skipValidation: true,
249
- sendIR,
250
- profile,
251
- });
252
- if (uploadError) {
253
- if (isSpecifiedError(uploadError, {
254
- subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED,
255
- })) {
256
- uiLogger.log('');
257
- uiLogger.error(lib.localDevHelpers.createInitialBuildForNewProject.projectLockedError);
258
- uiLogger.log('');
259
- }
260
- else {
261
- logError(uploadError, new ApiErrorContext({
262
- accountId: targetAccountId,
263
- projectName: projectConfig.name,
264
- }));
265
- }
266
- process.exit(EXIT_CODES.ERROR);
267
- }
268
- if (!initialUploadResult?.succeeded) {
269
- let subTasks = [];
270
- if (initialUploadResult?.buildResult.status === 'FAILURE') {
271
- subTasks =
272
- initialUploadResult.buildResult[PROJECT_BUILD_TEXT.SUBTASK_KEY];
273
- }
274
- else if (initialUploadResult?.deployResult?.status === 'FAILURE') {
275
- subTasks =
276
- initialUploadResult.deployResult[PROJECT_DEPLOY_TEXT.SUBTASK_KEY];
277
- }
278
- const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
279
- uiLogger.log('');
280
- failedSubTasks.forEach(failedSubTask => {
281
- uiLogger.error(failedSubTask.errorMessage);
282
- });
283
- uiLogger.log('');
284
- process.exit(EXIT_CODES.ERROR);
285
- }
286
- return initialUploadResult.buildResult;
287
- }
288
- export function getAccountHomeUrl(accountId) {
289
- const baseUrl = getHubSpotWebsiteOrigin(getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
290
- return `${baseUrl}/home?portalId=${accountId}`;
291
- }
292
192
  export async function hasSandboxes(account) {
293
193
  const accountId = getAccountIdentifier(account);
294
194
  if (!accountId) {
@@ -306,25 +206,25 @@ export async function hasSandboxes(account) {
306
206
  // Top level prompt to choose the type of account to test on
307
207
  export async function selectAccountTypePrompt(accountConfig) {
308
208
  const hasAccessToSandboxes = await hasSandboxes(accountConfig);
309
- const result = await listPrompt(lib.localDevHelpers.selectAccountTypePrompt.message, {
209
+ const accountId = getAccountIdentifier(accountConfig);
210
+ const result = await listPrompt(lib.localDevHelpers.account.selectAccountTypePrompt.message, {
310
211
  choices: [
311
212
  {
312
- name: lib.localDevHelpers.selectAccountTypePrompt
213
+ name: lib.localDevHelpers.account.selectAccountTypePrompt
313
214
  .developerTestAccountOption,
314
215
  value: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST,
315
216
  },
316
217
  {
317
- name: lib.localDevHelpers.selectAccountTypePrompt
218
+ name: lib.localDevHelpers.account.selectAccountTypePrompt
318
219
  .sandboxAccountOption,
319
220
  value: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
320
221
  disabled: !hasAccessToSandboxes
321
- ? lib.localDevHelpers.selectAccountTypePrompt
222
+ ? lib.localDevHelpers.account.selectAccountTypePrompt
322
223
  .sandboxAccountOptionDisabled
323
224
  : false,
324
225
  },
325
226
  {
326
- name: lib.localDevHelpers.selectAccountTypePrompt
327
- .productionAccountOption,
227
+ name: lib.localDevHelpers.account.selectAccountTypePrompt.productionAccountOption(accountId),
328
228
  value: null,
329
229
  },
330
230
  ],
@@ -0,0 +1,12 @@
1
+ import { Build } from '@hubspot/local-dev-lib/types/Build';
2
+ import { Project } from '@hubspot/local-dev-lib/types/Project';
3
+ import { IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/src/lib/types.js';
4
+ import { ProjectConfig } from '../../../../types/Projects.js';
5
+ export declare function createNewProjectForLocalDev(projectConfig: ProjectConfig, targetAccountId: number, shouldCreateWithoutConfirmation: boolean, hasPublicApps: boolean): Promise<Project>;
6
+ export declare function createInitialBuildForNewProject(projectConfig: ProjectConfig, projectDir: string, targetAccountId: number, sendIR?: boolean, profile?: string): Promise<Build>;
7
+ export declare function compareLocalProjectToDeployed(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number | undefined, localProjectNodes: {
8
+ [key: string]: IntermediateRepresentationNodeLocalDev;
9
+ }): Promise<void>;
10
+ export declare function isDeployedProjectUpToDateWithLocal(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, localProjectNodes: {
11
+ [key: string]: IntermediateRepresentationNodeLocalDev;
12
+ }): Promise<boolean>;
@@ -0,0 +1,173 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { createProject } from '@hubspot/local-dev-lib/api/projects';
5
+ import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
6
+ import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
7
+ import { sanitizeFileName } from '@hubspot/local-dev-lib/path';
8
+ import { isDeepEqual } from '@hubspot/local-dev-lib/isDeepEqual';
9
+ import { translate } from '@hubspot/project-parsing-lib';
10
+ import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
11
+ import { PROJECT_ERROR_TYPES, PROJECT_BUILD_TEXT, PROJECT_DEPLOY_TEXT, } from '../../../constants.js';
12
+ import { lib } from '../../../../lang/en.js';
13
+ import { uiLogger } from '../../../ui/logger.js';
14
+ import { uiLine } from '../../../ui/index.js';
15
+ import { confirmPrompt } from '../../../prompts/promptUtils.js';
16
+ import { uiAccountDescription } from '../../../ui/index.js';
17
+ import SpinniesManager from '../../../ui/SpinniesManager.js';
18
+ import { EXIT_CODES } from '../../../enums/exitCodes.js';
19
+ import { handleProjectUpload } from '../../upload.js';
20
+ import { pollProjectBuildAndDeploy } from '../../buildAndDeploy.js';
21
+ import { logError } from '../../../errorHandlers/index.js';
22
+ import { ApiErrorContext } from '../../../errorHandlers/index.js';
23
+ // Prompt the user to create a new project if one doesn't exist on their target account
24
+ export async function createNewProjectForLocalDev(projectConfig, targetAccountId, shouldCreateWithoutConfirmation, hasPublicApps) {
25
+ // Create the project without prompting if this is a newly created sandbox
26
+ let shouldCreateProject = shouldCreateWithoutConfirmation;
27
+ if (!shouldCreateProject) {
28
+ const explanationLangFunction = hasPublicApps
29
+ ? lib.localDevHelpers.project.createNewProjectForLocalDev
30
+ .publicAppProjectMustExistExplanation
31
+ : lib.localDevHelpers.project.createNewProjectForLocalDev
32
+ .projectMustExistExplanation;
33
+ const explanationString = explanationLangFunction(projectConfig.name, targetAccountId);
34
+ uiLogger.log('');
35
+ uiLine();
36
+ uiLogger.log(explanationString);
37
+ uiLine();
38
+ shouldCreateProject = await confirmPrompt(lib.localDevHelpers.project.createNewProjectForLocalDev.createProject(projectConfig.name, uiAccountDescription(targetAccountId)));
39
+ }
40
+ if (shouldCreateProject) {
41
+ SpinniesManager.add('createProject', {
42
+ text: lib.localDevHelpers.project.createNewProjectForLocalDev.creatingProject(projectConfig.name, uiAccountDescription(targetAccountId)),
43
+ });
44
+ try {
45
+ const { data: project } = await createProject(targetAccountId, projectConfig.name);
46
+ SpinniesManager.succeed('createProject', {
47
+ text: lib.localDevHelpers.project.createNewProjectForLocalDev.createdProject(projectConfig.name, uiAccountDescription(targetAccountId)),
48
+ succeedColor: 'white',
49
+ });
50
+ return project;
51
+ }
52
+ catch (err) {
53
+ SpinniesManager.fail('createProject');
54
+ uiLogger.log(lib.localDevHelpers.project.createNewProjectForLocalDev
55
+ .failedToCreateProject);
56
+ process.exit(EXIT_CODES.ERROR);
57
+ }
58
+ }
59
+ else {
60
+ // We cannot continue if the project does not exist in the target account
61
+ uiLogger.log('');
62
+ uiLogger.log(lib.localDevHelpers.project.createNewProjectForLocalDev
63
+ .choseNotToCreateProject);
64
+ process.exit(EXIT_CODES.SUCCESS);
65
+ }
66
+ }
67
+ function projectUploadCallback(accountId, projectConfig, tempFile, buildId) {
68
+ if (!buildId) {
69
+ uiLogger.error(lib.localDevHelpers.project.createInitialBuildForNewProject.genericError);
70
+ process.exit(EXIT_CODES.ERROR);
71
+ }
72
+ return pollProjectBuildAndDeploy(accountId, projectConfig, tempFile, buildId, true);
73
+ }
74
+ // Create an initial build if the project was newly created in the account
75
+ // Return the newly deployed build
76
+ export async function createInitialBuildForNewProject(projectConfig, projectDir, targetAccountId, sendIR, profile) {
77
+ const { result: initialUploadResult, uploadError } = await handleProjectUpload({
78
+ accountId: targetAccountId,
79
+ projectConfig,
80
+ projectDir,
81
+ callbackFunc: projectUploadCallback,
82
+ uploadMessage: lib.localDevHelpers.project.createInitialBuildForNewProject
83
+ .initialUploadMessage,
84
+ forceCreate: true,
85
+ skipValidation: true,
86
+ sendIR,
87
+ profile,
88
+ });
89
+ if (uploadError) {
90
+ if (isSpecifiedError(uploadError, {
91
+ subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED,
92
+ })) {
93
+ uiLogger.log('');
94
+ uiLogger.error(lib.localDevHelpers.project.createInitialBuildForNewProject
95
+ .projectLockedError);
96
+ uiLogger.log('');
97
+ }
98
+ else {
99
+ logError(uploadError, new ApiErrorContext({
100
+ accountId: targetAccountId,
101
+ projectName: projectConfig.name,
102
+ }));
103
+ }
104
+ process.exit(EXIT_CODES.ERROR);
105
+ }
106
+ if (!initialUploadResult?.succeeded) {
107
+ let subTasks = [];
108
+ if (initialUploadResult?.buildResult.status === 'FAILURE') {
109
+ subTasks =
110
+ initialUploadResult.buildResult[PROJECT_BUILD_TEXT.SUBTASK_KEY];
111
+ }
112
+ else if (initialUploadResult?.deployResult?.status === 'FAILURE') {
113
+ subTasks =
114
+ initialUploadResult.deployResult[PROJECT_DEPLOY_TEXT.SUBTASK_KEY];
115
+ }
116
+ const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
117
+ uiLogger.log('');
118
+ failedSubTasks.forEach(failedSubTask => {
119
+ uiLogger.error(failedSubTask.errorMessage);
120
+ });
121
+ uiLogger.log('');
122
+ process.exit(EXIT_CODES.ERROR);
123
+ }
124
+ return initialUploadResult.buildResult;
125
+ }
126
+ export async function compareLocalProjectToDeployed(projectConfig, accountId, deployedBuildId, localProjectNodes) {
127
+ uiLogger.log('');
128
+ if (!deployedBuildId) {
129
+ uiLogger.error(lib.localDevHelpers.project.compareLocalProjectToDeployed.noDeployedBuild(projectConfig.name, uiAccountDescription(accountId)));
130
+ process.exit(EXIT_CODES.SUCCESS);
131
+ }
132
+ SpinniesManager.add('compareLocalProjectToDeployed', {
133
+ text: lib.localDevHelpers.project.compareLocalProjectToDeployed.checking,
134
+ });
135
+ const isUpToDate = await isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes);
136
+ if (isUpToDate) {
137
+ SpinniesManager.succeed('compareLocalProjectToDeployed', {
138
+ text: lib.localDevHelpers.project.compareLocalProjectToDeployed.upToDate,
139
+ });
140
+ }
141
+ else {
142
+ SpinniesManager.fail('compareLocalProjectToDeployed', {
143
+ text: lib.localDevHelpers.project.compareLocalProjectToDeployed
144
+ .notUpToDate,
145
+ });
146
+ uiLogger.log('');
147
+ uiLogger.log(lib.localDevHelpers.project.compareLocalProjectToDeployed
148
+ .notUpToDateExplanation);
149
+ process.exit(EXIT_CODES.SUCCESS);
150
+ }
151
+ }
152
+ export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes) {
153
+ let tempDir = null;
154
+ try {
155
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hubspot-project-compare-'));
156
+ const { data: zippedProject } = await downloadProject(accountId, projectConfig.name, deployedBuildId);
157
+ const extractedProjectPath = path.join(tempDir, sanitizeFileName(projectConfig.name));
158
+ await extractZipArchive(zippedProject, sanitizeFileName(projectConfig.name), tempDir, { includesRootDir: false, hideLogs: true });
159
+ const deployedProjectSourceDir = path.join(extractedProjectPath, projectConfig.srcDir);
160
+ const { intermediateNodesIndexedByUid: deployedProjectNodes } = await translate({
161
+ projectSourceDir: deployedProjectSourceDir,
162
+ platformVersion: projectConfig.platformVersion,
163
+ accountId: accountId,
164
+ }, {});
165
+ return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
166
+ }
167
+ finally {
168
+ // Clean up temporary directory
169
+ if (tempDir && (await fs.pathExists(tempDir))) {
170
+ await fs.remove(tempDir);
171
+ }
172
+ }
173
+ }
@@ -6,3 +6,4 @@ export declare function getProjectActivityUrl(projectName: string, accountId: nu
6
6
  export declare function getProjectBuildDetailUrl(projectName: string, buildId: number, accountId: number): string;
7
7
  export declare function getProjectDeployDetailUrl(projectName: string, deployId: number, accountId: number): string;
8
8
  export declare function getLocalDevUiUrl(accountId: number, showWelcomeScreen?: boolean): string;
9
+ export declare function getAccountHomeUrl(accountId: number): string;
@@ -37,3 +37,7 @@ export function getProjectDeployDetailUrl(projectName, deployId, accountId) {
37
37
  export function getLocalDevUiUrl(accountId, showWelcomeScreen) {
38
38
  return `${getBaseUrl(accountId)}/developer-projects-local-dev/${accountId}${showWelcomeScreen ? '?welcome' : ''}`;
39
39
  }
40
+ export function getAccountHomeUrl(accountId) {
41
+ const baseUrl = getHubSpotWebsiteOrigin(getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
42
+ return `${baseUrl}/home?portalId=${accountId}`;
43
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createFunctionPrompt } from '../createFunctionPrompt.js';
3
+ import { promptUser } from '../promptUtils.js';
4
+ vi.mock('../promptUtils.js');
5
+ const mockPromptUser = vi.mocked(promptUser);
6
+ describe('createFunctionPrompt', () => {
7
+ beforeEach(() => {
8
+ vi.resetAllMocks();
9
+ });
10
+ describe('when all parameters are provided', () => {
11
+ it('should return provided values without prompting', async () => {
12
+ const commandArgs = {
13
+ functionsFolder: 'my-functions',
14
+ filename: 'my-function',
15
+ endpointMethod: 'POST',
16
+ endpointPath: '/api/test',
17
+ };
18
+ const result = await createFunctionPrompt(commandArgs);
19
+ expect(mockPromptUser).not.toHaveBeenCalled();
20
+ expect(result).toEqual({
21
+ functionsFolder: 'my-functions',
22
+ filename: 'my-function',
23
+ endpointMethod: 'POST',
24
+ endpointPath: '/api/test',
25
+ });
26
+ });
27
+ it('should use default GET method when endpointMethod not provided', async () => {
28
+ const commandArgs = {
29
+ functionsFolder: 'my-functions',
30
+ filename: 'my-function',
31
+ endpointPath: '/api/test',
32
+ };
33
+ const result = await createFunctionPrompt(commandArgs);
34
+ expect(mockPromptUser).not.toHaveBeenCalled();
35
+ expect(result).toEqual({
36
+ functionsFolder: 'my-functions',
37
+ filename: 'my-function',
38
+ endpointMethod: 'GET',
39
+ endpointPath: '/api/test',
40
+ });
41
+ });
42
+ });
43
+ describe('when some parameters are missing', () => {
44
+ it('should only prompt for missing parameters', async () => {
45
+ const commandArgs = {
46
+ functionsFolder: 'my-functions',
47
+ endpointMethod: 'POST',
48
+ };
49
+ mockPromptUser.mockResolvedValue({
50
+ filename: 'prompted-function',
51
+ endpointPath: '/prompted-path',
52
+ });
53
+ const result = await createFunctionPrompt(commandArgs);
54
+ expect(mockPromptUser).toHaveBeenCalledWith([
55
+ expect.objectContaining({ name: 'filename' }),
56
+ expect.objectContaining({ name: 'endpointPath' }),
57
+ ]);
58
+ expect(result).toEqual({
59
+ functionsFolder: 'my-functions',
60
+ filename: 'prompted-function',
61
+ endpointMethod: 'POST',
62
+ endpointPath: '/prompted-path',
63
+ });
64
+ });
65
+ });
66
+ describe('when no parameters are provided', () => {
67
+ it('should prompt for all parameters', async () => {
68
+ mockPromptUser.mockResolvedValue({
69
+ functionsFolder: 'prompted-functions',
70
+ filename: 'prompted-function',
71
+ endpointMethod: 'GET',
72
+ endpointPath: '/prompted-path',
73
+ });
74
+ const result = await createFunctionPrompt();
75
+ expect(mockPromptUser).toHaveBeenCalledWith([
76
+ expect.objectContaining({ name: 'functionsFolder' }),
77
+ expect.objectContaining({ name: 'filename' }),
78
+ expect.objectContaining({ name: 'endpointMethod' }),
79
+ expect.objectContaining({ name: 'endpointPath' }),
80
+ ]);
81
+ expect(result).toEqual({
82
+ functionsFolder: 'prompted-functions',
83
+ filename: 'prompted-function',
84
+ endpointMethod: 'GET',
85
+ endpointPath: '/prompted-path',
86
+ });
87
+ });
88
+ });
89
+ describe('parameter precedence', () => {
90
+ it('should prioritize command args over prompted values', async () => {
91
+ const commandArgs = {
92
+ functionsFolder: 'arg-functions',
93
+ };
94
+ mockPromptUser.mockResolvedValue({
95
+ filename: 'prompted-function',
96
+ endpointMethod: 'POST',
97
+ endpointPath: '/prompted-path',
98
+ });
99
+ const result = await createFunctionPrompt(commandArgs);
100
+ expect(result).toEqual({
101
+ functionsFolder: 'arg-functions', // from commandArgs
102
+ filename: 'prompted-function', // from prompt
103
+ endpointMethod: 'POST', // from prompt
104
+ endpointPath: '/prompted-path', // from prompt
105
+ });
106
+ });
107
+ it('should handle mixed scenario with partial command args and prompting', async () => {
108
+ const commandArgs = {
109
+ functionsFolder: 'my-funcs',
110
+ endpointMethod: 'DELETE',
111
+ };
112
+ mockPromptUser.mockResolvedValue({
113
+ filename: 'delete-handler',
114
+ endpointPath: '/api/delete',
115
+ });
116
+ const result = await createFunctionPrompt(commandArgs);
117
+ expect(mockPromptUser).toHaveBeenCalledWith([
118
+ expect.objectContaining({ name: 'filename' }),
119
+ expect.objectContaining({ name: 'endpointPath' }),
120
+ ]);
121
+ expect(result).toEqual({
122
+ functionsFolder: 'my-funcs', // from commandArgs
123
+ filename: 'delete-handler', // from prompt
124
+ endpointMethod: 'DELETE', // from commandArgs
125
+ endpointPath: '/api/delete', // from prompt
126
+ });
127
+ });
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};