@hubspot/cli 8.0.10-experimental.3 → 8.0.10-experimental.4

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 (49) hide show
  1. package/api/migrate.js +5 -1
  2. package/commands/account/auth.js +5 -15
  3. package/commands/account/use.js +4 -14
  4. package/commands/app/migrate.js +2 -2
  5. package/commands/auth.js +6 -10
  6. package/commands/cms/__tests__/upload.test.js +4 -0
  7. package/commands/hubdb/clear.js +0 -4
  8. package/commands/hubdb/delete.js +0 -4
  9. package/commands/hubdb/fetch.js +0 -4
  10. package/commands/init.js +0 -4
  11. package/commands/project/__tests__/migrate.test.js +2 -2
  12. package/commands/project/dev/index.js +19 -29
  13. package/commands/project/download.js +1 -5
  14. package/commands/project/migrate.js +6 -6
  15. package/commands/sandbox/__tests__/create.test.js +48 -1
  16. package/commands/sandbox/create.js +30 -3
  17. package/commands/testAccount/create.js +0 -4
  18. package/lang/en.d.ts +2 -3
  19. package/lang/en.js +2 -3
  20. package/lib/__tests__/buildAccount.test.js +52 -1
  21. package/lib/__tests__/sandboxSync.test.d.ts +1 -0
  22. package/lib/__tests__/sandboxSync.test.js +147 -0
  23. package/lib/__tests__/sandboxes.test.js +29 -1
  24. package/lib/accountAuth.js +0 -4
  25. package/lib/buildAccount.d.ts +6 -1
  26. package/lib/buildAccount.js +42 -9
  27. package/lib/constants.d.ts +2 -0
  28. package/lib/constants.js +2 -0
  29. package/lib/projects/__tests__/components.test.js +0 -14
  30. package/lib/projects/components.js +2 -12
  31. package/lib/projects/localDev/AppDevModeInterface.js +0 -4
  32. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +0 -4
  33. package/lib/projects/localDev/helpers/account.js +11 -5
  34. package/lib/prompts/downloadProjectPrompt.js +10 -11
  35. package/lib/prompts/installAppPrompt.js +2 -3
  36. package/lib/prompts/personalAccessKeyPrompt.js +2 -3
  37. package/lib/prompts/projectDevTargetAccountPrompt.js +16 -13
  38. package/lib/prompts/selectHubDBTablePrompt.js +4 -8
  39. package/lib/prompts/selectPublicAppForMigrationPrompt.js +6 -12
  40. package/lib/sandboxSync.d.ts +4 -0
  41. package/lib/sandboxSync.js +102 -0
  42. package/lib/sandboxes.d.ts +9 -1
  43. package/lib/sandboxes.js +21 -0
  44. package/lib/theme/__tests__/migrate.test.js +6 -11
  45. package/lib/theme/migrate.d.ts +1 -1
  46. package/lib/theme/migrate.js +1 -5
  47. package/package.json +3 -3
  48. package/lib/errors/PromptExitError.d.ts +0 -4
  49. package/lib/errors/PromptExitError.js +0 -8
@@ -1,7 +1,7 @@
1
1
  import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
2
2
  import { getConfigAccountIfExists, updateConfigAccount, } from '@hubspot/local-dev-lib/config';
3
3
  import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
4
- import { createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
4
+ import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
5
5
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
6
6
  import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
7
7
  import { cliAccountNamePrompt } from '../prompts/accountNamePrompt.js';
@@ -32,6 +32,7 @@ const mockedCliAccountNamePrompt = cliAccountNamePrompt;
32
32
  const mockedCreateDeveloperTestAccount = createDeveloperTestAccount;
33
33
  const mockedFetchDeveloperTestAccountGateSyncStatus = fetchDeveloperTestAccountGateSyncStatus;
34
34
  const mockedGenerateDeveloperTestAccountPersonalAccessKey = generateDeveloperTestAccountPersonalAccessKey;
35
+ const mockedCreateSandbox = createSandbox;
35
36
  const mockedCreateV2Sandbox = createV2Sandbox;
36
37
  const mockedGetPersonalAccessKey = getSandboxPersonalAccessKey;
37
38
  const mockedPoll = poll;
@@ -161,6 +162,56 @@ describe('lib/buildAccount', () => {
161
162
  await expect(buildAccount.buildDeveloperTestAccount(mockDeveloperTestAccount.accountName, mockParentAccountConfig, mockParentAccountConfig.env, 10)).rejects.toThrow();
162
163
  });
163
164
  });
