@hubspot/cli 8.0.0 → 8.0.2-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 (49) hide show
  1. package/bin/cli.js +8 -5
  2. package/commands/__tests__/getStarted.test.js +12 -0
  3. package/commands/getStarted.d.ts +2 -1
  4. package/commands/getStarted.js +38 -15
  5. package/lang/en.d.ts +31 -5
  6. package/lang/en.js +33 -7
  7. package/lib/CLIWebSocketServer.d.ts +28 -0
  8. package/lib/CLIWebSocketServer.js +91 -0
  9. package/lib/__tests__/CLIWebSocketServer.test.d.ts +1 -0
  10. package/lib/__tests__/CLIWebSocketServer.test.js +252 -0
  11. package/lib/__tests__/commandSuggestion.test.d.ts +1 -0
  12. package/lib/__tests__/commandSuggestion.test.js +119 -0
  13. package/lib/commandSuggestion.d.ts +3 -0
  14. package/lib/commandSuggestion.js +45 -0
  15. package/lib/constants.d.ts +0 -1
  16. package/lib/constants.js +0 -1
  17. package/lib/getStarted/getStartedV2.d.ts +7 -0
  18. package/lib/getStarted/getStartedV2.js +56 -0
  19. package/lib/getStartedV2Actions.d.ts +8 -0
  20. package/lib/getStartedV2Actions.js +51 -0
  21. package/lib/mcp/setup.js +48 -54
  22. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +43 -175
  23. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -7
  24. package/lib/projects/localDev/LocalDevWebsocketServer.js +51 -98
  25. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +8 -7
  26. package/lib/projects/platformVersion.js +1 -1
  27. package/package.json +9 -4
  28. package/types/LocalDev.d.ts +0 -4
  29. package/ui/components/ActionSection.d.ts +12 -0
  30. package/ui/components/ActionSection.js +25 -0
  31. package/ui/components/BoxWithTitle.d.ts +4 -2
  32. package/ui/components/BoxWithTitle.js +2 -2
  33. package/ui/components/FullScreen.d.ts +6 -0
  34. package/ui/components/FullScreen.js +13 -0
  35. package/ui/components/GetStartedFlow.d.ts +24 -0
  36. package/ui/components/GetStartedFlow.js +128 -0
  37. package/ui/components/InputField.d.ts +10 -0
  38. package/ui/components/InputField.js +10 -0
  39. package/ui/components/SelectInput.d.ts +11 -0
  40. package/ui/components/SelectInput.js +59 -0
  41. package/ui/components/StatusIcon.d.ts +9 -0
  42. package/ui/components/StatusIcon.js +17 -0
  43. package/ui/constants.d.ts +6 -0
  44. package/ui/constants.js +6 -0
  45. package/ui/playground/fixtures.js +47 -0
  46. package/ui/render.d.ts +4 -0
  47. package/ui/render.js +25 -0
  48. package/ui/styles.d.ts +3 -0
  49. package/ui/styles.js +3 -0
package/lib/mcp/setup.js CHANGED
@@ -179,38 +179,30 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
179
179
  try {
180
180
  // Check if claude command is available
181
181
  await execAsync('claude --version');
182
- // Run claude mcp add command
183
- const mcpConfig = JSON.stringify({
184
- type: 'stdio',
185
- ...buildCommandWithAgentString(mcpCommand, claudeCode),
186
- });
187
- const { stdout } = await execAsync('claude mcp list');
188
- if (stdout.includes(mcpServerName)) {
189
- SpinniesManager.update('claudeCode', {
190
- text: commands.mcp.setup.spinners.alreadyInstalled,
191
- });
192
- await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
193
- }
194
- await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
195
- SpinniesManager.succeed('claudeCode', {
196
- text: commands.mcp.setup.spinners.configuredClaudeCode,
197
- });
198
- return true;
199
182
  }
