@hubspot/cli 8.2.0 → 8.3.0-beta.1

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 (64) hide show
  1. package/api/migrate.js +5 -1
  2. package/commands/account/auth.js +15 -5
  3. package/commands/account/use.js +14 -4
  4. package/commands/app/__tests__/migrate.test.js +2 -2
  5. package/commands/app/migrate.js +3 -3
  6. package/commands/auth.js +10 -6
  7. package/commands/cms/__tests__/upload.test.js +4 -0
  8. package/commands/getStarted.js +1 -1
  9. package/commands/hubdb/clear.js +4 -0
  10. package/commands/hubdb/delete.js +4 -0
  11. package/commands/hubdb/fetch.js +4 -0
  12. package/commands/init.js +4 -0
  13. package/commands/project/__tests__/create.test.js +2 -2
  14. package/commands/project/__tests__/migrate.test.js +3 -3
  15. package/commands/project/create.js +3 -3
  16. package/commands/project/dev/index.js +29 -19
  17. package/commands/project/download.js +5 -1
  18. package/commands/project/migrate.js +7 -7
  19. package/commands/sandbox/__tests__/create.test.js +1 -48
  20. package/commands/sandbox/create.js +3 -30
  21. package/commands/testAccount/create.js +4 -0
  22. package/lang/en.d.ts +3 -3
  23. package/lang/en.js +4 -4
  24. package/lib/__tests__/buildAccount.test.js +1 -52
  25. package/lib/__tests__/sandboxes.test.js +1 -29
  26. package/lib/accountAuth.js +4 -0
  27. package/lib/app/__tests__/migrate.test.js +1 -1
  28. package/lib/app/migrate.js +11 -6
  29. package/lib/buildAccount.d.ts +1 -6
  30. package/lib/buildAccount.js +9 -42
  31. package/lib/constants.d.ts +0 -2
  32. package/lib/constants.js +0 -2
  33. package/lib/errors/PromptExitError.d.ts +4 -0
  34. package/lib/errors/PromptExitError.js +8 -0
  35. package/lib/getStartedV2Actions.js +1 -1
  36. package/lib/projects/__tests__/components.test.js +14 -0
  37. package/lib/projects/components.js +15 -4
  38. package/lib/projects/create/v2.js +1 -1
  39. package/lib/projects/localDev/AppDevModeInterface.js +4 -0
  40. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +4 -0
  41. package/lib/projects/localDev/helpers/account.js +5 -11
  42. package/lib/prompts/downloadProjectPrompt.js +11 -10
  43. package/lib/prompts/installAppPrompt.js +3 -2
  44. package/lib/prompts/personalAccessKeyPrompt.js +3 -2
  45. package/lib/prompts/projectDevTargetAccountPrompt.js +13 -16
  46. package/lib/prompts/selectHubDBTablePrompt.js +8 -4
  47. package/lib/prompts/selectPublicAppForMigrationPrompt.js +12 -6
  48. package/lib/sandboxes.d.ts +1 -9
  49. package/lib/sandboxes.js +0 -21
  50. package/lib/theme/__tests__/migrate.test.js +7 -16
  51. package/lib/theme/migrate.d.ts +1 -1
  52. package/lib/theme/migrate.js +1 -5
  53. package/lib/ui/SpinniesManager.js +2 -0
  54. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -0
  55. package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -0
  56. package/mcp-server/tools/project/CreateProjectTool.js +2 -1
  57. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +0 -1
  58. package/mcp-server/tools/project/constants.d.ts +1 -0
  59. package/mcp-server/tools/project/constants.js +2 -1
  60. package/package.json +2 -2
  61. package/lib/__tests__/sandboxSync.test.d.ts +0 -1
  62. package/lib/__tests__/sandboxSync.test.js +0 -147
  63. package/lib/sandboxSync.d.ts +0 -4
  64. package/lib/sandboxSync.js +0 -102
@@ -6,6 +6,7 @@ import { lib } from '../../lang/en.js';
6
6
  import { uiLogger } from '../ui/logger.js';
7
7
  import { uiAccountDescription } from '../ui/index.js';
8
8
  import { isSandbox } from '../accountTypes.js';
9
+ import { PromptExitError } from '../errors/PromptExitError.js';
9
10
  import { EXIT_CODES } from '../enums/exitCodes.js';
