@hubspot/cli 7.7.22-experimental.0 → 7.7.24-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/commands/account/auth.js +15 -4
  2. package/commands/auth.js +1 -1
  3. package/commands/config/set.d.ts +1 -0
  4. package/commands/config/set.js +19 -9
  5. package/commands/mcp/start.d.ts +1 -0
  6. package/commands/mcp/start.js +12 -4
  7. package/commands/project/create.js +2 -2
  8. package/commands/project/validate.js +1 -0
  9. package/commands/sandbox/__tests__/create.test.js +207 -0
  10. package/commands/sandbox/create.d.ts +1 -1
  11. package/commands/sandbox/create.js +31 -16
  12. package/commands/testAccount/createConfig.js +1 -1
  13. package/lang/en.d.ts +17 -4
  14. package/lang/en.js +22 -6
  15. package/lang/en.lyaml +4 -2
  16. package/lib/__tests__/buildAccount.test.js +62 -4
  17. package/lib/__tests__/yargsUtils.test.js +12 -1
  18. package/lib/buildAccount.d.ts +4 -1
  19. package/lib/buildAccount.js +57 -2
  20. package/lib/commonOpts.js +25 -0
  21. package/lib/configOptions.d.ts +5 -0
  22. package/lib/configOptions.js +11 -1
  23. package/lib/constants.d.ts +8 -0
  24. package/lib/constants.js +8 -0
  25. package/lib/errorHandlers/index.js +1 -3
  26. package/lib/errors/ProjectValidationError.d.ts +4 -0
  27. package/lib/errors/ProjectValidationError.js +9 -0
  28. package/lib/mcp/setup.d.ts +4 -0
  29. package/lib/mcp/setup.js +36 -0
  30. package/lib/projects/__tests__/LocalDevProcess.test.js +35 -0
  31. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +170 -1
  32. package/lib/projects/add/legacyAddComponent.js +1 -1
  33. package/lib/projects/add/v3AddComponent.js +3 -2
  34. package/lib/projects/create/index.js +3 -3
  35. package/lib/projects/create/legacy.js +2 -2
  36. package/lib/projects/create/v3.d.ts +0 -2
  37. package/lib/projects/create/v3.js +1 -3
  38. package/lib/projects/localDev/LocalDevLogger.js +11 -2
  39. package/lib/projects/localDev/LocalDevProcess.d.ts +2 -0
  40. package/lib/projects/localDev/LocalDevProcess.js +15 -0
  41. package/lib/projects/localDev/LocalDevState.d.ts +1 -0
  42. package/lib/projects/localDev/LocalDevState.js +5 -0
  43. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -2
  44. package/lib/projects/localDev/LocalDevWebsocketServer.js +40 -30
  45. package/lib/projects/upload.js +5 -12
  46. package/lib/projects/urls.d.ts +1 -1
  47. package/lib/projects/urls.js +2 -2
  48. package/lib/sandboxes.d.ts +4 -0
  49. package/lib/sandboxes.js +4 -0
  50. package/lib/ui/index.d.ts +6 -0
  51. package/lib/ui/index.js +3 -5
  52. package/lib/yargsUtils.d.ts +1 -0
  53. package/lib/yargsUtils.js +6 -0
  54. package/mcp-server/tools/index.js +6 -4
  55. package/mcp-server/tools/project/{AddFeatureToProject.d.ts → AddFeatureToProjectTool.d.ts} +4 -4
  56. package/mcp-server/tools/project/{AddFeatureToProject.js → AddFeatureToProjectTool.js} +6 -14
  57. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  58. package/mcp-server/tools/project/CreateProjectTool.js +4 -14
  59. package/mcp-server/tools/project/{DeployProject.d.ts → DeployProjectTool.d.ts} +1 -1
  60. package/mcp-server/tools/project/{DeployProject.js → DeployProjectTool.js} +2 -2
  61. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +20 -0
  62. package/mcp-server/tools/project/GetConfigValuesTool.js +51 -0
  63. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  64. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  65. package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.js → AddFeatureToProjectTool.test.js} +7 -7
  66. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +3 -4
  67. package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js} +4 -4
  68. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +198 -0
  69. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -2
  70. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  71. package/mcp-server/tools/project/constants.d.ts +1 -0
  72. package/mcp-server/tools/project/constants.js +11 -0
  73. package/mcp-server/utils/__tests__/command.test.js +76 -3
  74. package/mcp-server/utils/command.d.ts +6 -0
  75. package/mcp-server/utils/command.js +19 -0
  76. package/package.json +3 -3
  77. package/mcp-server/utils/__tests__/project.test.js +0 -79
  78. package/mcp-server/utils/project.d.ts +0 -5
  79. package/mcp-server/utils/project.js +0 -14
  80. /package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.d.ts → AddFeatureToProjectTool.test.d.ts} +0 -0
  81. /package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts} +0 -0
  82. /package/mcp-server/{utils/__tests__/project.test.d.ts → tools/project/__tests__/GetConfigValuesTool.test.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
3
3
  import { logger } from '@hubspot/local-dev-lib/logger';
4
- import { LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
4
+ import { LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
5
5
  import LocalDevWebsocketServer from '../localDev/LocalDevWebsocketServer.js';
6
6
  import { lib } from '../../../lang/en.js';
7
7
  vi.mock('ws');
@@ -29,6 +29,7 @@ describe('LocalDevWebsocketServer', () => {
29
29
  // Setup mock LocalDevProcess
30
30
  mockLocalDevProcess = {
31
31
  addStateListener: vi.fn(),
32
+ removeStateListener: vi.fn(),
32
33
  uploadProject: vi.fn(),
33
34
  sendDevServerMessage: vi.fn(),
34
35
  };
@@ -167,4 +168,172 @@ describe('LocalDevWebsocketServer', () => {
167
168
  expect(mockWebSocketServer.close).toHaveBeenCalled();
168
169
  });
169
170
  });
171
+ describe('multiple connections', () => {
172
+ let mockWebSocket1;
173
+ let mockWebSocket2;
174
+ let mockWebSocket3;
175
+ let connectionCallback;
176
+ beforeEach(async () => {
177
+ // Setup multiple mock WebSockets
178
+ mockWebSocket1 = {
179
+ on: vi.fn(),
180
+ send: vi.fn(),
181
+ close: vi.fn(),
182
+ };
183
+ mockWebSocket2 = {
184
+ on: vi.fn(),
185
+ send: vi.fn(),
186
+ close: vi.fn(),
187
+ };
188
+ mockWebSocket3 = {
189
+ on: vi.fn(),
190
+ send: vi.fn(),
191
+ close: vi.fn(),
192
+ };
193
+ // Start the server
194
+ isPortManagerServerRunning.mockResolvedValue(true);
195
+ requestPorts.mockResolvedValue({
196
+ 'local-dev-ui-websocket-server': 1234,
197
+ });
198
+ await server.start();
199
+ // Get the connection callback
200
+ connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
201
+ });
202
+ it('should handle multiple valid connections simultaneously', () => {
203
+ // Establish three connections from valid origins
204
+ connectionCallback(mockWebSocket1, {
205
+ headers: { origin: 'https://app.hubspot.com' },
206
+ });
207
+ connectionCallback(mockWebSocket2, {
208
+ headers: { origin: 'https://app.hubspotqa.com' },
209
+ });
210
+ connectionCallback(mockWebSocket3, {
211
+ headers: { origin: 'https://local.hubspot.com' },
212
+ });
213
+ // All connections should be established with proper setup
214
+ expect(mockWebSocket1.on).toHaveBeenCalledWith('message', expect.any(Function));
215
+ expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
216
+ expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
217
+ // Each connection should trigger state listener setup
218
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(6); // 2 listeners per connection * 3 connections
219
+ // Each connection should trigger dev server message
220
+ expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
221
+ expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
222
+ // No connections should be closed
223
+ expect(mockWebSocket1.close).not.toHaveBeenCalled();
224
+ expect(mockWebSocket2.close).not.toHaveBeenCalled();
225
+ expect(mockWebSocket3.close).not.toHaveBeenCalled();
226
+ });
227
+ it('should send project data to each connection independently', () => {
228
+ // Setup mock project data properties as getters
229
+ Object.defineProperty(mockLocalDevProcess, 'projectName', {
230
+ get: () => 'test-project',
231
+ configurable: true,
232
+ });
233
+ Object.defineProperty(mockLocalDevProcess, 'projectId', {
234
+ get: () => 123,
235
+ configurable: true,
236
+ });
237
+ Object.defineProperty(mockLocalDevProcess, 'targetProjectAccountId', {
238
+ get: () => 456,
239
+ configurable: true,
240
+ });
241
+ Object.defineProperty(mockLocalDevProcess, 'targetTestingAccountId', {
242
+ get: () => 789,
243
+ configurable: true,
244
+ });
245
+ // Establish multiple connections
246
+ connectionCallback(mockWebSocket1, {
247
+ headers: { origin: 'https://app.hubspot.com' },
248
+ });
249
+ connectionCallback(mockWebSocket2, {
250
+ headers: { origin: 'https://app.hubspotqa.com' },
251
+ });
252
+ // Each websocket should receive project data
253
+ expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
254
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
255
+ data: {
256
+ projectName: 'test-project',
257
+ projectId: 123,
258
+ targetProjectAccountId: 456,
259
+ targetTestingAccountId: 789,
260
+ },
261
+ }));
262
+ expect(mockWebSocket2.send).toHaveBeenCalledWith(JSON.stringify({
263
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
264
+ data: {
265
+ projectName: 'test-project',
266
+ projectId: 123,
267
+ targetProjectAccountId: 456,
268
+ targetTestingAccountId: 789,
269
+ },
270
+ }));
271
+ });
272
+ it('should properly cleanup listeners when connections close', () => {
273
+ // Establish connections
274
+ connectionCallback(mockWebSocket1, {
275
+ headers: { origin: 'https://app.hubspot.com' },
276
+ });
277
+ connectionCallback(mockWebSocket2, {
278
+ headers: { origin: 'https://app.hubspotqa.com' },
279
+ });
280
+ // Get all the close callbacks for both connections (there should be 2 per connection)
281
+ const closeCallbacks1 = mockWebSocket1.on.mock.calls
282
+ .filter(call => call[0] === 'close')
283
+ .map(call => call[1]);
284
+ const closeCallbacks2 = mockWebSocket2.on.mock.calls
285
+ .filter(call => call[0] === 'close')
286
+ .map(call => call[1]);
287
+ expect(closeCallbacks1).toHaveLength(2); // projectNodes and appData listeners
288
+ expect(closeCallbacks2).toHaveLength(2); // projectNodes and appData listeners
289
+ // Simulate first connection closing (call all close callbacks)
290
+ closeCallbacks1.forEach(callback => callback());
291
+ // Should have removed listeners for first connection (2 listeners: projectNodes and appData)
292
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(2);
293
+ // Simulate second connection closing
294
+ closeCallbacks2.forEach(callback => callback());
295
+ // Should have removed listeners for second connection as well
296
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
297
+ });
298
+ it('should broadcast state changes to all connected clients', () => {
299
+ // Establish connections
300
+ connectionCallback(mockWebSocket1, {
301
+ headers: { origin: 'https://app.hubspot.com' },
302
+ });
303
+ connectionCallback(mockWebSocket2, {
304
+ headers: { origin: 'https://app.hubspotqa.com' },
305
+ });
306
+ // Get the projectNodes listeners that were registered
307
+ const projectNodesListeners = mockLocalDevProcess.addStateListener.mock.calls
308
+ .filter(call => call[0] === 'projectNodes')
309
+ .map(call => call[1]);
310
+ expect(projectNodesListeners).toHaveLength(2);
311
+ // Simulate a project nodes update by calling the listeners
312
+ const mockProjectNodes = {
313
+ component1: {
314
+ uid: 'component1',
315
+ componentType: 'APP',
316
+ localDev: {
317
+ componentRoot: '/test/path',
318
+ componentConfigPath: '/test/path/config.json',
319
+ configUpdatedSinceLastUpload: false,
320
+ },
321
+ componentDeps: {},
322
+ metaFilePath: '/test/path',
323
+ config: {},
324
+ files: [],
325
+ },
326
+ };
327
+ projectNodesListeners.forEach(listener => listener(mockProjectNodes));
328
+ // Both websockets should receive the update
329
+ expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
330
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
331
+ data: mockProjectNodes,
332
+ }));
333
+ expect(mockWebSocket2.send).toHaveBeenCalledWith(JSON.stringify({
334
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
335
+ data: mockProjectNodes,
336
+ }));
337
+ });
338
+ });
170
339
  });