200
- catch (error) {
201
- if (error instanceof Error && error.message.includes('claude')) {
202
- SpinniesManager.fail('claudeCode', {
203
- text: commands.mcp.setup.spinners.claudeCodeNotFound,
204
- });
205
- }
206
- else {
207
- SpinniesManager.fail('claudeCode', {
208
- text: commands.mcp.setup.spinners.claudeCodeInstallFailed,
209
- });
210
- logError(error);
211
- }
183
+ catch (e) {
184
+ SpinniesManager.fail('claudeCode', {
185
+ text: commands.mcp.setup.spinners.claudeCodeNotFound,
186
+ });
212
187
  return false;
213
188
  }
189
+ // Run claude mcp add command
190
+ const mcpConfig = JSON.stringify({
191
+ type: 'stdio',
192
+ ...buildCommandWithAgentString(mcpCommand, claudeCode),
193
+ });
194
+ const { stdout } = await execAsync('claude mcp list');
195
+ if (stdout.includes(mcpServerName)) {
196
+ SpinniesManager.update('claudeCode', {
197
+ text: commands.mcp.setup.spinners.alreadyInstalled,
198
+ });
199
+ await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
200
+ }
201
+ await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
202
+ SpinniesManager.succeed('claudeCode', {
203
+ text: commands.mcp.setup.spinners.configuredClaudeCode,
204
+ });
205
+ return true;
214
206
  }