10
11
  function mapNestedAccount(accountConfig) {
11
12
  const parentAccountId = accountConfig.parentAccountId ?? null;
@@ -23,20 +24,18 @@ function getNonConfigDeveloperTestAccountName(account) {
23
24
  }
24
25
  export async function selectSandboxTargetAccountPrompt(accounts, defaultAccountConfig) {
25
26
  const defaultAccountId = defaultAccountConfig.accountId;
27
+ if (!defaultAccountId) {
28
+ uiLogger.error(lib.prompts.projectDevTargetAccountPrompt.noAccountId);
29
+ throw new PromptExitError(lib.prompts.projectDevTargetAccountPrompt.noAccountId, EXIT_CODES.ERROR);
30
+ }
26
31
  let choices = [];
27
32
  let sandboxUsage = {
28
33
  STANDARD: { used: 0, available: 0, limit: 0 },
29
34
  DEVELOPER: { used: 0, available: 0, limit: 0 },
30
35
  };
31
36
  try {
32
- if (defaultAccountId) {
33
- const { data } = await getSandboxUsageLimits(defaultAccountId);
34
- sandboxUsage = data.usage;
35
- }
36
- else {
37
- uiLogger.error(lib.prompts.projectDevTargetAccountPrompt.noAccountId);
38
- process.exit(EXIT_CODES.ERROR);
39
- }
37
+ const { data } = await getSandboxUsageLimits(defaultAccountId);
38
+ sandboxUsage = data.usage;
40
39
  }
41
40
  catch (err) {
42
41
  uiLogger.debug('Unable to fetch sandbox usage limits: ', err);
@@ -83,16 +82,14 @@ export async function selectSandboxTargetAccountPrompt(accounts, defaultAccountC
83
82
  }
84
83
  export async function selectDeveloperTestTargetAccountPrompt(accounts, defaultAccountConfig) {
85
84
  const defaultAccountId = defaultAccountConfig.accountId;
85
+ if (!defaultAccountId) {
86
+ uiLogger.error(lib.prompts.projectDevTargetAccountPrompt.noAccountId);
87
+ throw new PromptExitError(lib.prompts.projectDevTargetAccountPrompt.noAccountId, EXIT_CODES.ERROR);
88
+ }
86
89
  let devTestAccountsResponse;
87
90
  try {
88
- if (defaultAccountId) {
89
- const { data } = await fetchDeveloperTestAccounts(defaultAccountId);
90
- devTestAccountsResponse = data;
91
- }
92
- else {
93
- uiLogger.error(lib.prompts.projectDevTargetAccountPrompt.noAccountId);
94
- process.exit(EXIT_CODES.ERROR);
95
- }
91
+ const { data } = await fetchDeveloperTestAccounts(defaultAccountId);
92
+ devTestAccountsResponse = data;
96
93
  }
97
94
  catch (err) {
98
95
  uiLogger.debug('Unable to fetch developer test account usage limits: ', err);
@@ -4,25 +4,29 @@ import { lib } from '../../lang/en.js';
4
4
  import { debugError } from '../errorHandlers/index.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
6
  import { fetchTables } from '@hubspot/local-dev-lib/api/hubdb';
7
- import { EXIT_CODES } from '../enums/exitCodes.js';
8
7
  import { isValidPath, untildify } from '@hubspot/local-dev-lib/path';
8
+ import { PromptExitError } from '../errors/PromptExitError.js';
9
+ import { EXIT_CODES } from '../enums/exitCodes.js';
9
10
  async function fetchHubDBOptions(accountId) {
10
11
  try {
11
12
  const { data: { results: tables }, } = await fetchTables(accountId);
12
13
  if (tables.length === 0) {
13
14
  uiLogger.log(lib.prompts.selectHubDBTablePrompt.errors.noTables(accountId.toString()));
14
- process.exit(EXIT_CODES.SUCCESS);
15
+ throw new PromptExitError(lib.prompts.selectHubDBTablePrompt.errors.noTables(accountId.toString()), EXIT_CODES.SUCCESS);
15
16
  }
16
17
  return tables;
17
18
  }
18
19
  catch (error) {
20
+ if (error instanceof PromptExitError) {
21
+ throw error;
22
+ }
19
23
  debugError(error, { accountId });
20
24
  uiLogger.error(lib.prompts.selectHubDBTablePrompt.errors.errorFetchingTables(accountId.toString()));
21
- process.exit(EXIT_CODES.ERROR);
25
+ throw new PromptExitError(lib.prompts.selectHubDBTablePrompt.errors.errorFetchingTables(accountId.toString()), EXIT_CODES.ERROR);
22
26
  }
23
27
  }
24
28
  export async function selectHubDBTablePrompt({ accountId, options, skipDestPrompt = true, }) {
25
- const hubdbTables = (await fetchHubDBOptions(accountId)) || [];
29
+ const hubdbTables = await fetchHubDBOptions(accountId);
26
30
  const id = options.tableId?.toString();
27
31
  const isValidTable = options.tableId && hubdbTables.find(table => table.id === id);
28
32
  return promptUser([
@@ -4,13 +4,14 @@ import { uiLine } from '../ui/index.js';
4
4
  import { logError } from '../errorHandlers/index.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
6
  import { fetchPublicAppsForPortal } from '@hubspot/local-dev-lib/api/appsDev';
7
+ import { PromptExitError } from '../errors/PromptExitError.js';
7
8
  import { EXIT_CODES } from '../enums/exitCodes.js';
8
9
  async function fetchPublicAppOptions(accountId, accountName, isMigratingApp = false) {
10
+ if (!accountId) {
11
+ uiLogger.error(lib.prompts.selectPublicAppForMigrationPrompt.errors.noAccountId);
12
+ throw new PromptExitError(lib.prompts.selectPublicAppForMigrationPrompt.errors.noAccountId, EXIT_CODES.ERROR);
13
+ }
9
14
  try {
10
- if (!accountId) {
11
- uiLogger.error(lib.prompts.selectPublicAppForMigrationPrompt.errors.noAccountId);
12
- process.exit(EXIT_CODES.ERROR);
13
- }
14
15
  const { data: { results: publicApps }, } = await fetchPublicAppsForPortal(accountId);
15
16
  const filteredPublicApps = publicApps.filter(app => !app.projectId && !app.sourceId);
16
17
  if (!filteredPublicApps.length ||
@@ -24,14 +25,19 @@ async function fetchPublicAppOptions(accountId, accountName, isMigratingApp = fa
24
25
  uiLogger.error(`${lib.prompts.selectPublicAppForMigrationPrompt.errors.noAppsClone}\n${lib.prompts.selectPublicAppForMigrationPrompt.errors.noAppsCloneMessage(accountName)}`);
25
26
  }
26
27
  uiLine();
27
- process.exit(EXIT_CODES.SUCCESS);
28
+ throw new PromptExitError(isMigratingApp
29
+ ? lib.prompts.selectPublicAppForMigrationPrompt.errors.noAppsMigration
30
+ : lib.prompts.selectPublicAppForMigrationPrompt.errors.noAppsClone, EXIT_CODES.SUCCESS);
28
31
  }
29
32
  return filteredPublicApps;
30
33
  }
31
34
  catch (error) {
35
+ if (error instanceof PromptExitError) {
36
+ throw error;
37
+ }
32
38
  logError(error, accountId ? { accountId } : undefined);
33
39
  uiLogger.error(lib.prompts.selectPublicAppForMigrationPrompt.errors.errorFetchingApps);
34
- process.exit(EXIT_CODES.ERROR);
40
+ throw new PromptExitError(lib.prompts.selectPublicAppForMigrationPrompt.errors.errorFetchingApps, EXIT_CODES.ERROR);
35
41
  }
36
42
  }
37
43
  export async function selectPublicAppForMigrationPrompt({ accountId, accountName, isMigratingApp = false, }) {
@@ -1,22 +1,14 @@
1
1
  import { AccountType, HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
2
  import { Environment } from '@hubspot/local-dev-lib/types/Accounts';
3
- import { SandboxSyncTask, SandboxAccountType } from '../types/Sandboxes.js';
4
- export declare const SYNC_TYPES: {
5
- readonly OBJECT_RECORDS: "object-records";
6
- };
3
+ import { SandboxAccountType } from '../types/Sandboxes.js';
7
4
  export declare const SANDBOX_TYPE_MAP: {
8
5
  [key: string]: SandboxAccountType;
9
6
  };
10
- export declare const SANDBOX_API_TYPE_MAP: {
11
- readonly STANDARD_SANDBOX: 1;
12
- readonly DEVELOPMENT_SANDBOX: 2;
13
- };
14
7
  export declare const SANDBOX_TYPE_MAP_V2: {
15
8
  readonly STANDARD_SANDBOX: "STANDARD";
16
9
  readonly DEVELOPMENT_SANDBOX: "DEVELOPER";
17
10
  };
18
11
  export declare function getSandboxTypeAsString(accountType?: AccountType): string;
19
12
  export declare function getHasSandboxesByType(parentAccountConfig: HubSpotConfigAccount, type: AccountType): boolean;
20
- export declare function getAvailableSyncTypes(parentAccountConfig: HubSpotConfigAccount, config: HubSpotConfigAccount): Promise<Array<SandboxSyncTask>>;
21
13
  export declare function validateSandboxUsageLimits(accountConfig: HubSpotConfigAccount, sandboxType: AccountType, env: Environment): Promise<void>;
22
14
  export declare function handleSandboxCreateError(err: unknown, env: Environment, name: string, accountId: number): never;
package/lib/sandboxes.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
2
- import { fetchTypes } from '@hubspot/local-dev-lib/api/sandboxSync';
3
2
  import { getAllConfigAccounts } from '@hubspot/local-dev-lib/config';
4
3
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
5
4
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
@@ -8,19 +7,12 @@ import { uiLogger } from './ui/logger.js';
8
7
  import { lib } from '../lang/en.js';
9
8
  import { logError } from './errorHandlers/index.js';
10
9
  import { uiAccountDescription } from './ui/index.js';
11
- export const SYNC_TYPES = {
12
- OBJECT_RECORDS: 'object-records',
13
- };
14
10
  export const SANDBOX_TYPE_MAP = {
15
11
  dev: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
16
12
  developer: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
17
13
  development: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
18
14
  standard: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX,
19
15
  };
20
- export const SANDBOX_API_TYPE_MAP = {
21
- [HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 1,
22
- [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 2,
23
- };
24
16
  export const SANDBOX_TYPE_MAP_V2 = {
25
17
  [HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 'STANDARD',
26
18
  [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 'DEVELOPER',
@@ -45,19 +37,6 @@ export function getHasSandboxesByType(parentAccountConfig, type) {
45
37
  }
46
38
  return false;
47
39
  }
48
- // Fetches available sync types for a given sandbox portal
49
- export async function getAvailableSyncTypes(parentAccountConfig, config) {
50
- const parentPortalId = parentAccountConfig.accountId;
51
- const portalId = config.accountId;
52
- if (!parentPortalId || !portalId) {
53
- throw new Error(lib.sandbox.sync.failure.syncTypeFetch);
54
- }
55
- const { data: { results: syncTypes }, } = await fetchTypes(parentPortalId, portalId);
56
- if (!syncTypes) {
57
- throw new Error(lib.sandbox.sync.failure.syncTypeFetch);
58
- }
59
- return syncTypes.map(t => ({ type: t.name }));
60
- }
61
40
  export async function validateSandboxUsageLimits(accountConfig, sandboxType, env) {
62
41
  const accountId = accountConfig.accountId;
63
42
  if (!accountId) {
@@ -3,15 +3,13 @@ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
3
3
  import { confirmPrompt } from '../../prompts/promptUtils.js';
4
4
  import { writeProjectConfig, } from '../../projects/config.js';
5
5
  import { ensureProjectExists } from '../../projects/ensureProjectExists.js';
6
- import { isV2Project } from '../../projects/platformVersion.js';
7
6
  import { fetchMigrationApps } from '../../app/migrate.js';
8
- import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration, migrateThemes2025_2, } from '../migrate.js';
7
+ import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration, migrateThemesV2, } from '../migrate.js';
9
8
  import { lib } from '../../../lang/en.js';
10
9
  vi.mock('@hubspot/project-parsing-lib/themes');
11
10
  vi.mock('../../prompts/promptUtils');
12
11
  vi.mock('../../projects/config');
13
12
  vi.mock('../../projects/ensureProjectExists');
14
- vi.mock('../../projects/platformVersion');
15
13
  vi.mock('../../app/migrate');
16
14
  vi.mock('@hubspot/local-dev-lib/config');
17
15
  vi.mock('../../ui/SpinniesManager', () => ({
@@ -28,7 +26,6 @@ const mockedMigrateThemes = migrateThemes;
28
26
  const mockedConfirmPrompt = confirmPrompt;
29
27
  const mockedWriteProjectConfig = writeProjectConfig;
30
28
  const mockedEnsureProjectExists = ensureProjectExists;
31
- const mockedUseV2Api = isV2Project;
32
29
  const mockedFetchMigrationApps = fetchMigrationApps;
33
30
  const mockedGetConfigAccountById = getConfigAccountById;
34
31
  const ACCOUNT_ID = 123;
@@ -41,7 +38,6 @@ const createLoadedProjectConfig = (name) => ({
41
38
  });
42
39
  describe('lib/theme/migrate', () => {
43
40
  beforeEach(() => {
44
- mockedUseV2Api.mockReturnValue(false);
45
41
  // Mock account config for the test account ID
46
42
  mockedGetConfigAccountById.mockReturnValue({
47
43
  accountId: ACCOUNT_ID,
@@ -144,11 +140,6 @@ describe('lib/theme/migrate', () => {
144
140
  });
145
141
  });
146
142
  describe('validateMigrationAppsAndThemes', () => {
147
- it('should throw an error when themes are already migrated (v2 API)', async () => {
148
- mockedUseV2Api.mockReturnValue(true);
149
- const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
150
- await expect(validateMigrationAppsAndThemes(0, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAlreadyMigrated);
151
- });
152
143
  it('should throw an error when apps and themes are both present', async () => {
153
144
  const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
154
145
  await expect(validateMigrationAppsAndThemes(1, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAndAppsNotAllowed);
@@ -204,7 +195,7 @@ describe('lib/theme/migrate', () => {
204
195
  await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.failedToMigrateThemes);
205
196
  });
206
197
  });
207
- describe('migrateThemes2025_2', () => {
198
+ describe('migrateThemesV2', () => {
208
199
  const options = {
209
200
  platformVersion: PLATFORM_VERSION,
210
201
  };
@@ -230,25 +221,25 @@ describe('lib/theme/migrate', () => {
230
221
  projectConfig: undefined,
231
222
  projectDir: MOCK_PROJECT_DIR,
232
223
  };
233
- await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, invalidProjectConfig)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
224
+ await expect(migrateThemesV2(ACCOUNT_ID, options, themeCount, invalidProjectConfig)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
234
225
  });
235
226
  it('should throw an error when project does not exist', async () => {
236
227
  mockedEnsureProjectExists.mockResolvedValue({ projectExists: false });
237
- await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig)).rejects.toThrow(lib.migrate.errors.project.doesNotExist(ACCOUNT_ID));
228
+ await expect(migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig)).rejects.toThrow(lib.migrate.errors.project.doesNotExist(ACCOUNT_ID));
238
229
  });
239
230
  it('should proceed with migration when user confirms', async () => {
240
- await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
231
+ await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
241
232
  expect(mockedFetchMigrationApps).toHaveBeenCalledWith(ACCOUNT_ID, PLATFORM_VERSION, { projectConfig });
242
233
  expect(mockedConfirmPrompt).toHaveBeenCalledWith(lib.migrate.prompt.proceed, { defaultAnswer: false });
243
234
  expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
244
235
  });
245
236
  it('should exit without migrating when user cancels', async () => {
246
237
  mockedConfirmPrompt.mockResolvedValue(false);
247
- await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
238
+ await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
248
239
  expect(mockedMigrateThemes).not.toHaveBeenCalled();
249
240
  });
250
241
  it('should validate migration apps and themes', async () => {
251
- await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
242
+ await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
252
243
  // The validation is called internally, so we verify it through the error handling
253
244
  expect(mockedFetchMigrationApps).toHaveBeenCalled();
254
245
  });
@@ -10,4 +10,4 @@ export declare function getHasMigratableThemes(projectConfig?: LoadedProjectConf
10
10
  }>;
11
11
  export declare function validateMigrationAppsAndThemes(hasApps: number, projectConfig?: LoadedProjectConfig): Promise<void>;
12
12
  export declare function handleThemesMigration(projectConfig: LoadedProjectConfig, platformVersion: string): Promise<void>;
13
- export declare function migrateThemes2025_2(derivedAccountId: number, options: ArgumentsCamelCase<MigrateThemesArgs>, themeCount: number, projectConfig: LoadedProjectConfig): Promise<void>;
13
+ export declare function migrateThemesV2(derivedAccountId: number, options: ArgumentsCamelCase<MigrateThemesArgs>, themeCount: number, projectConfig: LoadedProjectConfig): Promise<void>;
@@ -7,7 +7,6 @@ import { lib } from '../../lang/en.js';
7
7
  import { PROJECT_CONFIG_FILE } from '../constants.js';
8
8
  import { uiLogger } from '../ui/logger.js';
9
9
  import { debugError } from '../errorHandlers/index.js';
10
- import { isV2Project } from '../projects/platformVersion.js';
11
10
  import { confirmPrompt } from '../prompts/promptUtils.js';
12
11
  import { fetchMigrationApps } from '../app/migrate.js';
13
12
  export async function getHasMigratableThemes(projectConfig) {
@@ -22,9 +21,6 @@ export async function getHasMigratableThemes(projectConfig) {
22
21
  };
23
22
  }
24
23
  export async function validateMigrationAppsAndThemes(hasApps, projectConfig) {
25
- if (isV2Project(projectConfig?.projectConfig?.platformVersion)) {
26
- throw new Error(lib.migrate.errors.project.themesAlreadyMigrated);
27
- }
28
24
  if (hasApps > 0 && projectConfig) {
29
25
  throw new Error(lib.migrate.errors.project.themesAndAppsNotAllowed);
30
26
  }
@@ -61,7 +57,7 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
61
57
  uiLogger.log('');
62
58
  uiLogger.log(lib.migrate.success.themesMigrationSuccess(platformVersion));
63
59
  }
64
- export async function migrateThemes2025_2(derivedAccountId, options, themeCount, projectConfig) {
60
+ export async function migrateThemesV2(derivedAccountId, options, themeCount, projectConfig) {
65
61
  if (!projectConfig?.projectConfig || !projectConfig?.projectDir) {
66
62
  throw new Error(lib.migrate.errors.project.invalidConfig);
67
63
  }
@@ -103,6 +103,8 @@ class SpinniesManager {
103
103
  return;
104
104
  }
105
105
  delete this.spinners[name];
106
+ // Update the spinner state to clean up the deleted spinner
107
+ this.updateSpinnerState();
106
108
  }
107
109
  stopAll(newStatus = 'stopped') {
108
110
  Object.keys(this.spinners).forEach(name => {
@@ -22,6 +22,7 @@ declare const inputSchemaZodObject: z.ZodObject<{
22
22
  webhooks: "webhooks";
23
23
  "workflow-action": "workflow-action";
24
24
  "app-function": "app-function";
25
+ "app-function-endpoint": "app-function-endpoint";
25
26
  "app-object": "app-object";
26
27
  scim: "scim";
27
28
  }>>>;
@@ -26,6 +26,7 @@ declare const inputSchemaZodObject: z.ZodObject<{
26
26
  webhooks: "webhooks";
27
27
  "workflow-action": "workflow-action";
28
28
  "app-function": "app-function";
29
+ "app-function-endpoint": "app-function-endpoint";
29
30
  "app-object": "app-object";
30
31
  scim: "scim";
31
32
  }>>>;
@@ -8,6 +8,7 @@ import { formatTextContents, formatTextContent } from '../../utils/content.js';
8
8
  import { trackToolUsage } from '../../utils/toolUsageTracking.js';
9
9
  import { setupHubSpotConfig } from '../../utils/config.js';
10
10
  import { getErrorMessage } from '../../../lib/errorHandlers/index.js';
11
+ import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
11
12
  const inputSchema = {
12
13
  absoluteCurrentWorkingDirectory,
13
14
  name: z
@@ -40,7 +41,7 @@ export class CreateProjectTool extends Tool {
40
41
  async handler({ name, destination, projectBase, distribution, auth, features, absoluteCurrentWorkingDirectory, }) {
41
42
  setupHubSpotConfig(absoluteCurrentWorkingDirectory);
42
43
  await trackToolUsage(toolName);
43
- let command = addFlag('hs project create', 'platform-version', '2025.2');
44
+ let command = addFlag('hs project create', 'platform-version', PLATFORM_VERSIONS.v2026_03);
44
45
  const content = [];
45
46
  if (name) {
46
47
  command = addFlag(command, 'name', name);
@@ -55,7 +55,6 @@ describe('mcp-server/tools/project/CreateProjectTool', () => {
55
55
  stderr: '',
56
56
  });
57
57
  const result = await tool.handler(baseInput);
58
- expect(mockAddFlag).toHaveBeenCalledWith('hs project create', 'platform-version', '2025.2');
59
58
  expect(mockAddFlag).toHaveBeenCalledWith(expect.any(String), 'name', 'test-project');
60
59
  expect(mockAddFlag).toHaveBeenCalledWith(expect.any(String), 'dest', './test-dest');
61
60
  expect(mockAddFlag).toHaveBeenCalledWith(expect.any(String), 'project-base', EMPTY_PROJECT);
@@ -10,6 +10,7 @@ export declare const features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
10
10
  webhooks: "webhooks";
11
11
  "workflow-action": "workflow-action";
12
12
  "app-function": "app-function";
13
+ "app-function-endpoint": "app-function-endpoint";
13
14
  "app-object": "app-object";
14
15
  scim: "scim";
15
16
  }>>>;
@@ -10,6 +10,7 @@ export const features = z
10
10
  'card',
11
11
  'settings',
12
12
  'app-function',
13
+ 'app-function-endpoint',
13
14
  'webhooks',
14
15
  'workflow-action',
15
16
  'workflow-action-tool',
@@ -18,7 +19,7 @@ export const features = z
18
19
  'scim',
19
20
  'page',
20
21
  ]))
21
- .describe('The features to include in the project, multiple options can be selected. "app-function" is also known as a public serverless function. "workflow-action" is also known as a custom workflow action. "workflow-action-tool" is also known as agent tools.')
22
+ .describe('The features to include in the project, multiple options can be selected. "app-function" is also known as a private serverless function. "app-function-endpoint" is a serverless functions that is publicly accessible via endpoint. "workflow-action" is also known as a custom workflow action. "workflow-action-tool" is also known as agent tools.')
22
23
  .optional();
23
24
  export const docsSearchQuery = z
24
25
  .string()
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.2.0",
3
+ "version": "8.3.0-beta.1",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "@hubspot/local-dev-lib": "5.1.2",
9
+ "@hubspot/local-dev-lib": "5.2.0",
10
10
  "@hubspot/project-parsing-lib": "0.12.1",
11
11
  "@hubspot/serverless-dev-runtime": "7.0.7",
12
12
  "@hubspot/ui-extensions-dev-server": "2.0.0",
@@ -1 +0,0 @@
1
- export {};
@@ -1,147 +0,0 @@
1
- import { uiLogger } from '../ui/logger.js';
2
- import { initiateSync } from '@hubspot/local-dev-lib/api/sandboxSync';
3
- import { getConfigAccountIfExists, getConfigAccountById, } from '@hubspot/local-dev-lib/config';
4
- import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
5
- import { mockHubSpotHttpError } from '../testUtils.js';
6
- import { getAvailableSyncTypes } from '../sandboxes.js';
7
- import { syncSandbox } from '../sandboxSync.js';
8
- import SpinniesManager from '../ui/SpinniesManager.js';
9
- vi.mock('@hubspot/local-dev-lib/api/sandboxSync');
10
- vi.mock('@hubspot/local-dev-lib/config');
11
- vi.mock('../sandboxes');
12
- vi.mock('../ui/SpinniesManager');
13
- const mockedUiLogger = uiLogger;
14
- const mockedInitiateSync = initiateSync;
15
- const mockedGetConfigAccountIfExists = getConfigAccountIfExists;
16
- const mockedGetConfigAccountById = getConfigAccountById;
17
- const mockedGetAvailableSyncTypes = getAvailableSyncTypes;
18
- const mockedSpinniesInit = SpinniesManager.init;
19
- const mockedSpinniesAdd = SpinniesManager.add;
20
- const mockedSpinniesSucceed = SpinniesManager.succeed;
21
- const mockedSpinniesFail = SpinniesManager.fail;
22
- describe('lib/sandboxSync', () => {
23
- const mockEnv = 'qa';
24
- const mockParentAccount = {
25
- name: 'Parent Account',
26
- accountId: 123,
27
- accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX,
28
- env: mockEnv,
29
- authType: 'personalaccesskey',
30
- };
31
- const mockChildAccount = {
32
- name: 'Child Account',
33
- accountId: 456,
34
- accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
35
- env: mockEnv,
36
- authType: 'personalaccesskey',
37
- };
38
- const mockChildAccountWithMissingId = {
39
- name: 'Child Account',
40
- accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
41
- env: mockEnv,
42
- authType: 'personalaccesskey',
43
- };
44
- const mockSyncTasks = [{ type: 'mock-sync-type' }];
45
- beforeEach(() => {
46
- mockedGetConfigAccountIfExists
47
- .mockReturnValueOnce(mockChildAccount)
48
- .mockReturnValueOnce(mockParentAccount);
49
- mockedGetAvailableSyncTypes.mockResolvedValue(mockSyncTasks);
50
- // Mock SpinniesManager methods to prevent spinner errors
51
- mockedSpinniesInit.mockImplementation(() => { });
52
- mockedSpinniesAdd.mockImplementation(() => { });
53
- mockedSpinniesSucceed.mockImplementation(() => { });
54
- mockedSpinniesFail.mockImplementation(() => { });
55
- // Mock account config for uiAccountDescription calls
56
- mockedGetConfigAccountById.mockImplementation(accountId => {
57
- if (accountId === mockChildAccount.accountId) {
58
- return mockChildAccount;
59
- }
60
- if (accountId === mockParentAccount.accountId) {
61
- return mockParentAccount;
62
- }
63
- return undefined; // Don't throw, just return undefined for unknown accounts
64
- });
65
- });
66
- describe('syncSandbox()', () => {
67
- it('successfully syncs a sandbox with provided sync tasks', async () => {
68
- mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
69
- await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks);
70
- expect(mockedSpinniesInit).toHaveBeenCalled();
71
- expect(mockedSpinniesAdd).toHaveBeenCalled();
72
- expect(mockedInitiateSync).toHaveBeenCalledWith(mockParentAccount.accountId, mockChildAccount.accountId, mockSyncTasks, mockChildAccount.accountId);
73
- expect(mockedSpinniesSucceed).toHaveBeenCalled();
74
- });
75
- it('fetches sync types when no tasks are provided', async () => {
76
- mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
77
- await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, []);
78
- expect(mockedGetAvailableSyncTypes).toHaveBeenCalledWith(mockParentAccount, mockChildAccount);
79
- expect(mockedGetAvailableSyncTypes).toHaveBeenCalledWith(mockParentAccount, mockChildAccount);
80
- expect(mockedInitiateSync).toHaveBeenCalled();
81
- });
82
- it('throws error when account IDs are missing', async () => {
83
- const errorRegex = new RegExp(`because your account has been removed from`);
84
- await expect(syncSandbox(mockChildAccountWithMissingId, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toThrow(errorRegex);
85
- });
86
- it('handles sync in progress error', async () => {
87
- const error = mockHubSpotHttpError('', {
88
- status: 429,
89
- data: {
90
- category: 'RATE_LIMITS',
91
- subCategory: 'sandboxes-sync-api.SYNC_IN_PROGRESS',
92
- },
93
- });
94
- mockedInitiateSync.mockRejectedValue(error);
95
- await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
96
- expect(mockedSpinniesFail).toHaveBeenCalled();
97
- expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Couldn't run the sync because there's another sync in progress/));
98
- });
99
- it('handles invalid user error', async () => {
100
- const error = mockHubSpotHttpError('', {
101
- status: 403,
102
- data: {
103
- category: 'BANNED',
104
- subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USER',
105
- },
106
- });
107
- mockedInitiateSync.mockRejectedValue(error);
108
- await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
109
- expect(mockedSpinniesFail).toHaveBeenCalled();
110
- expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/because your account has been removed from/));
111
- });
112
- it('handles not super admin error', async () => {
113
- const error = mockHubSpotHttpError('', {
114
- status: 403,
115
- data: {
116
- category: 'BANNED',
117
- subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USERID',
118
- },
119
- });
120
- mockedInitiateSync.mockRejectedValue(error);
121
- await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
122
- expect(mockedSpinniesFail).toHaveBeenCalled();
123
- expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Couldn't run the sync because you are not a super admin in/));
124
- });
125
- it('handles sandbox not found error', async () => {
126
- const error = mockHubSpotHttpError('', {
127
- status: 404,
128
- data: {
129
- category: 'OBJECT_NOT_FOUND',
130
- subCategory: 'SandboxErrors.SANDBOX_NOT_FOUND',
131
- },
132
- });
133
- mockedInitiateSync.mockRejectedValue(error);
134
- await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
135
- expect(mockedSpinniesFail).toHaveBeenCalled();
136
- expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/may have been deleted through the UI/));
137
- });
138
- it('displays slim info message when specified', async () => {
139
- mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
140
- await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks, true);
141
- expect(mockedUiLogger.info).not.toHaveBeenCalled();
142
- expect(mockedSpinniesSucceed).toHaveBeenCalledWith('sandboxSync', expect.objectContaining({
143
- text: expect.stringMatching(/Initiated sync of object definitions from production to /),
144
- }));
145
- });
146
- });
147
- });
@@ -1,4 +0,0 @@
1
- import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
- import { Environment } from '@hubspot/local-dev-lib/types/Accounts';
3
- import { SandboxSyncTask } from '../types/Sandboxes.js';
4
- export declare function syncSandbox(accountConfig: HubSpotConfigAccount, parentAccountConfig: HubSpotConfigAccount, env: Environment, syncTasks: Array<SandboxSyncTask>, slimInfoMessage?: boolean): Promise<void>;