165
+ describe('buildSandbox()', () => {
166
+ const mockParentAccountConfig = {
167
+ name: 'Prod account',
168
+ accountId: 123456,
169
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
170
+ env: 'prod',
171
+ };
172
+ const mockSandbox = {
173
+ sandboxHubId: 56789,
174
+ parentHubId: 123456,
175
+ createdAt: '2025-01-01',
176
+ type: 'STANDARD',
177
+ version: 'V1',
178
+ archived: false,
179
+ name: 'Test Sandbox',
180
+ domain: 'test-sandbox.hubspot.com',
181
+ createdByUser: {
182
+ id: 123456,
183
+ email: 'test@test.com',
184
+ firstName: 'Test',
185
+ lastName: 'User',
186
+ },
187
+ };
188
+ beforeEach(() => {
189
+ vi.spyOn(buildAccount, 'saveAccountToConfig').mockResolvedValue(mockParentAccountConfig.name);
190
+ mockedCreateSandbox.mockResolvedValue({
191
+ data: { sandbox: mockSandbox, personalAccessKey: 'test-key' },
192
+ });
193
+ });
194
+ it('should create a standard sandbox successfully', async () => {
195
+ const result = await buildAccount.buildSandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, mockParentAccountConfig.env, false);
196
+ expect(result).toEqual({
197
+ name: mockSandbox.name,
198
+ personalAccessKey: 'test-key',
199
+ sandbox: mockSandbox,
200
+ });
201
+ });
202
+ it('should create a development sandbox successfully', async () => {
203
+ const result = await buildAccount.buildSandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, mockParentAccountConfig.env);
204
+ expect(result).toEqual({
205
+ name: mockSandbox.name,
206
+ personalAccessKey: 'test-key',
207
+ sandbox: mockSandbox,
208
+ });
209
+ });
210
+ it('should handle API errors when creating sandbox', async () => {
211
+ mockedCreateSandbox.mockRejectedValue(new Error('test-error'));
212
+ await expect(buildAccount.buildSandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, mockParentAccountConfig.env, false)).rejects.toThrow();
213
+ });
214
+ });
164
215
  describe('buildV2Sandbox()', () => {
165
216
  const mockParentAccountConfig = {
166
217
  name: 'Prod account',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,147 @@
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,9 +1,10 @@
1
1
  import { uiLogger } from '../ui/logger.js';
2
2
  import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
3
+ import { fetchTypes } from '@hubspot/local-dev-lib/api/sandboxSync';
3
4
  import { getAllConfigAccounts, getConfigAccountIfExists, } from '@hubspot/local-dev-lib/config';
4
5
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
5
6
  import { mockHubSpotHttpError } from '../testUtils.js';
6
- import { getSandboxTypeAsString, getHasSandboxesByType, validateSandboxUsageLimits, handleSandboxCreateError, } from '../sandboxes.js';
7
+ import { getSandboxTypeAsString, getHasSandboxesByType, getAvailableSyncTypes, validateSandboxUsageLimits, handleSandboxCreateError, } from '../sandboxes.js';
7
8
  import { isMissingScopeError, isSpecifiedError, } from '@hubspot/local-dev-lib/errors/index';
8
9
  vi.mock('@hubspot/local-dev-lib/api/sandboxHubs');
9
10
  vi.mock('@hubspot/local-dev-lib/api/sandboxSync');
@@ -11,6 +12,7 @@ vi.mock('@hubspot/local-dev-lib/config');
11
12
  vi.mock('@hubspot/local-dev-lib/errors/index');
12
13
  const mockedGetConfigAccountIfExists = getConfigAccountIfExists;
13
14
  const mockedGetSandboxUsageLimits = getSandboxUsageLimits;
15
+ const mockedFetchTypes = fetchTypes;
14
16
  const mockedGetAllConfigAccounts = getAllConfigAccounts;
15
17
  const mockedUiLogger = uiLogger;
16
18
  const mockedIsMissingScopeError = isMissingScopeError;
@@ -51,6 +53,32 @@ describe('lib/sandboxes', () => {
51
53
  expect(getHasSandboxesByType(mockParentAccount, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX)).toBe(false);
52
54
  });
53
55
  });
56
+ describe('getAvailableSyncTypes()', () => {
57
+ const mockParentAccount = {
58
+ name: 'Parent Account',
59
+ accountId: 123,
60
+ env: 'qa',
61
+ };
62
+ const mockChildAccount = {
63
+ ...mockParentAccount,
64
+ accountId: 456,
65
+ };
66
+ it('returns available sync types when fetch is successful', async () => {
67
+ const mockSyncTypes = [{ name: 'type1' }, { name: 'type2' }];
68
+ mockedGetConfigAccountIfExists
69
+ .mockReturnValue(mockParentAccount.accountId)
70
+ .mockReturnValue(mockChildAccount.accountId);
71
+ mockedFetchTypes.mockResolvedValue({
72
+ data: { results: mockSyncTypes },
73
+ });
74
+ const result = await getAvailableSyncTypes(mockParentAccount, mockChildAccount);
75
+ expect(result).toEqual([{ type: 'type1' }, { type: 'type2' }]);
76
+ });
77
+ it('throws error when sync types fetch fails', async () => {
78
+ mockedFetchTypes.mockResolvedValue({ data: { results: null } });
79
+ await expect(getAvailableSyncTypes(mockParentAccount, mockChildAccount)).rejects.toThrow(/Unable to fetch available sandbox sync types/);
80
+ });
81
+ });
54
82
  describe('validateSandboxUsageLimits()', () => {
55
83
  const mockAccount = {
56
84
  name: 'Test Account',
@@ -3,7 +3,6 @@ import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev
3
3
  import { toKebabCase } from '@hubspot/local-dev-lib/text';
4
4
  import { handleMerge, handleMigration } from './configMigrate.js';
5
5
  import { debugError, logError } from './errorHandlers/index.js';
6
- import { PromptExitError } from './errors/PromptExitError.js';
7
6
  import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
8
7
  import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
9
8
  import { setAsDefaultAccountPrompt } from './prompts/setAsDefaultAccountPrompt.js';
@@ -37,9 +36,6 @@ async function updateConfigWithNewAccount(env, configAlreadyExists, providedPers
37
36
  return updatedConfig;
38
37
  }
39
38
  catch (e) {
40
- if (e instanceof PromptExitError) {
41
- throw e;
42
- }
43
39
  debugError(e);
44
40
  return null;
45
41
  }
@@ -1,7 +1,7 @@
1
1
  import { DeveloperTestAccountConfig } from '@hubspot/local-dev-lib/types/developerTestAccounts';
2
2
  import { Environment } from '@hubspot/local-dev-lib/types/Accounts';
3
3
  import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
4
- import { V2Sandbox } from '@hubspot/local-dev-lib/types/Sandbox';
4
+ import { SandboxResponse, V2Sandbox } from '@hubspot/local-dev-lib/types/Sandbox';
5
5
  import { SandboxAccountType } from '../types/Sandboxes.js';
6
6
  export declare function saveAccountToConfig(accountId: number | undefined, accountName: string, env: Environment, personalAccessKey?: string, force?: boolean): Promise<string>;
7
7
  export declare function createDeveloperTestAccountV2(parentAccountId: number, testAccountConfig: DeveloperTestAccountConfig): Promise<{
@@ -10,6 +10,11 @@ export declare function createDeveloperTestAccountV2(parentAccountId: number, te
10
10
  personalAccessKey?: string;
11
11
  }>;
12
12
  export declare function buildDeveloperTestAccount(testAccountName: string, parentAccountConfig: HubSpotConfigAccount, env: Environment, portalLimit: number, useV2?: boolean): Promise<number>;
13
+ type SandboxAccount = SandboxResponse & {
14
+ name: string;
15
+ };
16
+ export declare function buildSandbox(sandboxName: string, parentAccountConfig: HubSpotConfigAccount, sandboxType: SandboxAccountType, env: Environment, force?: boolean): Promise<SandboxAccount>;
13
17
  export declare function buildV2Sandbox(sandboxName: string, parentAccountConfig: HubSpotConfigAccount, sandboxType: SandboxAccountType, syncObjectRecords: boolean, env: Environment, force?: boolean): Promise<{
14
18
  sandbox: V2Sandbox;
15
19
  }>;
20
+ export {};
@@ -3,14 +3,13 @@ import { getConfigAccountIfExists, updateConfigAccount, } from '@hubspot/local-d
3
3
  import { uiLogger } from './ui/logger.js';
4
4
  import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
5
5
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
6
- import { createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
7
- import { PromptExitError } from './errors/PromptExitError.js';
6
+ import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
8
7
  import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
9
8
  import { createDeveloperTestAccountConfigPrompt } from './prompts/createDeveloperTestAccountConfigPrompt.js';
10
9
  import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
11
10
  import SpinniesManager from './ui/SpinniesManager.js';
12
11
  import { debugError, logError } from './errorHandlers/index.js';
13
- import { SANDBOX_TYPE_MAP_V2, handleSandboxCreateError } from './sandboxes.js';
12
+ import { SANDBOX_API_TYPE_MAP, SANDBOX_TYPE_MAP_V2, handleSandboxCreateError, } from './sandboxes.js';
14
13
  import { handleDeveloperTestAccountCreateError } from './developerTestAccounts.js';
15
14
  import { lib } from '../lang/en.js';
16
15
  import { poll } from './polling.js';
@@ -128,14 +127,51 @@ export async function buildDeveloperTestAccount(testAccountName, parentAccountCo
128
127
  await saveAccountToConfig(developerTestAccountId, testAccountName, env, developerTestAccountPersonalAccessKey);
129
128
  }
130
129
  catch (err) {
131
- if (err instanceof PromptExitError) {
132
- throw err;
133
- }
134
130
  logError(err);
135
131
  throw err;
136
132
  }
137
133
  return developerTestAccountId;
138
134
  }
135
+ export async function buildSandbox(sandboxName, parentAccountConfig, sandboxType, env, force = false) {
136
+ const sandboxTypeKey = sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX
137
+ ? 'standard'
138
+ : 'developer';
139
+ const parentAccountId = parentAccountConfig.accountId;
140
+ if (!parentAccountId) {
141
+ throw new Error(lib.sandbox.create[sandboxTypeKey].loading.fail(''));
142
+ }
143
+ SpinniesManager.init({
144
+ succeedColor: 'white',
145
+ });
146
+ uiLogger.log('');
147
+ SpinniesManager.add('buildSandbox', {
148
+ text: lib.sandbox.create[sandboxTypeKey].loading.add(sandboxName),
149
+ });
150
+ let sandbox;
151
+ try {
152
+ const sandboxApiType = SANDBOX_API_TYPE_MAP[sandboxType];
153
+ const { data } = await createSandbox(parentAccountId, sandboxName, sandboxApiType);
154
+ sandbox = { name: sandboxName, ...data };
155
+ SpinniesManager.succeed('buildSandbox', {
156
+ text: lib.sandbox.create[sandboxTypeKey].loading.succeed(sandboxName, sandbox.sandbox.sandboxHubId.toString()),
157
+ });
158
+ }
159
+ catch (e) {
160
+ debugError(e);
161
+ SpinniesManager.fail('buildSandbox', {
162
+ text: lib.sandbox.create[sandboxTypeKey].loading.fail(sandboxName),
163
+ });
164
+ handleSandboxCreateError(e, env, sandboxName, parentAccountId);
165
+ }
166
+ try {
167
+ await saveAccountToConfig(sandbox.sandbox.sandboxHubId, sandboxName, env, sandbox.personalAccessKey, force);
168
+ }
169
+ catch (err) {
170
+ logError(err);
171
+ throw err;
172
+ }
173
+ return sandbox;
174
+ }
139
175
  export async function buildV2Sandbox(sandboxName, parentAccountConfig, sandboxType, syncObjectRecords, env, force = false) {
140
176
  const sandboxTypeKey = sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX
141
177
  ? 'standard'
@@ -174,9 +210,6 @@ export async function buildV2Sandbox(sandboxName, parentAccountConfig, sandboxTy
174
210
  await saveAccountToConfig(sandbox.sandboxHubId, sandboxName, env, pak, force);
175
211
  }
176
212
  catch (err) {
177
- if (err instanceof PromptExitError) {
178
- throw err;
179
- }
180
213
  logError(err);
181
214
  throw err;
182
215
  }
@@ -77,6 +77,8 @@ export declare const APP_AUTH_TYPES: {
77
77
  };
78
78
  export declare const FEATURES: {
79
79
  readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
80
+ readonly SANDBOXES_V2: "sandboxes:v2:enabled";
81
+ readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
80
82
  readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
81
83
  readonly APPS_HOME: "UIE:AppHome";
82
84
  readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
package/lib/constants.js CHANGED
@@ -69,6 +69,8 @@ export const APP_AUTH_TYPES = {
69
69
  };
70
70
  export const FEATURES = {
71
71
  UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
72
+ SANDBOXES_V2: 'sandboxes:v2:enabled',
73
+ SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
72
74
  APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
73
75
  APPS_HOME: 'UIE:AppHome',
74
76
  THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
@@ -174,20 +174,6 @@ describe('lib/projects/components', () => {
174
174
  expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/package.json', 'utf-8');
175
175
  consoleSpy.mockRestore();
176
176
  });
177
- it('skips tooling config files on collision instead of renaming them', () => {
178
- const collision = {
179
- ...mockCollision,
180
- collisions: [
181
- 'src/app/cards/.prettierrc.json',
182
- 'src/app/cards/component.js',
183
- 'src/app/cards/eslint.config.js',
184
- ],
185
- };
186
- mockedFs.copyFileSync.mockImplementation(() => { });
187
- handleComponentCollision(collision);
188
- expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(1);
189
- expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/src/app/cards/component.js', '/dest/path/src/app/cards/component-2.js');
190
- });
191
177
  it('falls back to timestamp when maxAttempts is exhausted', () => {
192
178
  const collision = {
193
179
  ...mockCollision,
@@ -114,19 +114,9 @@ export function handleComponentCollision({ dest, src, collisions }) {
114
114
  }
115
115
  });
116
116
  const filenameDifferentiator = generateSafeFilenameDifferentiator(sourceFiles, hsMetaFiles);
117
+ // Exclude markdown files fromthe rename process because they should not be duplicated
117
118
  const sourceFilenameMapping = sourceFiles
118
- .filter(filename => {
119
- // Exclude markdown files from the rename process because they should not be duplicated
120
- if (filename.endsWith('.md')) {
121
- return false;
122
- }
123
- // Also exclude the tooling config we add to new projects
124
- const base = path.parse(filename).base;
125
- if (base === '.prettierrc.json' || base === 'eslint.config.js') {
126
- return false;
127
- }
128
- return true;
129
- })
119
+ .filter(filename => !filename.endsWith('.md'))
130
120
  .reduce((acc, filename) => {
131
121
  return {
132
122
  ...acc,
@@ -6,7 +6,6 @@ import { EXIT_CODES } from '../../enums/exitCodes.js';
6
6
  import { isAppIRNode } from '../../projects/structure.js';
7
7
  import { uiLine } from '../../ui/index.js';
8
8
  import { logError } from '../../errorHandlers/index.js';
9
- import { PromptExitError } from '../../errors/PromptExitError.js';
10
9
  import { installAppAutoPrompt, installAppBrowserPrompt, } from '../../prompts/installAppPrompt.js';
11
10
  import { confirmPrompt } from '../../prompts/promptUtils.js';
12
11
  import { lib } from '../../../lang/en.js';
@@ -314,9 +313,6 @@ class AppDevModeInterface {
314
313
  }
315
314
  }
316
315
  catch (e) {
317
- if (e instanceof PromptExitError) {
318
- throw e;
319
- }
320
316
  if (SpinniesManager.pick('fetchAppData')) {
321
317
  SpinniesManager.fail('fetchAppData', {
322
318
  text: lib.AppDevModeInterface.fetchAppData.error,
@@ -15,7 +15,6 @@ import { componentIsApp, componentIsPublicApp, CONFIG_FILES, getAppCardConfigs,
15
15
  import { ComponentTypes, } from '../../../types/Projects.js';
16
16
  import { UI_COLORS, uiCommandReference, uiAccountDescription, uiLink, uiLine, } from '../../ui/index.js';
17
17
  import { logError } from '../../errorHandlers/index.js';
18
- import { PromptExitError } from '../../errors/PromptExitError.js';
19
18
  import { installAppBrowserPrompt } from '../../prompts/installAppPrompt.js';
20
19
  import { confirmPrompt } from '../../prompts/promptUtils.js';
21
20
  import { handleKeypress } from '../../process.js';
@@ -90,9 +89,6 @@ class LocalDevManager_DEPRECATED {
90
89
  await this.checkPublicAppInstallation();
91
90
  }
92
91
  catch (e) {
93
- if (e instanceof PromptExitError) {
94
- throw e;
95
- }
96
92
  logError(e);
97
93
  }
98
94
  }
@@ -1,5 +1,5 @@
1
1
  import { HUBSPOT_ACCOUNT_TYPE_STRINGS } from '@hubspot/local-dev-lib/constants/config';
2
- import { getConfigAccountIfExists } from '@hubspot/local-dev-lib/config';
2
+ import { getConfigAccountById, getConfigAccountIfExists, } from '@hubspot/local-dev-lib/config';
3
3
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
4
4
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
5
5
  import { isMissingScopeError } from '@hubspot/local-dev-lib/errors/index';
@@ -18,10 +18,12 @@ import { selectDeveloperTestTargetAccountPrompt } from '../../../prompts/project
18
18
  import { selectSandboxTargetAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
19
19
  import { validateSandboxUsageLimits } from '../../../sandboxes.js';
20
20
  import { logError } from '../../../errorHandlers/index.js';
21
+ import { syncSandbox } from '../../../sandboxSync.js';
22
+ import { getAvailableSyncTypes } from '../../../sandboxes.js';
21
23
  import { hubspotAccountNamePrompt } from '../../../prompts/accountNamePrompt.js';
22
24
  import { trackCommandMetadataUsage } from '../../../usageTracking.js';
23
25
  import { validateDevTestAccountUsageLimits } from '../../../developerTestAccounts.js';
24
- import { buildDeveloperTestAccount, saveAccountToConfig, buildV2Sandbox, } from '../../../buildAccount.js';
26
+ import { buildSandbox, buildDeveloperTestAccount, saveAccountToConfig, } from '../../../buildAccount.js';
25
27
  import { debugError } from '../../../errorHandlers/index.js';
26
28
  import { listPrompt } from '../../../prompts/promptUtils.js';
27
29
  import { confirmUseExistingDeveloperTestAccountPrompt } from '../../../prompts/projectDevTargetAccountPrompt.js';
@@ -117,9 +119,13 @@ export async function createSandboxForLocalDev(accountId, accountConfig, env) {
117
119
  accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
118
120
  });
119
121
  trackCommandMetadataUsage('sandbox-create', { step: 'project-dev' }, accountId);
120
- const result = await buildV2Sandbox(name, accountConfig, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, false, // syncObjectRecords
121
- env);
122
- return result.sandbox.sandboxHubId;
122
+ const result = await buildSandbox(name, accountConfig, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, env);
123
+ const targetAccountId = result.sandbox.sandboxHubId;
124
+ const sandboxAccountConfig = getConfigAccountById(result.sandbox.sandboxHubId);
125
+ const syncTasks = await getAvailableSyncTypes(accountConfig, sandboxAccountConfig);
126
+ // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically
127
+ await syncSandbox(sandboxAccountConfig, accountConfig, env, syncTasks, true);
128
+ return targetAccountId;
123
129
  }
124
130
  catch (err) {
125
131
  logError(err);
@@ -2,22 +2,21 @@ import { promptUser } from './promptUtils.js';
2
2
  import { getConfigAccountIfExists } from '@hubspot/local-dev-lib/config';
3
3
  import { fetchProjects } from '@hubspot/local-dev-lib/api/projects';
4
4
  import { logError, ApiErrorContext } from '../errorHandlers/index.js';
5
- import { lib } from '../../lang/en.js';
6
- import { PromptExitError } from '../errors/PromptExitError.js';
7
- import { EXIT_CODES } from '../enums/exitCodes.js';
8
5
  import { uiLogger } from '../ui/logger.js';
6
+ import { EXIT_CODES } from '../enums/exitCodes.js';
7
+ import { lib } from '../../lang/en.js';
9
8
  async function createProjectsList(accountId) {
10
- if (!accountId) {
11
- uiLogger.error(lib.prompts.downloadProjectPrompt.errors.accountIdRequired);
12
- throw new PromptExitError(lib.prompts.downloadProjectPrompt.errors.accountIdRequired, EXIT_CODES.ERROR);
13
- }
14
9
  try {
15
- const { data: projects } = await fetchProjects(accountId);
16
- return projects.results;
10
+ if (accountId) {
11
+ const { data: projects } = await fetchProjects(accountId);
12
+ return projects.results;
13
+ }
14
+ uiLogger.error(lib.prompts.downloadProjectPrompt.errors.accountIdRequired);
15
+ process.exit(EXIT_CODES.ERROR);
17
16
  }
18
17
  catch (e) {
19
- logError(e, new ApiErrorContext({ accountId }));
20
- throw new PromptExitError('Failed to fetch projects', EXIT_CODES.ERROR);
18
+ logError(e, accountId ? new ApiErrorContext({ accountId }) : undefined);
19
+ process.exit(EXIT_CODES.ERROR);
21
20
  }
22
21
  }
23
22
  export async function downloadProjectPrompt(promptOptions) {
@@ -1,9 +1,8 @@
1
1
  import open from 'open';
2
2
  import { promptUser } from './promptUtils.js';
3
+ import { EXIT_CODES } from '../enums/exitCodes.js';
3
4
  import { lib } from '../../lang/en.js';
4
5
  import { uiLogger } from '../ui/logger.js';
5
- import { PromptExitError } from '../errors/PromptExitError.js';
6
- import { EXIT_CODES } from '../enums/exitCodes.js';
7
6
  export async function installAppBrowserPrompt(installUrl, isReinstall = false) {
8
7
  uiLogger.log('');
9
8
  if (isReinstall) {
@@ -21,7 +20,7 @@ export async function installAppBrowserPrompt(installUrl, isReinstall = false) {
21
20
  });
22
21
  if (!isReinstall && !shouldOpenBrowser) {
23
22
  uiLogger.log(lib.prompts.installAppPrompt.decline);
24
- throw new PromptExitError(lib.prompts.installAppPrompt.decline, EXIT_CODES.SUCCESS);
23
+ process.exit(EXIT_CODES.SUCCESS);
25
24
  }
26
25
  else if (!shouldOpenBrowser) {
27
26
  return;
@@ -6,9 +6,8 @@ import { uiLogger } from '../ui/logger.js';
6
6
  import { promptUser } from './promptUtils.js';
7
7
  import { getCliAccountNamePromptConfig, } from './accountNamePrompt.js';
8
8
  import { uiInfoSection } from '../ui/index.js';
9
- import { lib } from '../../lang/en.js';
10
- import { PromptExitError } from '../errors/PromptExitError.js';
11
9
  import { EXIT_CODES } from '../enums/exitCodes.js';
10
+ import { lib } from '../../lang/en.js';
12
11
  /**
13
12
  * Displays notification to user that we are about to open the browser,
14
13
  * then opens their browser to the personal-access-key shortlink
@@ -28,7 +27,7 @@ export async function personalAccessKeyPrompt({ env, account, }) {
28
27
  ]);
29
28
  if (!choice) {
30
29
  deleteConfigFileIfEmpty();
31
- throw new PromptExitError(lib.prompts.personalAccessKeyPrompt.errors.authCancelled, EXIT_CODES.SUCCESS);
30
+ process.exit(EXIT_CODES.SUCCESS);
32
31
  }
33
32
  if (choice ===
34
33
  lib.prompts.personalAccessKeyPrompt.personalAccessKeyPromptChoices