@@ -5,7 +5,7 @@ import { commands } from '../../../lang/en.js';
5
5
  import { getProjectComponentListFromRepo } from '../create/legacy.js';
6
6
  import { projectAddPrompt } from '../../prompts/projectAddPrompt.js';
7
7
  import path from 'path';
8
- import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH } from '../../constants.js';
8
+ import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
9
9
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
10
10
  import { uiLogger } from '../../ui/logger.js';
11
11
  export async function legacyAddComponent(args, projectDir, projectConfig) {
@@ -1,10 +1,11 @@
1
1
  import { commands, lib } from '../../../lang/en.js';
2
2
  import { getConfigForPlatformVersion } from '../create/legacy.js';
3
- import { calculateComponentTemplateChoices, createV3App, PROJECT_WITH_APP, } from '../create/v3.js';
3
+ import { calculateComponentTemplateChoices, createV3App, } from '../create/v3.js';
4
+ import { PROJECT_WITH_APP } from '../../constants.js';
4
5
  import path from 'path';
5
6
  import fs from 'fs';
6
7
  import { projectAddPromptV3 } from '../../prompts/projectAddPrompt.js';
7
- import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH } from '../../constants.js';
8
+ import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
8
9
  import { handleComponentCollision } from '../components.js';
9
10
  import { getProjectMetadata, } from '@hubspot/project-parsing-lib/src/lib/project.js';
