@hubspot/cli 7.8.0-beta.0 → 7.8.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 (90) hide show
  1. package/commands/__tests__/project.test.js +2 -0
  2. package/commands/account/auth.js +1 -0
  3. package/commands/auth.js +1 -0
  4. package/commands/feedback.js +1 -1
  5. package/commands/project/__tests__/add.test.js +12 -12
  6. package/commands/project/__tests__/list.test.js +31 -0
  7. package/commands/project/__tests__/migrate.test.js +1 -0
  8. package/commands/project/add.d.ts +2 -2
  9. package/commands/project/add.js +3 -2
  10. package/commands/project/create.js +1 -1
  11. package/commands/project/dev/deprecatedFlow.js +2 -2
  12. package/commands/project/dev/index.js +5 -5
  13. package/commands/project/dev/unifiedFlow.js +6 -3
  14. package/commands/project/download.js +5 -2
  15. package/commands/project/installDeps.d.ts +2 -2
  16. package/commands/project/installDeps.js +1 -0
  17. package/commands/project/list.d.ts +4 -0
  18. package/commands/project/list.js +62 -0
  19. package/commands/project/migrate.js +5 -2
  20. package/commands/project.js +2 -0
  21. package/commands/sandbox/delete.js +5 -2
  22. package/commands/testAccount/create.js +2 -2
  23. package/commands/theme/preview.js +1 -4
  24. package/lang/en.d.ts +49 -14
  25. package/lang/en.js +121 -86
  26. package/lang/en.lyaml +2 -2
  27. package/lib/__tests__/buildAccount.test.js +2 -2
  28. package/lib/app/migrate.js +1 -1
  29. package/lib/buildAccount.d.ts +2 -2
  30. package/lib/buildAccount.js +7 -7
  31. package/lib/configMigrate.js +88 -9
  32. package/lib/constants.d.ts +8 -1
  33. package/lib/constants.js +8 -1
  34. package/lib/doctor/Doctor.js +2 -2
  35. package/lib/errorHandlers/suppressError.js +2 -2
  36. package/lib/middleware/commandTargetingUtils.d.ts +1 -1
  37. package/lib/middleware/commandTargetingUtils.js +16 -20
  38. package/lib/projects/__tests__/AppDevModeInterface.test.js +85 -90
  39. package/lib/projects/__tests__/LocalDevProcess.test.js +6 -5
  40. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  41. package/lib/projects/__tests__/deploy.test.js +9 -9
  42. package/lib/projects/__tests__/upload.test.js +2 -2
  43. package/lib/projects/add/__tests__/{v3AddComponent.test.js → v2AddComponent.test.js} +35 -35
  44. package/lib/projects/add/{v3AddComponent.d.ts → v2AddComponent.d.ts} +1 -1
  45. package/lib/projects/add/{v3AddComponent.js → v2AddComponent.js} +5 -5
  46. package/lib/projects/create/__tests__/v2.test.d.ts +1 -0
  47. package/lib/projects/create/__tests__/{v3.test.js → v2.test.js} +2 -2
  48. package/lib/projects/create/index.js +2 -2
  49. package/lib/projects/create/{v3.d.ts → v2.d.ts} +3 -3
  50. package/lib/projects/create/{v3.js → v2.js} +3 -3
  51. package/lib/projects/deploy.d.ts +1 -1
  52. package/lib/projects/deploy.js +2 -2
  53. package/lib/projects/localDev/AppDevModeInterface.d.ts +8 -1
  54. package/lib/projects/localDev/AppDevModeInterface.js +106 -86
  55. package/lib/projects/localDev/DevServerManager.d.ts +11 -29
  56. package/lib/projects/localDev/DevServerManager.js +19 -61
  57. package/lib/projects/localDev/DevServerManager_DEPRECATED.d.ts +40 -0
  58. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +120 -0
  59. package/lib/projects/localDev/{LocalDevManager.js → LocalDevManager_DEPRECATED.js} +6 -6
  60. package/lib/projects/localDev/LocalDevProcess.js +3 -2
  61. package/lib/projects/localDev/LocalDevState.d.ts +3 -0
  62. package/lib/projects/localDev/LocalDevState.js +9 -0
  63. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +4 -0
  64. package/lib/projects/localDev/LocalDevWebsocketServer.js +34 -2
  65. package/lib/projects/localDev/helpers/account.d.ts +1 -1
  66. package/lib/projects/localDev/helpers/account.js +2 -2
  67. package/lib/projects/localDev/helpers/project.js +2 -3
  68. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +3 -0
  69. package/lib/projects/localDev/localDevWebsocketServerUtils.js +9 -0
  70. package/lib/projects/urls.d.ts +0 -1
  71. package/lib/projects/urls.js +0 -3
  72. package/lib/prompts/__tests__/downloadProjectPrompt.test.js +1 -0
  73. package/lib/prompts/__tests__/projectAddPrompt.test.js +10 -10
  74. package/lib/prompts/installAppPrompt.d.ts +1 -6
  75. package/lib/prompts/installAppPrompt.js +1 -6
  76. package/lib/prompts/projectAddPrompt.d.ts +2 -2
  77. package/lib/prompts/projectAddPrompt.js +1 -1
  78. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -1
  79. package/lib/theme/__tests__/migrate.test.js +4 -4
  80. package/lib/ui/index.d.ts +4 -0
  81. package/lib/ui/index.js +9 -1
  82. package/lib/ui/uiMessages.d.ts +4 -0
  83. package/lib/ui/uiMessages.js +4 -0
  84. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  85. package/package.json +6 -5
  86. package/lib/projects/localDev/DevServerManagerV2.d.ts +0 -22
  87. package/lib/projects/localDev/DevServerManagerV2.js +0 -81
  88. /package/{lib/projects/add/__tests__/v3AddComponent.test.d.ts → commands/project/__tests__/list.test.d.ts} +0 -0
  89. /package/lib/projects/{create/__tests__/v3.test.d.ts → add/__tests__/v2AddComponent.test.d.ts} +0 -0
  90. /package/lib/projects/localDev/{LocalDevManager.d.ts → LocalDevManager_DEPRECATED.d.ts} +0 -0