215
207
  catch (error) {
216
208
  SpinniesManager.fail('claudeCode', {
@@ -245,8 +237,16 @@ export async function setupCodex(mcpCommand = defaultMcpCommand) {
245
237
  SpinniesManager.add('codexSpinner', {
246
238
  text: commands.mcp.setup.spinners.configuringCodex,
247
239
  });
248
- // Check if codex command is available
249
- await execAsync('codex --version');
240
+ try {
241
+ // Check if codex command is available
242
+ await execAsync('codex --version');
243
+ }
244
+ catch (error) {
245
+ SpinniesManager.fail('codexSpinner', {
246
+ text: commands.mcp.setup.spinners.codexNotFound,
247
+ });
248
+ return false;
249
+ }
250
250
  const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, codex);
251
251
  await execAsync(`codex mcp add "${mcpServerName}" -- ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
252
252
  SpinniesManager.succeed('codexSpinner', {
@@ -255,17 +255,10 @@ export async function setupCodex(mcpCommand = defaultMcpCommand) {
255
255
  return true;
256
256
  }
257
257
  catch (error) {
258
- if (error instanceof Error && error.message.includes('codex')) {
259
- SpinniesManager.fail('codexSpinner', {
260
- text: commands.mcp.setup.spinners.codexNotFound,
261
- });
262
- }
263
- else {
264
- SpinniesManager.fail('codexSpinner', {
265
- text: commands.mcp.setup.spinners.codexInstallFailed,
266
- });
267
- logError(error);
268
- }
258
+ SpinniesManager.fail('codexSpinner', {
259
+ text: commands.mcp.setup.spinners.codexInstallFailed,
260
+ });
261
+ logError(error);
269
262
  return false;
270
263
  }
271
264
  }
@@ -274,7 +267,15 @@ export async function setupGemini(mcpCommand = defaultMcpCommand) {
274
267
  SpinniesManager.add('geminiSpinner', {
275
268
  text: commands.mcp.setup.spinners.configuringGemini,
276
269
  });
277
- await execAsync('gemini --version');
270
+ try {
271
+ await execAsync('gemini --version');
272
+ }
273
+ catch (e) {
274
+ SpinniesManager.fail('geminiSpinner', {
275
+ text: commands.mcp.setup.spinners.geminiNotFound,
276
+ });
277
+ return false;
278
+ }
278
279
  const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, gemini);
279
280
  await execAsync(`gemini mcp add -s user "${mcpServerName}" ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
280
281
  SpinniesManager.succeed('geminiSpinner', {
@@ -283,17 +284,10 @@ export async function setupGemini(mcpCommand = defaultMcpCommand) {
283
284
  return true;
284
285
  }
285
286
  catch (error) {
286
- if (error instanceof Error && error.message.includes('gemini')) {
287
- SpinniesManager.fail('geminiSpinner', {
288
- text: commands.mcp.setup.spinners.geminiNotFound,
289
- });
290
- }
291
- else {
292
- SpinniesManager.fail('geminiSpinner', {
293
- text: commands.mcp.setup.spinners.geminiInstallFailed,
294
- });
295
- logError(error);
296
- }
287
+ SpinniesManager.fail('geminiSpinner', {
288
+ text: commands.mcp.setup.spinners.geminiInstallFailed,
289
+ });
290
+ logError(error);
297
291
  return false;
298
292
  }
299
293
  }
@@ -1,9 +1,7 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
3
- import { uiLogger } from '../../ui/logger.js';
4
3
  import { LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
5
4
  import LocalDevWebsocketServer from '../localDev/LocalDevWebsocketServer.js';
6
- import { lib } from '../../../lang/en.js';
7
5
  vi.mock('ws');
8
6
  vi.mock('@hubspot/local-dev-lib/portManager');
9
7
  describe('LocalDevWebsocketServer', () => {
@@ -12,19 +10,15 @@ describe('LocalDevWebsocketServer', () => {
12
10
  let mockWebSocketServer;
13
11
  let server;
14
12
  beforeEach(() => {
15
- // Reset all mocks
16
- // Setup mock WebSocket
17
13
  mockWebSocket = {
18
14
  on: vi.fn(),
19
15
  send: vi.fn(),
20
16
  close: vi.fn(),
21
17
  };
22
- // Setup mock WebSocketServer
23
18
  mockWebSocketServer = {
24
19
  on: vi.fn(),
25
20
  close: vi.fn(),
26
21
  };
27
- // Setup mock LocalDevProcess
28
22
  mockLocalDevProcess = {
29
23
  addStateListener: vi.fn(),
30
24
  removeStateListener: vi.fn(),
@@ -39,116 +33,47 @@ describe('LocalDevWebsocketServer', () => {
39
33
  targetProjectAccountId: 456,
40
34
  targetTestingAccountId: 789,
41
35
  };
42
- // Mock WebSocketServer constructor
43
36
  WebSocketServer.mockImplementation(() => mockWebSocketServer);
44
- // Create server instance
45
37
  server = new LocalDevWebsocketServer(mockLocalDevProcess, true);
46
38
  });
47
- describe('start()', () => {
48
- it('should throw error if port manager is not running', async () => {
49
- isPortManagerServerRunning.mockResolvedValue(false);
50
- await expect(server.start()).rejects.toThrow();
39
+ function startServerAndConnect(ws) {
40
+ const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
41
+ connectionCallback(ws ?? mockWebSocket, {
42
+ headers: { origin: 'https://app.hubspot.com' },
51
43
  });
52
- it('should start websocket server successfully', async () => {
44
+ }
45
+ describe('start()', () => {
46
+ beforeEach(async () => {
53
47
  isPortManagerServerRunning.mockResolvedValue(true);
54
48
  requestPorts.mockResolvedValue({
55
49
  'local-dev-ui-websocket-server': 1234,
56
50
  });
57
51
  await server.start();
58
- expect(WebSocketServer).toHaveBeenCalledWith({ port: 1234 });
59
- expect(mockWebSocketServer.on).toHaveBeenCalledWith('connection', expect.any(Function));
60
- expect(uiLogger.log).toHaveBeenCalled();
61
52
  });
62
- describe('valid origins', () => {
63
- const validOrigins = [
64
- 'https://app.hubspot.com',
65
- 'https://app.hubspotqa.com',
66
- 'https://local.hubspot.com',
67
- 'https://local.hubspotqa.com',
68
- 'https://app-na2.hubspot.com',
69
- 'https://app-na2.hubspotqa.com',
70
- 'https://app-na3.hubspot.com',
71
- 'https://app-na3.hubspotqa.com',
72
- 'https://app-ap1.hubspot.com',
73
- 'https://app-ap1.hubspotqa.com',
74
- 'https://app-eu1.hubspot.com',
75
- 'https://app-eu1.hubspotqa.com',
76
- ];
77
- validOrigins.forEach(origin => {
78
- it(`should accept connection from ${origin}`, async () => {
79
- isPortManagerServerRunning.mockResolvedValue(true);
80
- requestPorts.mockResolvedValue({
81
- 'local-dev-ui-websocket-server': 1234,
82
- });
83
- await server.start();
84
- // Get the connection callback
85
- const connectionCallback = mockWebSocketServer.on.mock
86
- .calls[0][1];
87
- // Simulate connection from valid origin
88
- connectionCallback(mockWebSocket, {
89
- headers: { origin },
90
- });
91
- expect(mockWebSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
92
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
93
- expect(mockWebSocket.close).not.toHaveBeenCalled();
94
- });
95
- });
96
- });
97
- describe('invalid origins', () => {
98
- const invalidOrigins = [
99
- 'https://malicious-site.com',
100
- 'https://app.malicious-site.com',
101
- 'https://app.hubspot.com.evil.com',
102
- ];
103
- invalidOrigins.forEach(origin => {
104
- it(`should reject connection from "${origin}"`, async () => {
105
- isPortManagerServerRunning.mockResolvedValue(true);
106
- requestPorts.mockResolvedValue({
107
- 'local-dev-ui-websocket-server': 1234,
108
- });
109
- await server.start();
110
- // Get the connection callback
111
- const connectionCallback = mockWebSocketServer.on.mock
112
- .calls[0][1];
113
- // Simulate connection from invalid origin
114
- connectionCallback(mockWebSocket, {
115
- headers: { origin },
116
- });
117
- expect(mockWebSocket.close).toHaveBeenCalledWith(1008, lib.LocalDevWebsocketServer.errors.originNotAllowed(origin));
118
- expect(mockWebSocket.on).not.toHaveBeenCalled();
119
- expect(mockLocalDevProcess.addStateListener).not.toHaveBeenCalled();
120
- });
121
- });
53
+ it('should send WEBSOCKET_SERVER_CONNECTED message when valid connection is established', () => {
54
+ startServerAndConnect();
55
+ expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
122
56
  });
123
- it('should reject connection with no origin header', async () => {
124
- isPortManagerServerRunning.mockResolvedValue(true);
125
- requestPorts.mockResolvedValue({
126
- 'local-dev-ui-websocket-server': 1234,
127
- });
128
- await server.start();
129
- // Get the connection callback
130
- const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
131
- // Simulate connection with no origin header
132
- connectionCallback(mockWebSocket, {
133
- headers: {},
134
- });
135
- expect(mockWebSocket.close).toHaveBeenCalledWith(1008, lib.LocalDevWebsocketServer.errors.originNotAllowed());
136
- expect(mockWebSocket.on).not.toHaveBeenCalled();
137
- expect(mockLocalDevProcess.addStateListener).not.toHaveBeenCalled();
57
+ it('should send project data on connection', () => {
58
+ startServerAndConnect();
59
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({
60
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
61
+ data: {
62
+ projectName: 'test-project',
63
+ projectId: 123,
64
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
65
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
66
+ targetProjectAccountId: 456,
67
+ targetTestingAccountId: 789,
68
+ },
69
+ }));
138
70
  });
139
- it('should send WEBSOCKET_SERVER_CONNECTED message when valid connection is established', async () => {
140
- isPortManagerServerRunning.mockResolvedValue(true);
141
- requestPorts.mockResolvedValue({
142
- 'local-dev-ui-websocket-server': 1234,
143
- });
144
- await server.start();
145
- // Get the connection callback
146
- const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
147
- // Simulate connection from valid origin
148
- connectionCallback(mockWebSocket, {
149
- headers: { origin: 'https://app-na3.hubspot.com' },
150
- });
151
- expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
71
+ it('should setup state listeners on connection', () => {
72
+ startServerAndConnect();
73
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
74
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('appData', expect.any(Function));
75
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('uploadWarnings', expect.any(Function));
76
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('devServersStarted', expect.any(Function));
152
77
  });
153
78
  });
154
79
  describe('message handling', () => {
@@ -158,39 +83,13 @@ describe('LocalDevWebsocketServer', () => {
158
83
  'local-dev-ui-websocket-server': 1234,
159
84
  });
160
85
  await server.start();
161
- const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
162
- connectionCallback(mockWebSocket, {
163
- headers: { origin: 'https://app.hubspot.com' },
164
- });
86
+ startServerAndConnect();
165
87
  });
166
88
  it('should handle UPLOAD message type', () => {
167
- const messageCallback = mockWebSocket.on.mock.calls[0][1];
168
- const message = {
169
- type: LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.UPLOAD,
170
- };
171
- messageCallback(JSON.stringify(message));
89
+ const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')[1];
90
+ messageCallback(JSON.stringify({ type: LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.UPLOAD }));
172
91
  expect(mockLocalDevProcess.uploadProject).toHaveBeenCalled();
173
92
  });
174
- it('should log error for missing message type', () => {
175
- const messageCallback = mockWebSocket.on.mock.calls[0][1];
176
- const message = {};
177
- messageCallback(JSON.stringify(message));
178
- expect(uiLogger.error).toHaveBeenCalled();
179
- });
180
- it('should log error for unknown message type', () => {
181
- const messageCallback = mockWebSocket.on.mock.calls[0][1];
182
- const message = {
183
- type: 'UNKNOWN_TYPE',
184
- };
185
- messageCallback(JSON.stringify(message));
186
- expect(uiLogger.error).toHaveBeenCalled();
187
- });
188
- it('should log error for invalid JSON', () => {
189
- const messageCallback = mockWebSocket.on.mock.calls[0][1];
190
- const invalidJson = 'invalid json';
191
- messageCallback(invalidJson);
192
- expect(uiLogger.error).toHaveBeenCalled();
193
- });
194
93
  });
195
94
  describe('shutdown()', () => {
196
95
  it('should close the websocket server', async () => {
@@ -209,7 +108,6 @@ describe('LocalDevWebsocketServer', () => {
209
108
  let mockWebSocket3;
210
109
  let connectionCallback;
211
110
  beforeEach(async () => {
212
- // Setup multiple mock WebSockets
213
111
  mockWebSocket1 = {
214
112
  on: vi.fn(),
215
113
  send: vi.fn(),
@@ -225,17 +123,14 @@ describe('LocalDevWebsocketServer', () => {
225
123
  send: vi.fn(),
226
124
  close: vi.fn(),
227
125
  };
228
- // Start the server
229
126
  isPortManagerServerRunning.mockResolvedValue(true);
230
127
  requestPorts.mockResolvedValue({
231
128
  'local-dev-ui-websocket-server': 1234,
232
129
  });
233
130
  await server.start();
234
- // Get the connection callback
235
131
  connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
236
132
  });
237
133
  it('should handle multiple valid connections simultaneously', () => {
238
- // Establish three connections from valid origins
239
134
  connectionCallback(mockWebSocket1, {
240
135
  headers: { origin: 'https://app.hubspot.com' },
241
136
  });
@@ -245,30 +140,24 @@ describe('LocalDevWebsocketServer', () => {
245
140
  connectionCallback(mockWebSocket3, {
246
141
  headers: { origin: 'https://local.hubspot.com' },
247
142
  });
248
- // All connections should be established with proper setup
249
143
  expect(mockWebSocket1.on).toHaveBeenCalledWith('message', expect.any(Function));
250
144
  expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
251
145
  expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
252
- // Each connection should trigger state listener setup
253
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12); // 4 listeners per connection * 3 connections
254
- // Each connection should trigger dev server message
146
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12);
255
147
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
256
148
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
257
- // No connections should be closed
258
149
  expect(mockWebSocket1.close).not.toHaveBeenCalled();
259
150
  expect(mockWebSocket2.close).not.toHaveBeenCalled();
260
151
  expect(mockWebSocket3.close).not.toHaveBeenCalled();
261
152
  });
262
153
  it('should send project data to each connection independently', () => {
263
- // Establish multiple connections
264
154
  connectionCallback(mockWebSocket1, {
265
155
  headers: { origin: 'https://app.hubspot.com' },
266
156
  });
267
157
  connectionCallback(mockWebSocket2, {
268
158
  headers: { origin: 'https://app-eu1.hubspotqa.com' },
269
159
  });
270
- // Each websocket should receive project data
271
- expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
160
+ const expectedProjectData = JSON.stringify({
272
161
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
273
162
  data: {
274
163
  projectName: 'test-project',
@@ -278,59 +167,41 @@ describe('LocalDevWebsocketServer', () => {
278
167
  targetProjectAccountId: 456,
279
168
  targetTestingAccountId: 789,
280
169
  },
281
- }));
282
- expect(mockWebSocket2.send).toHaveBeenCalledWith(JSON.stringify({
283
- type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
284
- data: {
285
- projectName: 'test-project',
286
- projectId: 123,
287
- latestBuild: { id: 'build-1', status: 'SUCCESS' },
288
- deployedBuild: { id: 'build-1', status: 'SUCCESS' },
289
- targetProjectAccountId: 456,
290
- targetTestingAccountId: 789,
291
- },
292
- }));
170
+ });
171
+ expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedProjectData);
172
+ expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedProjectData);
293
173
  });
294
174
  it('should properly cleanup listeners when connections close', () => {
295
- // Establish connections
296
175
  connectionCallback(mockWebSocket1, {
297
176
  headers: { origin: 'https://app.hubspot.com' },
298
177
  });
299
178
  connectionCallback(mockWebSocket2, {
300
179
  headers: { origin: 'https://app-ap1.hubspotqa.com' },
301
180
  });
302
- // Get all the close callbacks for both connections (there should be 2 per connection)
303
181
  const closeCallbacks1 = mockWebSocket1.on.mock.calls
304
182
  .filter(call => call[0] === 'close')
305
183
  .map(call => call[1]);
306
184
  const closeCallbacks2 = mockWebSocket2.on.mock.calls
307
185
  .filter(call => call[0] === 'close')
308
186
  .map(call => call[1]);
309
- expect(closeCallbacks1).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
310
- expect(closeCallbacks2).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
311
- // Simulate first connection closing (call all close callbacks)
187
+ expect(closeCallbacks1).toHaveLength(4);
188
+ expect(closeCallbacks2).toHaveLength(4);
312
189
  closeCallbacks1.forEach(callback => callback());
313
- // Should have removed listeners for first connection (4 listeners: projectNodes, appData, devServersStarted, and uploadWarnings)
314
190
  expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
315
- // Simulate second connection closing
316
191
  closeCallbacks2.forEach(callback => callback());
317
- // Should have removed listeners for second connection as well
318
192
  expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(8);
319
193
  });
320
194
  it('should broadcast state changes to all connected clients', () => {
321
- // Establish connections
322
195
  connectionCallback(mockWebSocket1, {
323
196
  headers: { origin: 'https://app.hubspot.com' },
324
197
  });
325
198
  connectionCallback(mockWebSocket2, {
326
199
  headers: { origin: 'https://local.hubspotqa.com' },
327
200
  });
328
- // Get the projectNodes listeners that were registered
329
201
  const projectNodesListeners = mockLocalDevProcess.addStateListener.mock.calls
330
202
  .filter(call => call[0] === 'projectNodes')
331
203
  .map(call => call[1]);
332
204
  expect(projectNodesListeners).toHaveLength(2);
333
- // Simulate a project nodes update by calling the listeners
334
205
  const mockProjectNodes = {
335
206
  component1: {
336
207
  uid: 'component1',
@@ -349,15 +220,12 @@ describe('LocalDevWebsocketServer', () => {
349
220
  },
350
221
  };
351
222
  projectNodesListeners.forEach(listener => listener(mockProjectNodes));
352
- // Both websockets should receive the update
353
- expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
354
- type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
355
- data: mockProjectNodes,
356
- }));
357
- expect(mockWebSocket2.send).toHaveBeenCalledWith(JSON.stringify({
223
+ const expectedMessage = JSON.stringify({
358
224
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
359
225
  data: mockProjectNodes,
360
- }));
226
+ });
227
+ expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMessage);
228
+ expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedMessage);
361
229
  });
362
230
  });
363
231
  });
@@ -1,19 +1,14 @@
1
1
  import LocalDevProcess from './LocalDevProcess.js';
2
2
  declare class LocalDevWebsocketServer {
3
- private server?;
4
- private debug?;
3
+ private cliWebSocketServer;
5
4
  private localDevProcess;
6
5
  constructor(localDevProcess: LocalDevProcess, debug?: boolean);
7
- private log;
8
- private logError;
9
- private sendMessage;
10
6
  private handleUpload;
11
7
  private handleDeploy;
12
8
  private handleAppInstallSuccess;
13
9
  private handleAppInstallFailure;
14
10
  private handleAppInstallInitiated;
15
- private setupMessageHandlers;
16
- private sendCliMetadata;
11
+ private handleMessage;
17
12
  private sendProjectData;
18
13
  private setupProjectNodesListener;
19
14
  private setupAppDataListener;