10
11
  import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
@@ -1,8 +1,8 @@
1
1
  import { selectProjectTemplatePrompt, } from '../../prompts/selectProjectTemplatePrompt.js';
2
2
  import { projectNameAndDestPrompt } from '../../prompts/projectNameAndDestPrompt.js';
3
- import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH } from '../../constants.js';
3
+ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, EMPTY_PROJECT, } from '../../constants.js';
4
4
  import { useV3Api } from '../buildAndDeploy.js';
5
- import { EMPTY_PROJECT, v3ComponentFlow } from './v3.js';
5
+ import { v3ComponentFlow } from './v3.js';
6
6
  import { getProjectTemplateListFromRepo } from './legacy.js';
7
7
  import { uiLogger } from '../../ui/logger.js';
8
8
  import { commands } from '../../../lang/en.js';
@@ -23,7 +23,7 @@ export async function handleProjectCreationFlow(args) {
23
23
  projectNameAndDestPromptResponse,
24
24
  };
25
25
  }
26
- const projectTemplates = await getProjectTemplateListFromRepo(repo, 'main');
26
+ const projectTemplates = await getProjectTemplateListFromRepo(repo, DEFAULT_PROJECT_TEMPLATE_BRANCH);
27
27
  if (!projectTemplates.length) {
28
28
  uiLogger.error(commands.project.create.errors.failedToFetchProjectList);
29
29
  process.exit(EXIT_CODES.ERROR);
@@ -1,5 +1,5 @@
1
1
  import { fetchRepoFile } from '@hubspot/local-dev-lib/api/github';
2
- import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_COMPONENT_TYPES, } from '../../constants.js';
2
+ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_COMPONENT_TYPES, } from '../../constants.js';
3
3
  import { EXIT_CODES } from '../../enums/exitCodes.js';
