@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
@@ -0,0 +1,252 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
3
+ import { uiLogger } from '../ui/logger.js';
4
+ import CLIWebSocketServer from '../CLIWebSocketServer.js';
5
+ import { lib } from '../../lang/en.js';
6
+ import { pkg } from '../jsonLoader.js';
7
+ vi.mock('ws');
8
+ vi.mock('@hubspot/local-dev-lib/portManager');
9
+ const INSTANCE_ID = 'test-websocket-server';
10
+ const LOG_PREFIX = '[TestWebSocketServer]';
11
+ const PORT = 1234;
12
+ describe('CLIWebSocketServer', () => {
13
+ let mockWebSocket;
14
+ let mockWebSocketServer;
15
+ let server;
16
+ beforeEach(() => {
17
+ mockWebSocket = {
18
+ on: vi.fn(),
19
+ send: vi.fn(),
20
+ close: vi.fn(),
21
+ };
22
+ mockWebSocketServer = {
23
+ on: vi.fn(),
24
+ close: vi.fn(),
25
+ };
26
+ WebSocketServer.mockImplementation(() => mockWebSocketServer);
27
+ server = new CLIWebSocketServer({
28
+ instanceId: INSTANCE_ID,
29
+ logPrefix: LOG_PREFIX,
30
+ debug: true,
31
+ });
32
+ });
33
+ describe('start()', () => {
34
+ it('should throw error if port manager is not running', async () => {
35
+ isPortManagerServerRunning.mockResolvedValue(false);
36
+ await expect(server.start({})).rejects.toThrow(lib.CLIWebsocketServer.errors.portManagerNotRunning(LOG_PREFIX));
37
+ });
38
+ it('should start websocket server on the assigned port', async () => {
39
+ isPortManagerServerRunning.mockResolvedValue(true);
40
+ requestPorts.mockResolvedValue({
41
+ [INSTANCE_ID]: PORT,
42
+ });
43
+ await server.start({});
44
+ expect(WebSocketServer).toHaveBeenCalledWith({ port: PORT });
45
+ expect(mockWebSocketServer.on).toHaveBeenCalledWith('connection', expect.any(Function));
46
+ });
47
+ it('should log startup message when debug is enabled', async () => {
48
+ isPortManagerServerRunning.mockResolvedValue(true);
49
+ requestPorts.mockResolvedValue({
50
+ [INSTANCE_ID]: PORT,
51
+ });
52
+ await server.start({});
53
+ expect(uiLogger.log).toHaveBeenCalledWith(expect.stringContaining(String(PORT)));
54
+ });
55
+ });
56
+ describe('origin validation', () => {
57
+ const validOrigins = [
58
+ 'https://app.hubspot.com',
59
+ 'https://app.hubspotqa.com',
60
+ 'https://local.hubspot.com',
61
+ 'https://local.hubspotqa.com',
62
+ 'https://app-na2.hubspot.com',
63
+ 'https://app-na2.hubspotqa.com',
64
+ 'https://app-na3.hubspot.com',
65
+ 'https://app-na3.hubspotqa.com',
66
+ 'https://app-ap1.hubspot.com',
67
+ 'https://app-ap1.hubspotqa.com',
68
+ 'https://app-eu1.hubspot.com',
69
+ 'https://app-eu1.hubspotqa.com',
70
+ ];
71
+ let connectionCallback;
72
+ beforeEach(async () => {
73
+ isPortManagerServerRunning.mockResolvedValue(true);
74
+ requestPorts.mockResolvedValue({
75
+ [INSTANCE_ID]: PORT,
76
+ });
77
+ await server.start({});
78
+ connectionCallback = mockWebSocketServer.on.mock
79
+ .calls[0][1];
80
+ });
81
+ validOrigins.forEach(origin => {
82
+ it(`should accept connection from ${origin}`, () => {
83
+ connectionCallback(mockWebSocket, { headers: { origin } });
84
+ expect(mockWebSocket.close).not.toHaveBeenCalled();
85
+ expect(mockWebSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
86
+ });
87
+ });
88
+ const invalidOrigins = [
89
+ 'https://malicious-site.com',
90
+ 'https://app.malicious-site.com',
91
+ 'https://app.hubspot.com.evil.com',
92
+ ];
93
+ invalidOrigins.forEach(origin => {
94
+ it(`should reject connection from "${origin}"`, () => {
95
+ connectionCallback(mockWebSocket, { headers: { origin } });
96
+ expect(mockWebSocket.close).toHaveBeenCalledWith(1008, lib.CLIWebsocketServer.errors.originNotAllowed(origin));
97
+ expect(mockWebSocket.on).not.toHaveBeenCalled();
98
+ });
99
+ });
100
+ it('should reject connection with no origin header', () => {
101
+ connectionCallback(mockWebSocket, { headers: {} });
102
+ expect(mockWebSocket.close).toHaveBeenCalledWith(1008, lib.CLIWebsocketServer.errors.originNotAllowed());
103
+ expect(mockWebSocket.on).not.toHaveBeenCalled();
104
+ });
105
+ });
106
+ describe('sendCliMetadata', () => {
107
+ let connectionCallback;
108
+ beforeEach(async () => {
109
+ isPortManagerServerRunning.mockResolvedValue(true);
110
+ requestPorts.mockResolvedValue({
111
+ [INSTANCE_ID]: PORT,
112
+ });
113
+ });
114
+ it('should send cliVersion on connection', async () => {
115
+ await server.start({});
116
+ connectionCallback = mockWebSocketServer.on.mock
117
+ .calls[0][1];
118
+ connectionCallback(mockWebSocket, {
119
+ headers: { origin: 'https://app.hubspot.com' },
120
+ });
121
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({
122
+ type: 'server:cliMetadata',
123
+ data: { cliVersion: pkg.version },
124
+ }));
125
+ });
126
+ it('should merge additional metadata when provided', async () => {
127
+ await server.start({
128
+ metadata: { customField: 'customValue', version: 2 },
129
+ });
130
+ connectionCallback = mockWebSocketServer.on.mock
131
+ .calls[0][1];
132
+ connectionCallback(mockWebSocket, {
133
+ headers: { origin: 'https://app.hubspot.com' },
134
+ });
135
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({
136
+ type: 'server:cliMetadata',
137
+ data: {
138
+ cliVersion: pkg.version,
139
+ customField: 'customValue',
140
+ version: 2,
141
+ },
142
+ }));
143
+ });
144
+ });
145
+ describe('callbacks', () => {
146
+ let connectionCallback;
147
+ beforeEach(async () => {
148
+ isPortManagerServerRunning.mockResolvedValue(true);
149
+ requestPorts.mockResolvedValue({
150
+ [INSTANCE_ID]: PORT,
151
+ });
152
+ });
153
+ it('should call onConnection for valid connections', async () => {
154
+ const onConnection = vi.fn();
155
+ await server.start({ onConnection });
156
+ connectionCallback = mockWebSocketServer.on.mock
157
+ .calls[0][1];
158
+ connectionCallback(mockWebSocket, {
159
+ headers: { origin: 'https://app.hubspot.com' },
160
+ });
161
+ expect(onConnection).toHaveBeenCalledWith(mockWebSocket);
162
+ });
163
+ it('should not call onConnection for rejected connections', async () => {
164
+ const onConnection = vi.fn();
165
+ await server.start({ onConnection });
166
+ connectionCallback = mockWebSocketServer.on.mock
167
+ .calls[0][1];
168
+ connectionCallback(mockWebSocket, {
169
+ headers: { origin: 'https://malicious.com' },
170
+ });
171
+ expect(onConnection).not.toHaveBeenCalled();
172
+ });
173
+ it('should call onMessage when a valid message is received', async () => {
174
+ const onMessage = vi.fn().mockReturnValue(true);
175
+ await server.start({ onMessage });
176
+ connectionCallback = mockWebSocketServer.on.mock
177
+ .calls[0][1];
178
+ connectionCallback(mockWebSocket, {
179
+ headers: { origin: 'https://app.hubspot.com' },
180
+ });
181
+ const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')[1];
182
+ messageCallback(JSON.stringify({ type: 'test:action', data: { key: 'value' } }));
183
+ expect(onMessage).toHaveBeenCalledWith(mockWebSocket, {
184
+ type: 'test:action',
185
+ data: { key: 'value' },
186
+ });
187
+ });
188
+ it('should log error when onMessage returns false', async () => {
189
+ const onMessage = vi.fn().mockReturnValue(false);
190
+ await server.start({ onMessage });
191
+ connectionCallback = mockWebSocketServer.on.mock
192
+ .calls[0][1];
193
+ connectionCallback(mockWebSocket, {
194
+ headers: { origin: 'https://app.hubspot.com' },
195
+ });
196
+ const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')[1];
197
+ messageCallback(JSON.stringify({ type: 'UNKNOWN_TYPE' }));
198
+ expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('UNKNOWN_TYPE'));
199
+ });
200
+ it('should call onClose when server closes', async () => {
201
+ const onClose = vi.fn();
202
+ await server.start({ onClose });
203
+ const closeCallback = mockWebSocketServer.on.mock.calls.find(call => call[0] === 'close')[1];
204
+ closeCallback();
205
+ expect(onClose).toHaveBeenCalled();
206
+ });
207
+ });
208
+ describe('message parsing', () => {
209
+ let messageCallback;
210
+ beforeEach(async () => {
211
+ isPortManagerServerRunning.mockResolvedValue(true);
212
+ requestPorts.mockResolvedValue({
213
+ [INSTANCE_ID]: PORT,
214
+ });
215
+ await server.start({});
216
+ const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
217
+ connectionCallback(mockWebSocket, {
218
+ headers: { origin: 'https://app.hubspot.com' },
219
+ });
220
+ messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')[1];
221
+ });
222
+ it('should log error for messages missing a type field', () => {
223
+ messageCallback(JSON.stringify({}));
224
+ expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('Missing type field'));
225
+ });
226
+ it('should log error for invalid JSON', () => {
227
+ messageCallback('not valid json');
228
+ expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('Invalid JSON'));
229
+ });
230
+ });
231
+ describe('sendMessage()', () => {
232
+ it('should serialize and send a message to the websocket', () => {
233
+ const message = { type: 'test:message', data: { foo: 'bar' } };
234
+ server.sendMessage(mockWebSocket, message);
235
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(message));
236
+ });
237
+ });
238
+ describe('shutdown()', () => {
239
+ it('should close the websocket server', async () => {
240
+ isPortManagerServerRunning.mockResolvedValue(true);
241
+ requestPorts.mockResolvedValue({
242
+ [INSTANCE_ID]: PORT,
243
+ });
244
+ await server.start({});
245
+ server.shutdown();
246
+ expect(mockWebSocketServer.close).toHaveBeenCalled();
247
+ });
248
+ it('should handle shutdown when server was never started', () => {
249
+ expect(() => server.shutdown()).not.toThrow();
250
+ });
251
+ });
252
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ import yargs from 'yargs';
2
+ import { addCommandSuggestions, commandSuggestionMappings, } from '../commandSuggestion.js';
3
+ import { uiLogger } from '../ui/logger.js';
4
+ import { uiCommandReference } from '../ui/index.js';
5
+ import { EXIT_CODES } from '../enums/exitCodes.js';
6
+ vi.mock('../ui/logger.js');
7
+ vi.mock('../ui/index.js');
8
+ const commandSpy = vi
9
+ .spyOn(yargs, 'command')
10
+ .mockReturnValue(yargs);
11
+ describe('lib/commandSuggestion', () => {
12
+ const uiLoggerErrorMock = uiLogger.error;
13
+ const uiCommandReferenceMock = uiCommandReference;
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ commandSpy.mockClear();
17
+ uiCommandReferenceMock.mockImplementation((command) => `\`${command}\``);
18
+ });
19
+ describe('addCommandSuggestions', () => {
20
+ it('adds all command suggestions to yargs instance', () => {
21
+ addCommandSuggestions(yargs);
22
+ expect(commandSpy).toHaveBeenCalledTimes(Object.keys(commandSuggestionMappings).length);
23
+ });
24
+ it('registers each mapping from commandSuggestionMappings', () => {
25
+ addCommandSuggestions(yargs);
26
+ Object.entries(commandSuggestionMappings).forEach(([oldCommand]) => {
27
+ expect(commandSpy).toHaveBeenCalledWith(expect.objectContaining({
28
+ command: oldCommand,
29
+ }));
30
+ });
31
+ });
32
+ it('returns the yargs instance', () => {
33
+ const yargsInstance = yargs;
34
+ const result = addCommandSuggestions(yargsInstance);
35
+ expect(result).toBe(yargsInstance);
36
+ });
37
+ it('creates command modules with handler that logs error and exits', () => {
38
+ vi.spyOn(process, 'exit').mockImplementation(() => {
39
+ throw new Error(`process.exit called with ${EXIT_CODES.ERROR}`);
40
+ });
41
+ addCommandSuggestions(yargs);
42
+ // Get the first registered command module
43
+ const firstCall = commandSpy.mock.calls[0];
44
+ const commandModule = firstCall[0];
45
+ const firstMapping = Object.entries(commandSuggestionMappings)[0];
46
+ const [, newCommand] = firstMapping;
47
+ // Verify the command module structure
48
+ expect(commandModule).toHaveProperty('command');
49
+ expect(commandModule).toHaveProperty('handler');
50
+ expect(commandModule).toHaveProperty('builder');
51
+ // Invoke the handler to verify behavior
52
+ expect(() => {
53
+ commandModule.handler({});
54
+ }).toThrow('process.exit called');
55
+ expect(uiCommandReferenceMock).toHaveBeenCalledWith(newCommand);
56
+ expect(uiLoggerErrorMock).toHaveBeenCalledWith(`Did you mean \`${newCommand}\`?`);
57
+ expect(process.exit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
58
+ vi.restoreAllMocks();
59
+ });
60
+ it('creates command modules with builder that sets strict mode to false', async () => {
61
+ const yargsInstance = yargs;
62
+ const commandSpyForTest = vi
63
+ .spyOn(yargsInstance, 'command')
64
+ .mockReturnValue(yargsInstance);
65
+ addCommandSuggestions(yargsInstance);
66
+ // Get a command module and verify it has a builder
67
+ const firstCall = commandSpyForTest.mock.calls[0];
68
+ const commandModule = firstCall[0];
69
+ expect(commandModule).toHaveProperty('builder');
70
+ expect(typeof commandModule.builder).toBe('function');
71
+ // Create a mock yargs builder with strict method
72
+ const mockYargsBuilder = {
73
+ strict: vi.fn().mockReturnThis(),
74
+ };
75
+ await commandModule.builder(mockYargsBuilder);
76
+ expect(mockYargsBuilder.strict).toHaveBeenCalledWith(false);
77
+ commandSpyForTest.mockRestore();
78
+ });
79
+ it('handles commands with multiple words', () => {
80
+ vi.spyOn(process, 'exit').mockImplementation(() => {
81
+ throw new Error(`process.exit called with ${EXIT_CODES.ERROR}`);
82
+ });
83
+ // Find a multi-word command
84
+ const multiWordCommand = Object.entries(commandSuggestionMappings).find(([oldCommand]) => oldCommand.includes(' '));
85
+ expect(multiWordCommand).toBeDefined();
86
+ if (multiWordCommand) {
87
+ const [oldCommand, newCommand] = multiWordCommand;
88
+ const yargsInstance = yargs;
89
+ const commandSpyForTest = vi
90
+ .spyOn(yargsInstance, 'command')
91
+ .mockReturnValue(yargsInstance);
92
+ addCommandSuggestions(yargsInstance);
93
+ // Find the call for this multi-word command
94
+ const call = commandSpyForTest.mock.calls.find(call => call[0]
95
+ .command === oldCommand);
96
+ expect(call).toBeDefined();
97
+ const commandModule = call[0];
98
+ expect(commandModule).toHaveProperty('handler');
99
+ // Invoke handler to verify it works with multi-word commands
100
+ expect(() => {
101
+ commandModule.handler({});
102
+ }).toThrow('process.exit called');
103
+ expect(uiCommandReferenceMock).toHaveBeenCalledWith(newCommand);
104
+ expect(uiLoggerErrorMock).toHaveBeenCalledWith(`Did you mean \`${newCommand}\`?`);
105
+ commandSpyForTest.mockRestore();
106
+ }
107
+ vi.restoreAllMocks();
108
+ });
109
+ });
110
+ describe('commandSuggestionMappings', () => {
111
+ it('all mappings point to valid new commands', () => {
112
+ Object.entries(commandSuggestionMappings).forEach(([, newCommand]) => {
113
+ expect(newCommand).toMatch(/^hs /);
114
+ expect(typeof newCommand).toBe('string');
115
+ expect(newCommand.length).toBeGreaterThan(0);
116
+ });
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,3 @@
1
+ import { Argv } from 'yargs';
2
+ export declare const commandSuggestionMappings: Record<string, string>;
3
+ export declare function addCommandSuggestions(yargsInstance: Argv): Argv;
@@ -0,0 +1,45 @@
1
+ import { uiLogger } from './ui/logger.js';
2
+ import { uiCommandReference } from './ui/index.js';
3
+ import { EXIT_CODES } from './enums/exitCodes.js';
4
+ export const commandSuggestionMappings = {
5
+ create: 'hs cms app|theme|module|webpack|function|template create',
6
+ fetch: 'hs cms fetch',
7
+ lint: 'hs cms lint',
8
+ list: 'hs cms list',
9
+ mv: 'hs cms mv',
10
+ remove: 'hs cms delete',
11
+ upload: 'hs cms upload',
12
+ watch: 'hs cms watch',
13
+ 'function deploy': 'hs cms function deploy',
14
+ 'function list': 'hs cms function list',
15
+ 'function server': 'hs cms function server',
16
+ logs: 'hs cms function logs',
17
+ 'module marketplace-validate': 'hs cms module marketplace-validate',
18
+ 'theme generate-selectors': 'hs cms theme generate-selectors',
19
+ 'theme marketplace-validate': 'hs cms theme marketplace-validate',
20
+ 'theme preview': 'hs cms theme preview',
21
+ 'custom-object schema create': 'hs custom-object create-schema',
22
+ 'custom-object schema delete': 'hs custom-object delete-schema',
23
+ 'custom-object schema fetch-all': 'hs custom-object fetch-all-schemas',
24
+ 'custom-object schema fetch': 'hs custom-object fetch-schema',
25
+ 'custom-object schema list': 'hs custom-object list-schemas',
26
+ 'custom-object schema update': 'hs custom-object update-schema',
27
+ };
28
+ function createCommandSuggestionHandler(newCommand) {
29
+ return () => {
30
+ uiLogger.error(`Did you mean ${uiCommandReference(newCommand)}?`);
31
+ process.exit(EXIT_CODES.ERROR);
32
+ };
33
+ }
34
+ function createCommandSuggestion(oldCommand, newCommand) {
35
+ return {
36
+ command: oldCommand,
37
+ builder: async (yargs) => {
38
+ return yargs.strict(false);
39
+ },
40
+ handler: createCommandSuggestionHandler(newCommand),
41
+ };
42
+ }
43
+ export function addCommandSuggestions(yargsInstance) {
44
+ return Object.entries(commandSuggestionMappings).reduce((yargs, [oldCommand, newCommand]) => yargs.command(createCommandSuggestion(oldCommand, newCommand)), yargsInstance);
45
+ }
@@ -94,7 +94,6 @@ export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
94
94
  UPDATE_APP_DATA: string;
95
95
  UPDATE_PROJECT_DATA: string;
96
96
  UPDATE_UPLOAD_WARNINGS: string;
97
- CLI_METADATA: string;
98
97
  DEV_SERVERS_STARTED: string;
99
98
  };
100
99
  export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
package/lib/constants.js CHANGED
@@ -86,7 +86,6 @@ export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
86
86
  UPDATE_APP_DATA: 'server:updateAppData',
87
87
  UPDATE_PROJECT_DATA: 'server:updateProjectData',
88
88
  UPDATE_UPLOAD_WARNINGS: 'server:updateUploadWarnings',
89
- CLI_METADATA: 'server:cliMetadata',
90
89
  DEV_SERVERS_STARTED: 'server:devServersStarted',
91
90
  };
92
91
  export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
@@ -0,0 +1,7 @@
1
+ import { CommonArgs } from '../../types/Yargs.js';
2
+ type GetStartedV2Args = CommonArgs & {
3
+ name?: string;
4
+ dest?: string;
5
+ };
6
+ export declare function runGetStartedV2({ derivedAccountId, name, dest, }: GetStartedV2Args): Promise<void>;
7
+ export {};
@@ -0,0 +1,56 @@
1
+ import { LOG_LEVEL, setLogLevel } from '@hubspot/local-dev-lib/logger';
2
+ import { createProjectAction } from '../getStartedV2Actions.js';
3
+ import { trackCommandMetadataUsage } from '../usageTracking.js';
4
+ import { renderInteractive } from '../../ui/render.js';
5
+ import { getGetStartedFlow } from '../../ui/components/GetStartedFlow.js';
6
+ import { getErrorMessage } from '../errorHandlers/index.js';
7
+ export async function runGetStartedV2({ derivedAccountId, name, dest, }) {
8
+ setLogLevel(LOG_LEVEL.NONE);
9
+ const onRunCreateProject = async (args) => {
10
+ try {
11
+ const result = await createProjectAction(args);
12
+ await trackCommandMetadataUsage('get-started', {
13
+ successful: true,
14
+ step: 'github-clone',
15
+ }, derivedAccountId);
16
+ await trackCommandMetadataUsage('get-started', {
17
+ successful: true,
18
+ step: 'project-creation',
19
+ }, derivedAccountId);
20
+ return {
21
+ projectName: result.projectName,
22
+ projectDest: result.projectDest,
23
+ };
24
+ }
25
+ catch (error) {
26
+ const errorMessage = getErrorMessage(error);
27
+ // Check if this is a nested project error (happens before cloning)
28
+ const isNestedProjectError = errorMessage.includes('Projects cannot be nested within other projects');
29
+ if (isNestedProjectError) {
30
+ await trackCommandMetadataUsage('get-started', {
31
+ successful: false,
32
+ step: 'project-creation',
33
+ }, derivedAccountId);
34
+ }
35
+ else {
36
+ // Clone or other error (failedToDownloadProject message)
37
+ await trackCommandMetadataUsage('get-started', {
38
+ successful: false,
39
+ step: 'github-clone',
40
+ }, derivedAccountId);
41
+ }
42
+ throw error;
43
+ }
44
+ };
45
+ try {
46
+ await renderInteractive(getGetStartedFlow({
47
+ derivedAccountId,
48
+ onRunCreateProject,
49
+ initialName: name,
50
+ initialDest: dest,
51
+ }), { fullScreen: true });
52
+ }
53
+ finally {
54
+ setLogLevel(LOG_LEVEL.LOG);
55
+ }
56
+ }
@@ -0,0 +1,8 @@
1
+ export type CreateProjectResult = {
2
+ projectName: string;
3
+ projectDest: string;
4
+ };
5
+ export declare function createProjectAction({ projectName, projectDest, }: {
6
+ projectName: string;
7
+ projectDest: string;
8
+ }): Promise<CreateProjectResult>;
@@ -0,0 +1,51 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { getCwd } from '@hubspot/local-dev-lib/path';
4
+ import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
5
+ import { commands, lib } from '../lang/en.js';
6
+ import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_CONFIG_FILE, } from '../lib/constants.js';
7
+ import { getProjectConfig, writeProjectConfig, } from '../lib/projects/config.js';
8
+ import { validateProjectDirectory } from '../lib/prompts/projectNameAndDestPrompt.js';
9
+ import SpinniesManager from '../lib/ui/SpinniesManager.js';
10
+ export async function createProjectAction({ projectName, projectDest, }) {
11
+ // Validate project name
12
+ if (!projectName || projectName.trim() === '') {
13
+ throw new Error(lib.prompts.projectNameAndDestPrompt.errors.nameRequired);
14
+ }
15
+ // Validate destination path
16
+ const validationResult = validateProjectDirectory(projectDest);
17
+ if (validationResult !== true) {
18
+ throw new Error(typeof validationResult === 'string'
19
+ ? validationResult
20
+ : 'Invalid project directory');
21
+ }
22
+ const projectDestAbsolute = path.resolve(getCwd(), projectDest);
23
+ const { projectConfig: existingProjectConfig, projectDir: existingProjectDir, } = await getProjectConfig(projectDestAbsolute);
24
+ if (existingProjectConfig &&
25
+ existingProjectDir &&
26
+ projectDestAbsolute.startsWith(existingProjectDir)) {
27
+ throw new Error(commands.project.create.errors.cannotNestProjects(existingProjectDir));
28
+ }
29
+ SpinniesManager.setDisableOutput(true);
30
+ try {
31
+ await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDestAbsolute, {
32
+ sourceDir: '2025.2/private-app-get-started-template',
33
+ hideLogs: true,
34
+ });
35
+ const projectConfigPath = path.join(projectDestAbsolute, PROJECT_CONFIG_FILE);
36
+ const parsedConfigFile = JSON.parse(fs.readFileSync(projectConfigPath, 'utf8'));
37
+ writeProjectConfig(projectConfigPath, {
38
+ ...parsedConfigFile,
39
+ name: projectName,
40
+ });
41
+ return { projectName, projectDest: projectDestAbsolute };
42
+ }
43
+ catch (error) {
44
+ throw new Error(commands.project.create.errors.failedToDownloadProject, {
45
+ cause: error,
46
+ });
47
+ }
48
+ finally {
49
+ SpinniesManager.setDisableOutput(false);
50
+ }
51
+ }