@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.
- package/bin/cli.js +8 -5
- package/commands/__tests__/getStarted.test.js +12 -0
- package/commands/getStarted.d.ts +2 -1
- package/commands/getStarted.js +38 -15
- package/lang/en.d.ts +31 -5
- package/lang/en.js +33 -7
- package/lib/CLIWebSocketServer.d.ts +28 -0
- package/lib/CLIWebSocketServer.js +91 -0
- package/lib/__tests__/CLIWebSocketServer.test.d.ts +1 -0
- package/lib/__tests__/CLIWebSocketServer.test.js +252 -0
- package/lib/__tests__/commandSuggestion.test.d.ts +1 -0
- package/lib/__tests__/commandSuggestion.test.js +119 -0
- package/lib/commandSuggestion.d.ts +3 -0
- package/lib/commandSuggestion.js +45 -0
- package/lib/constants.d.ts +0 -1
- package/lib/constants.js +0 -1
- package/lib/getStarted/getStartedV2.d.ts +7 -0
- package/lib/getStarted/getStartedV2.js +56 -0
- package/lib/getStartedV2Actions.d.ts +8 -0
- package/lib/getStartedV2Actions.js +51 -0
- package/lib/mcp/setup.js +48 -54
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +43 -175
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -7
- package/lib/projects/localDev/LocalDevWebsocketServer.js +51 -98
- package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +8 -7
- package/lib/projects/platformVersion.js +1 -1
- package/package.json +9 -4
- package/types/LocalDev.d.ts +0 -4
- package/ui/components/ActionSection.d.ts +12 -0
- package/ui/components/ActionSection.js +25 -0
- package/ui/components/BoxWithTitle.d.ts +4 -2
- package/ui/components/BoxWithTitle.js +2 -2
- package/ui/components/FullScreen.d.ts +6 -0
- package/ui/components/FullScreen.js +13 -0
- package/ui/components/GetStartedFlow.d.ts +24 -0
- package/ui/components/GetStartedFlow.js +128 -0
- package/ui/components/InputField.d.ts +10 -0
- package/ui/components/InputField.js +10 -0
- package/ui/components/SelectInput.d.ts +11 -0
- package/ui/components/SelectInput.js +59 -0
- package/ui/components/StatusIcon.d.ts +9 -0
- package/ui/components/StatusIcon.js +17 -0
- package/ui/constants.d.ts +6 -0
- package/ui/constants.js +6 -0
- package/ui/playground/fixtures.js +47 -0
- package/ui/render.d.ts +4 -0
- package/ui/render.js +25 -0
- package/ui/styles.d.ts +3 -0
- 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 (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
283
|
-
|
|
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);
|
|
310
|
-
expect(closeCallbacks2).toHaveLength(4);
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
private sendCliMetadata;
|
|
11
|
+
private handleMessage;
|
|
17
12
|
private sendProjectData;
|
|
18
13
|
private setupProjectNodesListener;
|
|
19
14
|
private setupAppDataListener;
|