@@ -11,7 +11,7 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => {
11
11
  };
12
12
  });
13
13
  import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth';
14
- import { fetchPublicAppsForPortal, fetchPublicAppProductionInstallCounts, installStaticAuthAppOnTestAccount, } from '@hubspot/local-dev-lib/api/appsDev';
14
+ import { fetchAppMetadataByUid, fetchPublicAppProductionInstallCounts, installStaticAuthAppOnTestAccount, } from '@hubspot/local-dev-lib/api/appsDev';
15
15
  import { DevModeUnifiedInterface as UIEDevModeInterface } from '@hubspot/ui-extensions-dev-server';
16
16
  import { requestPorts } from '@hubspot/local-dev-lib/portManager';
17
17
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
@@ -109,8 +109,8 @@ describe('AppDevModeInterface', () => {
109
109
  LocalDevState.mockImplementation(() => mockLocalDevState);
110
110
  LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
111
111
  // Mock external dependencies
112
- fetchPublicAppsForPortal.mockResolvedValue({
113
- data: { results: [mockPublicApp] },
112
+ fetchAppMetadataByUid.mockResolvedValue({
113
+ data: mockPublicApp,
114
114
  });
115
115
  fetchPublicAppProductionInstallCounts.mockResolvedValue({
116
116
  data: { uniquePortalInstallCount: 5 },
@@ -129,7 +129,16 @@ describe('AppDevModeInterface', () => {
129
129
  getOauthAppInstallUrl.mockReturnValue('http://oauth-install-url');
130
130
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
131
131
  installAppAutoPrompt.mockResolvedValue(true);
132
- installAppBrowserPrompt.mockResolvedValue(undefined);
132
+ installAppBrowserPrompt.mockImplementation(async () => {
133
+ setTimeout(() => {
134
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
135
+ if (addListenerCall) {
136
+ const callback = addListenerCall[1];
137
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
138
+ }
139
+ }, 0);
140
+ return undefined;
141
+ });
133
142
  confirmPrompt.mockResolvedValue(true);
134
143
  installStaticAuthAppOnTestAccount.mockResolvedValue(undefined);
135
144
  // Mock process.exit
@@ -191,12 +200,12 @@ describe('AppDevModeInterface', () => {
191
200
  it('should return early if no app node exists', async () => {
192
201
  mockLocalDevState.projectNodes = {};
193
202
  await appDevModeInterface.setup({});
194
- expect(fetchPublicAppsForPortal).not.toHaveBeenCalled();
203
+ expect(fetchAppMetadataByUid).not.toHaveBeenCalled();
195
204
  expect(UIEDevModeInterface.setup).not.toHaveBeenCalled();
196
205
  });
197
206
  it('should setup successfully with private app', async () => {
198
207
  await appDevModeInterface.setup({});
199
- expect(fetchPublicAppsForPortal).toHaveBeenCalledWith(12345);
208
+ expect(fetchAppMetadataByUid).toHaveBeenCalledWith('test-app-uid', 12345);
200
209
  expect(fetchPublicAppProductionInstallCounts).toHaveBeenCalledWith(123, 12345);
201
210
  expect(fetchAppInstallationData).toHaveBeenCalledWith(67890, 999, 'test-app-uid', ['test-scope'], []);
202
211
  expect(UIEDevModeInterface.setup).toHaveBeenCalled();
@@ -242,21 +251,16 @@ describe('AppDevModeInterface', () => {
242
251
  await newAppDevModeInterface.setup({});
243
252
  expect(process.exit).toHaveBeenCalledWith(0);
244
253
  });
245
- // @TODO: Restore test account auto install functionality
246
- // it('should auto-install static auth app on test account', async () => {
247
- // (fetchAppInstallationData as Mock).mockResolvedValue({
248
- // data: {
249
- // isInstalledWithScopeGroups: false,
250
- // previouslyAuthorizedScopeGroups: [],
251
- // },
252
- // });
253
- // await appDevModeInterface.setup({});
254
- // expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
255
- // 123,
256
- // 67890,
257
- // [1, 2, 3]
258
- // );
259
- // });
254
+ it('should auto-install static auth app on test account', async () => {
255
+ fetchAppInstallationData.mockResolvedValue({
256
+ data: {
257
+ isInstalledWithScopeGroups: false,
258
+ previouslyAuthorizedScopeGroups: [],
259
+ },
260
+ });
261
+ await appDevModeInterface.setup({});
262
+ expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
263
+ });
260
264
  it('should open browser for OAuth app installation', async () => {
261
265
  const oauthAppNode = {
262
266
  ...mockAppNode,
@@ -295,81 +299,47 @@ describe('AppDevModeInterface', () => {
295
299
  },
296
300
  });
297
301
  await appDevModeInterface.setup({});
298
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
299
- appUid: 'test-app-uid',
300
- projectAccountId: 12345,
301
- projectName: 'test-project',
302
- testingAccountId: 67890,
303
- });
302
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
304
303
  });
305
304
  it('should handle errors during setup', async () => {
306
305
  const error = new Error('Setup failed');
307
- fetchPublicAppsForPortal.mockRejectedValue(error);
306
+ fetchAppMetadataByUid.mockRejectedValue(error);
308
307
  await appDevModeInterface.setup({});
309
308
  expect(logError).toHaveBeenCalledWith(error);
310
309
  });
311
- it('should exit if app not found in portal', async () => {
312
- // Set up conditions for non-automatic installation to force getAppInstallUrl call
313
- getAccountConfig.mockReturnValue(null);
314
- // First call for fetchAppData succeeds
315
- fetchPublicAppsForPortal
316
- .mockResolvedValueOnce({
317
- data: { results: [mockPublicApp] },
318
- })
319
- // Second call for getAppInstallUrl fails (app not found)
320
- .mockResolvedValueOnce({
321
- data: { results: [] },
310
+ it('should exit if user declines auto-install', async () => {
311
+ // Set up conditions for automatic installation
312
+ getAccountConfig.mockReturnValue({
313
+ parentAccountId: 12345, // matches targetProjectAccountId
322
314
  });
315
+ isDeveloperTestAccount.mockReturnValue(true);
323
316
  fetchAppInstallationData.mockResolvedValue({
324
317
  data: {
325
318
  isInstalledWithScopeGroups: false,
326
319
  previouslyAuthorizedScopeGroups: [],
327
320
  },
328
321
  });
322
+ installAppAutoPrompt.mockResolvedValue(false);
323
+ // Create a new instance to trigger the exit during setup
324
+ const newAppDevModeInterface = new AppDevModeInterface({
325
+ localDevState: mockLocalDevState,
326
+ localDevLogger: mockLocalDevLogger,
327
+ });
329
328
  // The setup method catches the error, so we check that process.exit was called
329
+ await newAppDevModeInterface.setup({});
330
+ expect(process.exit).toHaveBeenCalledWith(0);
331
+ });
332
+ it('should fallback to browser install if auto-install fails', async () => {
333
+ fetchAppInstallationData.mockResolvedValue({
334
+ data: {
335
+ isInstalledWithScopeGroups: false,
336
+ previouslyAuthorizedScopeGroups: [],
337
+ },
338
+ });
339
+ installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
330
340
  await appDevModeInterface.setup({});
331
- expect(process.exit).toHaveBeenCalledWith(1);
332
- });
333
- // @TODO: Restore test account auto install functionality
334
- // it('should exit if user declines auto-install', async () => {
335
- // // Set up conditions for automatic installation
336
- // (getAccountConfig as Mock).mockReturnValue({
337
- // parentAccountId: 12345, // matches targetProjectAccountId
338
- // });
339
- // (isDeveloperTestAccount as Mock).mockReturnValue(true);
340
- // (fetchAppInstallationData as Mock).mockResolvedValue({
341
- // data: {
342
- // isInstalledWithScopeGroups: false,
343
- // previouslyAuthorizedScopeGroups: [],
344
- // },
345
- // });
346
- // (installAppAutoPrompt as Mock).mockResolvedValue(false);
347
- // // Create a new instance to trigger the exit during setup
348
- // const newAppDevModeInterface = new AppDevModeInterface({
349
- // localDevState: mockLocalDevState,
350
- // localDevLogger: mockLocalDevLogger,
351
- // });
352
- // // The setup method catches the error, so we check that process.exit was called
353
- // await newAppDevModeInterface.setup({});
354
- // expect(process.exit).toHaveBeenCalledWith(0);
355
- // });
356
- // @TODO: Restore test account auto install functionality
357
- // it('should fallback to browser install if auto-install fails', async () => {
358
- // (fetchAppInstallationData as Mock).mockResolvedValue({
359
- // data: {
360
- // isInstalledWithScopeGroups: false,
361
- // previouslyAuthorizedScopeGroups: [],
362
- // },
363
- // });
364
- // (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
365
- // new Error('Install failed')
366
- // );
367
- // await appDevModeInterface.setup({});
368
- // expect(installAppBrowserPrompt).toHaveBeenCalledWith(
369
- // 'http://static-install-url',
370
- // false
371
- // );
372
- // });
341
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
342
+ });
373
343
  });
374
344
  describe('start()', () => {
375
345
  it('should return early if no app node exists', async () => {
@@ -426,14 +396,21 @@ describe('AppDevModeInterface', () => {
426
396
  // Reset mocks to ensure clean state
427
397
  vi.clearAllMocks();
428
398
  // Set up basic mocks
429
- fetchPublicAppsForPortal.mockResolvedValue({
430
- data: { results: [mockPublicApp] },
399
+ fetchAppMetadataByUid.mockResolvedValue({
400
+ data: mockPublicApp,
431
401
  });
432
402
  fetchPublicAppProductionInstallCounts.mockResolvedValue({
433
403
  data: { uniquePortalInstallCount: 5 },
434
404
  });
435
405
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
436
- installAppBrowserPrompt.mockResolvedValue(undefined);
406
+ installAppBrowserPrompt.mockImplementation(async () => {
407
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
408
+ if (addListenerCall) {
409
+ const callback = addListenerCall[1];
410
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
411
+ }
412
+ return undefined;
413
+ });
437
414
  // Reset the mock LocalDevState
438
415
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
439
416
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -459,14 +436,23 @@ describe('AppDevModeInterface', () => {
459
436
  // Reset mocks to ensure clean state
460
437
  vi.clearAllMocks();
461
438
  // Set up basic mocks
462
- fetchPublicAppsForPortal.mockResolvedValue({
463
- data: { results: [mockPublicApp] },
439
+ fetchAppMetadataByUid.mockResolvedValue({
440
+ data: mockPublicApp,
464
441
  });
465
442
  fetchPublicAppProductionInstallCounts.mockResolvedValue({
466
443
  data: { uniquePortalInstallCount: 5 },
467
444
  });
468
445
  getOauthAppInstallUrl.mockReturnValue('http://oauth-install-url');
469
- installAppBrowserPrompt.mockResolvedValue(undefined);
446
+ installAppBrowserPrompt.mockImplementation(async () => {
447
+ setTimeout(() => {
448
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
449
+ if (addListenerCall) {
450
+ const callback = addListenerCall[1];
451
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
452
+ }
453
+ }, 0);
454
+ return undefined;
455
+ });
470
456
  // Reset the mock LocalDevState
471
457
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
472
458
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -503,14 +489,23 @@ describe('AppDevModeInterface', () => {
503
489
  // Reset mocks to ensure clean state
504
490
  vi.clearAllMocks();
505
491
  // Set up basic mocks
506
- fetchPublicAppsForPortal.mockResolvedValue({
507
- data: { results: [mockPublicApp] },
492
+ fetchAppMetadataByUid.mockResolvedValue({
493
+ data: mockPublicApp,
508
494
  });
509
495
  fetchPublicAppProductionInstallCounts.mockResolvedValue({
510
496
  data: { uniquePortalInstallCount: 5 },
511
497
  });
512
498
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
513
- installAppBrowserPrompt.mockResolvedValue(undefined);
499
+ installAppBrowserPrompt.mockImplementation(async () => {
500
+ setTimeout(() => {
501
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
502
+ if (addListenerCall) {
503
+ const callback = addListenerCall[1];
504
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
505
+ }
506
+ }, 0);
507
+ return undefined;
508
+ });
514
509
  // Reset the mock LocalDevState
515
510
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
516
511
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -7,7 +7,7 @@ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
7
7
  import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
8
8
  import LocalDevProcess from '../localDev/LocalDevProcess.js';
9
9
  import LocalDevLogger from '../localDev/LocalDevLogger.js';
10
- import DevServerManagerV2 from '../localDev/DevServerManagerV2.js';
10
+ import DevServerManager from '../localDev/DevServerManager.js';
11
11
  import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
12
12
  import { vi } from 'vitest';
13
13
  // Mock @hubspot/ui-extensions-dev-server
@@ -27,7 +27,7 @@ vi.mock('../config');
27
27
  vi.mock('@hubspot/local-dev-lib/api/projects');
28
28
  vi.mock('@hubspot/local-dev-lib/errors/index');
29
29
  vi.mock('../localDev/LocalDevLogger');
30
- vi.mock('../localDev/DevServerManagerV2');
30
+ vi.mock('../localDev/DevServerManager');
31
31
  // Tests for LocalDevProcess and LocalDevState
32
32
  describe('LocalDevProcess', () => {
33
33
  let mockLocalDevLogger;
@@ -67,6 +67,7 @@ describe('LocalDevProcess', () => {
67
67
  subbuildStatuses: [],
68
68
  uploadMessage: 'Build completed',
69
69
  autoDeployId: 0,
70
+ platformVersion: '2025.2',
70
71
  },
71
72
  },
72
73
  initialProjectNodes: {},
@@ -104,7 +105,7 @@ describe('LocalDevProcess', () => {
104
105
  };
105
106
  // Mock constructors
106
107
  LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
107
- DevServerManagerV2.mockImplementation(() => mockDevServerManager);
108
+ DevServerManager.mockImplementation(() => mockDevServerManager);
108
109
  // Mock external functions
109
110
  isHubSpotHttpError.mockReturnValue(false);
110
111
  // Create process instance
@@ -398,7 +399,7 @@ describe('LocalDevProcess', () => {
398
399
  expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
399
400
  'test-project', // projectName
400
401
  123, // buildId
401
- true, // useV3Api
402
+ true, // useV2Api
402
403
  false // force
403
404
  );
404
405
  expect(mockLocalDevLogger.deploySuccess).toHaveBeenCalled();
@@ -426,7 +427,7 @@ describe('LocalDevProcess', () => {
426
427
  expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
427
428
  'test-project', // projectName
428
429
  123, // buildId
429
- true, // useV3Api
430
+ true, // useV2Api
430
431
  true // force
431
432
  );
432
433
  expect(result).toEqual({
@@ -252,7 +252,7 @@ describe('LocalDevWebsocketServer', () => {
252
252
  expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
253
253
  expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
254
254
  // Each connection should trigger state listener setup
255
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(9); // 3 listeners per connection * 3 connections
255
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12); // 4 listeners per connection * 3 connections
256
256
  // Each connection should trigger dev server message
257
257
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
258
258
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
@@ -308,16 +308,16 @@ describe('LocalDevWebsocketServer', () => {
308
308
  const closeCallbacks2 = mockWebSocket2.on.mock.calls
309
309
  .filter(call => call[0] === 'close')
310
310
  .map(call => call[1]);
311
- expect(closeCallbacks1).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
312
- expect(closeCallbacks2).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
311
+ expect(closeCallbacks1).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
312
+ expect(closeCallbacks2).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
313
313
  // Simulate first connection closing (call all close callbacks)
314
314
  closeCallbacks1.forEach(callback => callback());
315
- // Should have removed listeners for first connection (3 listeners: projectNodes, appData, and uploadWarnings)
316
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
315
+ // Should have removed listeners for first connection (4 listeners: projectNodes, appData, devServersStarted, and uploadWarnings)
316
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
317
317
  // Simulate second connection closing
318
318
  closeCallbacks2.forEach(callback => callback());
319
319
  // Should have removed listeners for second connection as well
320
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(6);
320
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(8);
321
321
  });
322
322
  it('should broadcast state changes to all connected clients', () => {
323
323
  // Establish connections
@@ -99,7 +99,7 @@ describe('lib/projects/deploy', () => {
99
99
  const targetAccountId = 12345;
100
100
  const projectName = 'test-project';
101
101
  const buildId = 5;
102
- const useV3Api = true;
102
+ const useV2Api = true;
103
103
  const force = false;
104
104
  it('successfully deploys and returns deploy result', async () => {
105
105
  const mockDeployResponseData = {
@@ -126,8 +126,8 @@ describe('lib/projects/deploy', () => {
126
126
  data: mockDeployResponseData,
127
127
  });
128
128
  mockPollDeployStatus.mockResolvedValue(mockDeployResult);
129
- const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
130
- expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
129
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
130
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV2Api, force);
131
131
  expect(deploy).toEqual(mockDeployResult);
132
132
  });
133
133
  it('handles blocked deploy with warnings', async () => {
@@ -150,7 +150,7 @@ describe('lib/projects/deploy', () => {
150
150
  mockDeployProject.mockResolvedValue({
151
151
  data: mockBlockedResponse,
152
152
  });
153
- await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
153
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
154
154
  expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
155
155
  });
156
156
  it('handles blocked deploy with errors (cannot be forced)', async () => {
@@ -173,7 +173,7 @@ describe('lib/projects/deploy', () => {
173
173
  mockDeployProject.mockResolvedValue({
174
174
  data: mockBlockedResponse,
175
175
  });
176
- await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
176
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
177
177
  expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deployBlockedHeader);
178
178
  expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentWarning('component-1', 'module', 'This is an error'));
179
179
  });
@@ -192,19 +192,19 @@ describe('lib/projects/deploy', () => {
192
192
  mockDeployProject.mockResolvedValue({
193
193
  data: mockBlockedResponse,
194
194
  });
195
- await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
195
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
196
196
  expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
197
197
  expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentGeneric('component-1', 'module'));
198
198
  });
199
199
  it('handles general deploy failure', async () => {
200
200
  mockDeployProject.mockResolvedValue({ data: null });
201
- const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
201
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
202
202
  expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
203
203
  expect(deploy).toBeUndefined();
204
204
  });
205
205
  it('handles undefined deploy response', async () => {
206
206
  mockDeployProject.mockResolvedValue({ data: undefined });
207
- const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
207
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
208
208
  expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
209
209
  expect(deploy).toBeUndefined();
210
210
  });
@@ -221,7 +221,7 @@ describe('lib/projects/deploy', () => {
221
221
  });
222
222
  mockPollDeployStatus.mockResolvedValue({});
223
223
  await handleProjectDeploy(targetAccountId, projectName, buildId, false, true);
224
- expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, false, // useV3Api
224
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, false, // useV2Api
225
225
  true // force
226
226
  );
227
227
  });
@@ -36,7 +36,7 @@ describe('lib/projects/upload', () => {
36
36
  await expect(validateSourceDirectory(srcDir, projectConfig, tempDir)).rejects.toThrow(ProjectValidationError);
37
37
  expect(walk).toHaveBeenCalledWith(srcDir, ['node_modules']);
38
38
  });
39
- it('should warn about legacy files in V3 projects', async () => {
39
+ it('should warn about legacy files in V2 projects', async () => {
40
40
  vi.mocked(isV2Project).mockReturnValue(true);
41
41
  const legacyFilePath = path.join(srcDir, 'app', 'serverless.json');
42
42
  vi.mocked(walk).mockResolvedValue([legacyFilePath]);
@@ -67,7 +67,7 @@ describe('lib/projects/upload', () => {
67
67
  await validateSourceDirectory(srcDir, projectConfig, tempDir);
68
68
  expect(uiLogger.warn).not.toHaveBeenCalled();
69
69
  });
70
- it('should not warn about legacy files in non-V3 projects', async () => {
70
+ it('should not warn about legacy files in non-V2 projects', async () => {
71
71
  vi.mocked(isV2Project).mockReturnValue(false);
72
72
  projectConfig.platformVersion = '2025.1';
73
73
  const filePaths = [
@@ -1,9 +1,9 @@
1
1
  import fs from 'fs';
2
- import { v3AddComponent } from '../v3AddComponent.js';
2
+ import { v2AddComponent } from '../v2AddComponent.js';
3
3
  import { getConfigForPlatformVersion } from '../../create/legacy.js';
4
- import { createV3App } from '../../create/v3.js';
4
+ import { createV2App } from '../../create/v2.js';
5
5
  import { confirmPrompt } from '../../../prompts/promptUtils.js';
6
- import { projectAddPromptV3 } from '../../../prompts/projectAddPrompt.js';
6
+ import { projectAddPromptV2 } from '../../../prompts/projectAddPrompt.js';
7
7
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
8
8
  import { uiLogger } from '../../../ui/logger.js';
9
9
  import { getProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
@@ -12,7 +12,7 @@ import { commands } from '../../../../lang/en.js';
12
12
  vi.mock('fs');
13
13
  vi.mock('../../../prompts/promptUtils');
14
14
  vi.mock('../../create/legacy');
15
- vi.mock('../../create/v3');
15
+ vi.mock('../../create/v2');
16
16
  vi.mock('../../../prompts/projectAddPrompt');
17
17
  vi.mock('@hubspot/local-dev-lib/github');
18
18
  vi.mock('../../../ui/logger.js');
@@ -21,17 +21,17 @@ vi.mock('../../../usageTracking');
21
21
  const mockedFs = vi.mocked(fs);
22
22
  const mockedGetConfigForPlatformVersion = vi.mocked(getConfigForPlatformVersion);
23
23
  const mockedConfirmPrompt = vi.mocked(confirmPrompt);
24
- const mockedCreateV3App = vi.mocked(createV3App);
25
- const mockedProjectAddPromptV3 = vi.mocked(projectAddPromptV3);
24
+ const mockedCreateV2App = vi.mocked(createV2App);
25
+ const mockedProjectAddPromptV2 = vi.mocked(projectAddPromptV2);
26
26
  const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
27
27
  const mockedUiLogger = vi.mocked(uiLogger);
28
28
  const mockedGetProjectMetadata = vi.mocked(getProjectMetadata);
29
29
  const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
30
- describe('lib/projects/add/v3AddComponent', () => {
30
+ describe('lib/projects/add/v2AddComponent', () => {
31
31
  const mockProjectConfig = {
32
32
  name: 'test-project',
33
33
  srcDir: 'src',
34
- platformVersion: 'v3',
34
+ platformVersion: '2025.2',
35
35
  };
36
36
  const mockArgs = {
37
37
  name: 'test-component',
@@ -66,13 +66,13 @@ describe('lib/projects/add/v3AddComponent', () => {
66
66
  },
67
67
  };
68
68
  beforeEach(() => {
69
- mockedCreateV3App.mockResolvedValue({
69
+ mockedCreateV2App.mockResolvedValue({
70
70
  authType: 'oauth',
71
71
  distribution: 'private',
72
72
  });
73
73
  mockedTrackCommandUsage.mockResolvedValue();
74
74
  });
75
- describe('v3AddComponent()', () => {
75
+ describe('v2AddComponent()', () => {
76
76
  it('successfully adds a component when app already exists', async () => {
77
77
  const mockAppMeta = {
78
78
  config: {
@@ -86,17 +86,17 @@ describe('lib/projects/add/v3AddComponent', () => {
86
86
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
87
87
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
88
88
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
89
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
89
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
90
90
  mockedCloneGithubRepo.mockResolvedValue(true);
91
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
92
- expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('v3');
91
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
92
+ expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('2025.2');
93
93
  expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
94
- expect(mockedProjectAddPromptV3).toHaveBeenCalled();
94
+ expect(mockedProjectAddPromptV2).toHaveBeenCalled();
95
95
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
96
96
  type: 'module',
97
97
  }, mockAccountId);
98
98
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
99
- sourceDir: ['v3/test-component'],
99
+ sourceDir: ['2025.2/test-component'],
100
100
  hideLogs: true,
101
101
  branch: 'main',
102
102
  }));
@@ -116,15 +116,15 @@ describe('lib/projects/add/v3AddComponent', () => {
116
116
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
117
117
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
118
118
  mockedConfirmPrompt.mockResolvedValue(true);
119
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
119
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
120
120
  mockedCloneGithubRepo.mockResolvedValue(true);
121
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
122
- expect(mockedCreateV3App).toHaveBeenCalled();
121
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
122
+ expect(mockedCreateV2App).toHaveBeenCalled();
123
123
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
124
124
  type: 'module',
125
125
  }, mockAccountId);
126
126
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
127
- sourceDir: ['v3/test-component', 'v3/app-template'],
127
+ sourceDir: ['2025.2/test-component', '2025.2/app-template'],
128
128
  }));
129
129
  });
130
130
  it('should not call clone', async () => {
@@ -145,10 +145,10 @@ describe('lib/projects/add/v3AddComponent', () => {
145
145
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
146
146
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
147
147
  mockedConfirmPrompt.mockResolvedValue(true);
148
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
148
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
149
149
  mockedCloneGithubRepo.mockResolvedValue(true);
150
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
151
- expect(mockedCreateV3App).not.toHaveBeenCalled();
150
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
151
+ expect(mockedCreateV2App).not.toHaveBeenCalled();
152
152
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
153
153
  type: '',
154
154
  }, mockAccountId);
@@ -164,7 +164,7 @@ describe('lib/projects/add/v3AddComponent', () => {
164
164
  };
165
165
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
166
166
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataMaxApps);
167
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
167
+ await expect(v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
168
168
  });
169
169
  it('throws an error when components list is empty', async () => {
170
170
  const mockEmptyConfig = {
@@ -172,7 +172,7 @@ describe('lib/projects/add/v3AddComponent', () => {
172
172
  parentComponents: [],
173
173
  };
174
174
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockEmptyConfig);
175
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
175
+ await expect(v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
176
176
  });
177
177
  it('throws an error when app meta file cannot be parsed', async () => {
178
178
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
@@ -180,7 +180,7 @@ describe('lib/projects/add/v3AddComponent', () => {
180
180
  mockedFs.readFileSync.mockImplementation(() => {
181
181
  throw new Error('File read error');
182
182
  });
183
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
183
+ await expect(v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
184
184
  });
185
185
  it('throws an error when cloning fails', async () => {
186
186
  const mockAppMeta = {
@@ -195,9 +195,9 @@ describe('lib/projects/add/v3AddComponent', () => {
195
195
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
196
196
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
197
197
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
198
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
198
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
199
199
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
200
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
200
+ await expect(v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
201
201
  });
202
202
  it('should track usage with multiple component types', async () => {
203
203
  const mockAppMeta = {
@@ -219,9 +219,9 @@ describe('lib/projects/add/v3AddComponent', () => {
219
219
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
220
220
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
221
221
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
222
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
222
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
223
223
  mockedCloneGithubRepo.mockResolvedValue(true);
224
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
224
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
225
225
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
226
226
  type: 'module,card',
227
227
  }, mockAccountId);
@@ -243,8 +243,8 @@ describe('lib/projects/add/v3AddComponent', () => {
243
243
  };
244
244
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
245
245
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
246
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
247
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
246
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
247
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
248
248
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
249
249
  type: '',
250
250
  }, mockAccountId);
@@ -270,9 +270,9 @@ describe('lib/projects/add/v3AddComponent', () => {
270
270
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
271
271
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
272
272
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
273
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
273
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
274
274
  mockedCloneGithubRepo.mockResolvedValue(true);
275
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
275
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
276
276
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
277
277
  type: 'workflow-action-tool',
278
278
  }, mockAccountId);
@@ -308,9 +308,9 @@ describe('lib/projects/add/v3AddComponent', () => {
308
308
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
309
309
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
310
310
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
311
- mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
311
+ mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
312
312
  mockedCloneGithubRepo.mockResolvedValue(true);
313
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
313
+ await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
314
314
  expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
315
315
  type: 'workflow-action-tool,module',
316
316
  }, mockAccountId);