4
4
  import { debugError } from '../../errorHandlers/index.js';
5
5
  import { uiLogger } from '../../ui/logger.js';
@@ -12,7 +12,7 @@ export async function getConfigForPlatformVersion(platformVersion) {
12
12
  if (useV3Api(platformVersion)) {
13
13
  path = `${platformVersion}/`;
14
14
  }
15
- const { data } = await fetchRepoFile(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, `${path}config.json`, 'main');
15
+ const { data } = await fetchRepoFile(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, `${path}config.json`, DEFAULT_PROJECT_TEMPLATE_BRANCH);
16
16
  return data;
17
17
  }
18
18
  export async function getProjectComponentListFromRepo(platformVersion) {
@@ -2,8 +2,6 @@ import { Separator } from '@inquirer/prompts';
2
2
  import { ComponentTemplate, ComponentTemplateChoice, ProjectTemplateRepoConfig } from '../../../types/Projects.js';
3
3
  import { ProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
4
4
  import { SelectProjectTemplatePromptResponse } from '../../prompts/selectProjectTemplatePrompt.js';
5
- export declare const EMPTY_PROJECT = "empty";
6
- export declare const PROJECT_WITH_APP = "app";
7
5
  export declare function createV3App(providedAuth: string | undefined, providedDistribution: string | undefined): Promise<{
8
6
  authType: string;
9
7
  distribution: string;
@@ -1,5 +1,5 @@
1
1
  import { Separator } from '@inquirer/prompts';
2
- import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../constants.js';
2
+ import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_PROJECT, PROJECT_WITH_APP, } from '../../constants.js';
3
3
  import { commands, lib } from '../../../lang/en.js';
4
4
  import { listPrompt } from '../../prompts/promptUtils.js';
5
5
  import chalk from 'chalk';
@@ -8,8 +8,6 @@ import path from 'path';
8
8
  import { getConfigForPlatformVersion } from './legacy.js';
9
9
  import { logError } from '../../errorHandlers/index.js';
10
10
  import { EXIT_CODES } from '../../enums/exitCodes.js';
11
- export const EMPTY_PROJECT = 'empty';
12
- export const PROJECT_WITH_APP = 'app';
13
11
  export async function createV3App(providedAuth, providedDistribution) {
14
12
  let authType;
15
13
  if (providedAuth &&
@@ -1,4 +1,4 @@
1
- import { getAccountId } from '@hubspot/local-dev-lib/config';
1
+ import { getAccountId, hasLocalStateFlag } from '@hubspot/local-dev-lib/config';
2
2
  import { getConfigDefaultAccount } from '@hubspot/local-dev-lib/config';
3
3
  import { logger } from '@hubspot/local-dev-lib/logger';
4
4
  import { uiLogger } from '../../ui/logger.js';
@@ -6,6 +6,8 @@ import { uiBetaTag, uiLine, uiAccountDescription, uiCommandReference, } from '..
6
6
  import { lib } from '../../../lang/en.js';
7
7
  import SpinniesManager from '../../ui/SpinniesManager.js';
8
8
  import { logError } from '../../errorHandlers/index.js';
9
+ import { isAutoOpenBrowserEnabled } from '../../configOptions.js';
10
+ import { CONFIG_LOCAL_STATE_FLAGS } from '../../constants.js';
9
11
  class LocalDevLogger {
10
12
  state;
11
13
  mostRecentUploadWarning;
@@ -96,7 +98,14 @@ class LocalDevLogger {
96
98
  uiLogger.log('');
97
99
  uiLogger.log(lib.LocalDevManager.running(this.state.projectConfig.name, uiAccountDescription(this.state.targetProjectAccountId)));
98
100
  uiLogger.log(lib.LocalDevManager.viewProjectLink(this.state.projectConfig.name, this.state.targetProjectAccountId));
99
- uiLogger.log(lib.LocalDevManager.viewLocalDevUILink(this.state.targetTestingAccountId));
101
+ const showWelcomeScreen = !hasLocalStateFlag(CONFIG_LOCAL_STATE_FLAGS.LOCAL_DEV_UI_WELCOME);
102
+ if (!isAutoOpenBrowserEnabled()) {
103
+ uiLogger.log(lib.LocalDevManager.viewLocalDevUILink(this.state.targetTestingAccountId, showWelcomeScreen));
104
+ }
105
+ else {
106
+ uiLogger.log('');
107
+ uiLogger.log(lib.LocalDevManager.localDevUIAutoMessage(this.state.targetTestingAccountId, showWelcomeScreen));
108
+ }
100
109
  uiLogger.log('');
101
110
  uiLogger.log(lib.LocalDevManager.quitHelper);
102
111
  uiLine();
@@ -24,6 +24,7 @@ declare class LocalDevProcess {
24
24
  private getIntermediateRepresentation;
25
25
  private updateProjectNodes;
26
26
  private updateProjectNodesAfterUpload;
27
+ private openLocalDevUi;
27
28
  handleFileChange(filePath: string, event: string): Promise<void>;
28
29
  handleConfigFileChange(): Promise<void>;
29
30
  start(): Promise<void>;
@@ -31,5 +32,6 @@ declare class LocalDevProcess {
31
32
  uploadProject(): Promise<boolean>;
32
33
  addStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>, callOnInit?: boolean): void;
33
34
  sendDevServerMessage(message: LocalDevServerMessage): void;
35
+ removeStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
34
36
  }
35
37
  export default LocalDevProcess;
@@ -1,5 +1,7 @@
1
1
  import { translateForLocalDev } from '@hubspot/project-parsing-lib';
2
+ import { hasLocalStateFlag } from '@hubspot/local-dev-lib/config';
2
3
  import path from 'path';
4
+ import open from 'open';
3
5
  import LocalDevState from './LocalDevState.js';
4
6
  import LocalDevLogger from './LocalDevLogger.js';
5
7
  import DevServerManagerV2 from './DevServerManagerV2.js';
@@ -8,6 +10,9 @@ import { mapToUserFriendlyName } from '@hubspot/project-parsing-lib/src/lib/tran
8
10
  import { getProjectConfig } from '../config.js';
9
11
  import { handleProjectUpload } from '../upload.js';
10
12
  import { pollProjectBuildAndDeploy } from '../buildAndDeploy.js';
13
+ import { getLocalDevUiUrl } from '../urls.js';
14
+ import { CONFIG_LOCAL_STATE_FLAGS } from '../../constants.js';
15
+ import { isAutoOpenBrowserEnabled } from '../../configOptions.js';
11
16
  class LocalDevProcess {
12
17
  state;
13
18
  _logger;
@@ -119,6 +124,10 @@ class LocalDevProcess {
119
124
  this.state.projectNodesAtLastUpload =
120
125
  intermediateRepresentation.intermediateNodesIndexedByUid;
121
126
  }
127
+ openLocalDevUi() {
128
+ const showWelcomeScreen = !hasLocalStateFlag(CONFIG_LOCAL_STATE_FLAGS.LOCAL_DEV_UI_WELCOME);
129
+ open(getLocalDevUiUrl(this.state.targetTestingAccountId, showWelcomeScreen));
130
+ }
122
131
  async handleFileChange(filePath, event) {
123
132
  await this.updateProjectNodes();
124
133
  try {
@@ -144,6 +153,9 @@ class LocalDevProcess {
144
153
  process.exit(EXIT_CODES.ERROR);
145
154
  }
146
155
  this.logger.startupMessage();
156
+ if (isAutoOpenBrowserEnabled()) {
157
+ this.openLocalDevUi();
158
+ }
147
159
  await this.startDevServers();
148
160
  this.logger.monitorConsoleOutput();
149
161
  // Verify that there are no mismatches between components in the local project
@@ -196,5 +208,8 @@ class LocalDevProcess {
196
208
  sendDevServerMessage(message) {
197
209
  this.state.devServerMessage = message;
198
210
  }
211
+ removeStateListener(key, listener) {
212
+ this.state.removeListener(key, listener);
213
+ }
199
214
  }
200
215
  export default LocalDevProcess;
@@ -51,5 +51,6 @@ declare class LocalDevState {
51
51
  get devServerMessage(): string;
52
52
  set devServerMessage(message: LocalDevServerMessage);
53
53
  addListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>, callOnInit?: boolean): void;
54
+ removeListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
54
55
  }
55
56
  export default LocalDevState;
@@ -111,5 +111,10 @@ class LocalDevState {
111
111
  listener(this[key]);
112
112
  }
113
113
  }
114
+ removeListener(key, listener) {
115
+ if (this._listeners[key]) {
116
+ this._listeners[key].splice(this._listeners[key].indexOf(listener), 1);
117
+ }
118
+ }
114
119
  }
115
120
  export default LocalDevState;
@@ -1,18 +1,18 @@
1
1
  import LocalDevProcess from './LocalDevProcess.js';
2
2
  declare class LocalDevWebsocketServer {
3
3
  private server?;
4
- private _websocket?;
5
4
  private debug?;
6
5
  private localDevProcess;
7
6
  private ALLOWED_ORIGINS;
8
7
  constructor(localDevProcess: LocalDevProcess, debug?: boolean);
9
- private websocket;
10
8
  private log;
11
9
  private logError;
12
10
  private sendMessage;
13
11
  private handleUpload;
14
12
  private setupMessageHandlers;
15
13
  private sendProjectData;
14
+ private setupProjectNodesListener;
15
+ private setupAppDataListener;
16
16
  private setupStateListeners;
17
17
  start(): Promise<void>;
18
18
  shutdown(): void;
@@ -1,13 +1,13 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
3
3
  import { logger } from '@hubspot/local-dev-lib/logger';
4
- import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
4
+ import { addLocalStateFlag } from '@hubspot/local-dev-lib/config';
5
+ import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, CONFIG_LOCAL_STATE_FLAGS, } from '../../constants.js';
5
6
  import { lib } from '../../../lang/en.js';
6
7
  const SERVER_INSTANCE_ID = 'local-dev-ui-websocket-server';
7
8
  const LOG_PREFIX = '[LocalDevWebsocketServer]';
8
9
  class LocalDevWebsocketServer {
9
10
  server;
10
- _websocket;
11
11
  debug;
12
12
  localDevProcess;
13
13
  ALLOWED_ORIGINS = [
@@ -20,12 +20,6 @@ class LocalDevWebsocketServer {
20
20
  this.localDevProcess = localDevProcess;
21
21
  this.debug = debug;
22
22
  }
23
- websocket() {
24
- if (!this._websocket) {
25
- throw new Error(lib.LocalDevWebsocketServer.errors.notInitialized(LOG_PREFIX));
26
- }
27
- return this._websocket;
28
- }
29
23
  log(message) {
30
24
  if (this.debug) {
31
25
  logger.log(LOG_PREFIX, message);
@@ -36,24 +30,24 @@ class LocalDevWebsocketServer {
36
30
  logger.error(LOG_PREFIX, message);
37
31
  }
38
32
  }
39
- sendMessage(message) {
40
- this.websocket().send(JSON.stringify(message));
33
+ sendMessage(websocket, message) {
34
+ websocket.send(JSON.stringify(message));
41
35
  }
42
- async handleUpload() {
36
+ async handleUpload(websocket) {
43
37
  const uploadSuccess = await this.localDevProcess.uploadProject();
44
38
  if (uploadSuccess) {
45
- this.sendMessage({
39
+ this.sendMessage(websocket, {
46
40
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPLOAD_SUCCESS,
47
41
  });
48
42
  }
49
43
  else {
50
- this.sendMessage({
44
+ this.sendMessage(websocket, {
51
45
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPLOAD_FAILURE,
52
46
  });
53
47
  }
54
48
  }
55
- setupMessageHandlers() {
56
- this.websocket().on('message', data => {
49
+ setupMessageHandlers(websocket) {
50
+ websocket.on('message', data => {
57
51
  try {
58
52
  const message = JSON.parse(data.toString());
59
53
  if (!message.type) {
@@ -62,7 +56,10 @@ class LocalDevWebsocketServer {
62
56
  }
63
57
  switch (message.type) {
64
58
  case LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.UPLOAD:
65
- this.handleUpload();
59
+ this.handleUpload(websocket);
60
+ break;
61
+ case LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.VIEWED_WELCOME_SCREEN:
62
+ addLocalStateFlag(CONFIG_LOCAL_STATE_FLAGS.LOCAL_DEV_UI_WELCOME);
66
63
  break;
67
64
  default:
68
65
  this.logError(lib.LocalDevWebsocketServer.errors.unknownMessageType(message.type));
@@ -73,8 +70,8 @@ class LocalDevWebsocketServer {
73
70
  }
74
71
  });
75
72
  }
76
- sendProjectData() {
77
- this.sendMessage({
73
+ sendProjectData(websocket) {
74
+ this.sendMessage(websocket, {
78
75
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
79
76
  data: {
80
77
  projectName: this.localDevProcess.projectName,
@@ -84,19 +81,33 @@ class LocalDevWebsocketServer {
84
81
  },
85
82
  });
86
83
  }
87
- setupStateListeners() {
88
- this.localDevProcess.addStateListener('projectNodes', nodes => {
89
- this.sendMessage({
84
+ setupProjectNodesListener(websocket) {
85
+ const listener = (nodes) => {
86
+ this.sendMessage(websocket, {
90
87
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
91
88
  data: nodes,
92
89
  });
93
- }, true);
94
- this.localDevProcess.addStateListener('appData', appData => {
95
- this.sendMessage({
90
+ };
91
+ this.localDevProcess.addStateListener('projectNodes', listener, true);
92
+ websocket.on('close', () => {
93
+ this.localDevProcess.removeStateListener('projectNodes', listener);
94
+ });
95
+ }
96
+ setupAppDataListener(websocket) {
97
+ const listener = (appData) => {
98
+ this.sendMessage(websocket, {
96
99
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_APP_DATA,
97
100
  data: appData,
98
101
  });
99
- }, true);
102
+ };
103
+ this.localDevProcess.addStateListener('appData', listener, true);
104
+ websocket.on('close', () => {
105
+ this.localDevProcess.removeStateListener('appData', listener);
106
+ });
107
+ }
108
+ setupStateListeners(websocket) {
109
+ this.setupProjectNodesListener(websocket);
110
+ this.setupAppDataListener(websocket);
100
111
  }
101
112
  async start() {
102
113
  const portManagerIsRunning = await isPortManagerServerRunning();
@@ -113,17 +124,16 @@ class LocalDevWebsocketServer {
113
124
  ws.close(1008, lib.LocalDevWebsocketServer.errors.originNotAllowed(origin));
114
125
  return;
115
126
  }
116
- this._websocket = ws;
117
- this.sendProjectData();
118
- this.setupMessageHandlers();
119
- this.setupStateListeners();
127
+ this.sendProjectData(ws);
128
+ this.setupMessageHandlers(ws);
129
+ this.setupStateListeners(ws);
120
130
  this.localDevProcess.sendDevServerMessage(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
121
131
  });
132
+ this.server.on('close', () => { });
122
133
  }
123
134
  shutdown() {
124
135
  this.server?.close();
125
136
  this.server = undefined;
126
- this._websocket = undefined;
127
137
  }
128
138
  }
129
139
  export default LocalDevWebsocketServer;
@@ -14,6 +14,7 @@ import { ensureProjectExists } from './ensureProjectExists.js';
14
14
  import { uiLogger } from '../ui/logger.js';
15
15
  import { useV3Api } from './buildAndDeploy.js';
16
16
  import { EXIT_CODES } from '../enums/exitCodes.js';
17
+ import ProjectValidationError from '../errors/ProjectValidationError.js';
17
18
  async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
18
19
  SpinniesManager.init({});
19
20
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
@@ -107,17 +108,13 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
107
108
  export function validateSourceDirectory(srcDir, projectConfig) {
108
109
  const filenames = fs.readdirSync(srcDir);
109
110
  if (!filenames || filenames.length === 0) {
110
- const validationError = new Error(lib.projectUpload.handleProjectUpload.emptySource(projectConfig.srcDir));
111
- validationError.name = 'ProjectValidationError';
112
- throw validationError;
111
+ throw new ProjectValidationError(lib.projectUpload.handleProjectUpload.emptySource(projectConfig.srcDir));
113
112
  }
114
113
  }
115
114
  export async function validateNoHSMetaMismatch(srcDir, projectConfig) {
116
115
  const hasHsMetaFiles = await projectContainsHsMetaFiles(srcDir);
117
116
  if (!useV3Api(projectConfig.platformVersion) && hasHsMetaFiles) {
118
- const validationError = new Error(lib.projectUpload.wrongPlatformVersionMetaFiles);
119
- validationError.name = 'ProjectValidationError';
120
- throw validationError;
117
+ throw new ProjectValidationError(lib.projectUpload.wrongPlatformVersionMetaFiles);
121
118
  }
122
119
  }
123
120
  export async function handleTranslate(projectDir, projectConfig, accountId, skipValidation, profile) {
@@ -132,12 +129,8 @@ export async function handleTranslate(projectDir, projectConfig, accountId, skip
132
129
  }
133
130
  catch (e) {
134
131
  if (isTranslationError(e)) {
135
- const validationError = new Error(e.toString());
136
- validationError.name = 'ProjectValidationError';
137
- throw validationError;
138
- }
139
- else {
140
- logError(e);
132
+ throw new ProjectValidationError(e.toString(), { cause: e });
141
133
  }
134
+ throw e;
142
135
  }
143
136
  }
@@ -5,4 +5,4 @@ export declare function getProjectSettingsUrl(projectName: string, accountId: nu
5
5
  export declare function getProjectActivityUrl(projectName: string, accountId: number): string;
6
6
  export declare function getProjectBuildDetailUrl(projectName: string, buildId: number, accountId: number): string;
7
7
  export declare function getProjectDeployDetailUrl(projectName: string, deployId: number, accountId: number): string;
8
- export declare function getLocalDevUiUrl(accountId: number): string;
8
+ export declare function getLocalDevUiUrl(accountId: number, showWelcomeScreen?: boolean): string;
@@ -34,6 +34,6 @@ export function getProjectBuildDetailUrl(projectName, buildId, accountId) {
34
34
  export function getProjectDeployDetailUrl(projectName, deployId, accountId) {
35
35
  return `${getProjectActivityUrl(projectName, accountId)}/deploy/${deployId}`;
36
36
  }
37
- export function getLocalDevUiUrl(accountId) {
38
- return `${getBaseUrl(accountId)}/developer-projects-local-dev/${accountId}`;
37
+ export function getLocalDevUiUrl(accountId, showWelcomeScreen) {
38
+ return `${getBaseUrl(accountId)}/developer-projects-local-dev/${accountId}${showWelcomeScreen ? '?welcome' : ''}`;
39
39
  }
@@ -11,6 +11,10 @@ export declare const SANDBOX_API_TYPE_MAP: {
11
11
  readonly STANDARD_SANDBOX: 1;
12
12
  readonly DEVELOPMENT_SANDBOX: 2;
13
13
  };
14
+ export declare const SANDBOX_TYPE_MAP_V2: {
15
+ readonly STANDARD_SANDBOX: "STANDARD";
16
+ readonly DEVELOPMENT_SANDBOX: "DEVELOPER";
17
+ };
14
18
  export declare function getSandboxTypeAsString(accountType?: AccountType): string;
15
19
  export declare function getHasSandboxesByType(parentAccountConfig: CLIAccount, type: AccountType): boolean;
16
20
  export declare function getAvailableSyncTypes(parentAccountConfig: CLIAccount, config: CLIAccount): Promise<Array<SandboxSyncTask>>;
package/lib/sandboxes.js CHANGED
@@ -22,6 +22,10 @@ export const SANDBOX_API_TYPE_MAP = {
22
22
  [HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 1,
23
23
  [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 2,
24
24
  };
25
+ export const SANDBOX_TYPE_MAP_V2 = {
26
+ [HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 'STANDARD',
27
+ [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 'DEVELOPER',
28
+ };
25
29
  export function getSandboxTypeAsString(accountType) {
26
30
  if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) {
27
31
  return 'development'; // Only place we're using this specific name