@hubspot/cli 8.0.8-experimental.1 → 8.0.8-experimental.2

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/commands/mcp/__tests__/start.test.js +8 -1
  2. package/commands/mcp/start.js +0 -1
  3. package/lang/en.d.ts +7 -0
  4. package/lang/en.js +8 -1
  5. package/lib/getStartedV2Actions.d.ts +13 -0
  6. package/lib/getStartedV2Actions.js +53 -0
  7. package/lib/mcp/__tests__/setup.test.js +15 -0
  8. package/lib/mcp/setup.d.ts +1 -0
  9. package/lib/mcp/setup.js +105 -34
  10. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  11. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  12. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  13. package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
  14. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  15. package/mcp-server/tools/cms/HsListTool.js +1 -1
  16. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
  17. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +2 -2
  18. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +2 -2
  19. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
  20. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +2 -2
  21. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +2 -2
  22. package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
  23. package/mcp-server/tools/project/CreateProjectTool.js +1 -1
  24. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  25. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  26. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  27. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  28. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +2 -2
  29. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +2 -2
  30. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +2 -2
  31. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +2 -2
  32. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
  33. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  34. package/mcp-server/utils/__tests__/command.test.js +233 -3
  35. package/mcp-server/utils/command.d.ts +5 -0
  36. package/mcp-server/utils/command.js +24 -0
  37. package/package.json +2 -8
  38. package/ui/components/getStarted/GetStartedFlow.js +79 -2
  39. package/ui/components/getStarted/reducer.d.ts +20 -0
  40. package/ui/components/getStarted/reducer.js +36 -0
  41. package/ui/components/getStarted/screens/InstallationScreen.d.ts +7 -0
  42. package/ui/components/getStarted/screens/InstallationScreen.js +16 -0
  43. package/ui/components/getStarted/screens/ProjectSetupScreen.js +2 -1
  44. package/ui/lib/constants.d.ts +1 -0
  45. package/ui/lib/constants.js +1 -0
  46. package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
  47. package/mcp-server/utils/__tests__/project.test.js +0 -140
  48. package/mcp-server/utils/project.d.ts +0 -5
  49. package/mcp-server/utils/project.js +0 -18
@@ -1,11 +1,11 @@
1
1
  import { CreateProjectTool, } from '../CreateProjectTool.js';
2
- import { runCommandInDir } from '../../../utils/project.js';
2
+ import { runCommandInDir } from '../../../utils/command.js';
3
3
  import { addFlag } from '../../../utils/command.js';
4
4
  import { APP_DISTRIBUTION_TYPES, EMPTY_PROJECT, PROJECT_WITH_APP, } from '../../../../lib/constants.js';
5
5
  import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
6
6
  import { trackToolUsage } from '../../../utils/toolUsageTracking.js';
7
7
  vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
8
- vi.mock('../../../utils/project');
8
+ vi.mock('../../../utils/command');
9
9
  vi.mock('../../../utils/command');
10
10
  vi.mock('../../../../lib/constants');
11
11
  vi.mock('../../../../lib/projects/create/v2');
@@ -1,12 +1,12 @@
1
1
  import { CreateTestAccountTool, } from '../CreateTestAccountTool.js';
2
- import { runCommandInDir } from '../../../utils/project.js';
2
+ import { runCommandInDir } from '../../../utils/command.js';
3
3
  import { addFlag } from '../../../utils/command.js';
4
4
  import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
5
5
  import { trackToolUsage } from '../../../utils/toolUsageTracking.js';
6
6
  import fs from 'fs';
7
7
  import * as config from '@hubspot/local-dev-lib/config';
8
8
  vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
9
- vi.mock('../../../utils/project');
9
+ vi.mock('../../../utils/command');
10
10
  vi.mock('../../../utils/command');
11
11
  vi.mock('../../../utils/toolUsageTracking');
12
12
  vi.mock('../../../utils/feedbackTracking');
@@ -1,10 +1,10 @@
1
1
  import { DeployProjectTool } from '../DeployProjectTool.js';
2
- import { runCommandInDir } from '../../../utils/project.js';
2
+ import { runCommandInDir } from '../../../utils/command.js';
3
3
  import { addFlag } from '../../../utils/command.js';
4
4
  import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
5
5
  import { trackToolUsage } from '../../../utils/toolUsageTracking.js';
6
6
  vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
7
- vi.mock('../../../utils/project');
7
+ vi.mock('../../../utils/command');
8
8
  vi.mock('../../../utils/command');
9
9
  vi.mock('../../../utils/toolUsageTracking');
10
10
  vi.mock('../../../utils/feedbackTracking');
@@ -1,13 +1,21 @@
1
1
  import { UploadProjectTools } from '../UploadProjectTools.js';
2
2
  import { getAllHsProfiles } from '@hubspot/project-parsing-lib/profiles';
3
3
  import { getProjectConfig } from '../../../../lib/projects/config.js';
4
- import { runCommandInDir } from '../../../utils/project.js';
4
+ import { runCommandInDir } from '../../../utils/command.js';
5
5
  import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
6
6
  import { trackToolUsage } from '../../../utils/toolUsageTracking.js';
7
7
  vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
8
8
  vi.mock('@hubspot/project-parsing-lib/profiles');
9
9
  vi.mock('../../../../lib/projects/config.js');
10
- vi.mock('../../../utils/project');
10
+ vi.mock('../../../utils/command', () => ({
11
+ runCommandInDir: vi.fn(),
12
+ addFlag: vi.fn((command, flagName, value) => {
13
+ if (Array.isArray(value)) {
14
+ return `${command} --${flagName} ${value.map(item => `"${item}"`).join(' ')}`;
15
+ }
16
+ return `${command} --${flagName} "${value}"`;
17
+ }),
18
+ }));
11
19
  vi.mock('../../../utils/toolUsageTracking');
12
20
  vi.mock('../../../utils/feedbackTracking');
13
21
  const mockTrackToolUsage = trackToolUsage;
@@ -1,9 +1,9 @@
1
1
  import { ValidateProjectTool, } from '../ValidateProjectTool.js';
2
- import { runCommandInDir } from '../../../utils/project.js';
2
+ import { runCommandInDir } from '../../../utils/command.js';
3
3
  import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
4
4
  import { trackToolUsage } from '../../../utils/toolUsageTracking.js';
5
5
  vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
6
- vi.mock('../../../utils/project');
6
+ vi.mock('../../../utils/command');
7
7
  vi.mock('../../../utils/toolUsageTracking');
8
8
  vi.mock('../../../utils/feedbackTracking');
9
9
  const mockTrackToolUsage = trackToolUsage;
@@ -1,6 +1,20 @@
1
- import { addFlag } from '../command.js';
2
- vi.mock('child_process');
3
- vi.mock('util');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const mockExecAsync = vi.fn();
4
+ vi.mock('node:child_process');
5
+ vi.mock('util', () => ({
6
+ default: {
7
+ promisify: () => mockExecAsync,
8
+ },
9
+ promisify: () => mockExecAsync,
10
+ }));
11
+ vi.mock('fs');
12
+ vi.mock('path');
13
+ // Import after mocks are set up
14
+ const { addFlag, runCommandInDir } = await import('../command.js');
15
+ const mockExistsSync = vi.mocked(fs.existsSync);
16
+ const mockMkdirSync = vi.mocked(fs.mkdirSync);
17
+ const mockResolve = vi.mocked(path.resolve);
4
18
  describe('mcp-server/utils/command', () => {
5
19
  describe('addFlag', () => {
6
20
  it('should add string flag to command', () => {
@@ -42,4 +56,220 @@ describe('mcp-server/utils/command', () => {
42
56
  expect(result).toBe('hs project create --features "card with spaces" "settings"');
43
57
  });
44
58
  });
59
+ describe('runCommandInDir', () => {
60
+ const mockDirectory = '/test/directory';
61
+ const mockCommand = 'npm install';
62
+ const mockResolvedPath = '/resolved/test/directory';
63
+ beforeEach(() => {
64
+ mockResolve.mockReturnValue(mockResolvedPath);
65
+ });
66
+ it('should run command in existing directory', async () => {
67
+ const expectedResult = {
68
+ stdout: 'command output',
69
+ stderr: '',
70
+ };
71
+ mockExistsSync.mockReturnValue(true);
72
+ mockExecAsync.mockResolvedValue(expectedResult);
73
+ const result = await runCommandInDir(mockDirectory, mockCommand);
74
+ expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
75
+ expect(mockMkdirSync).not.toHaveBeenCalled();
76
+ expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
77
+ expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
78
+ cwd: mockResolvedPath,
79
+ env: expect.any(Object),
80
+ }));
81
+ expect(result).toEqual(expectedResult);
82
+ });
83
+ it('should create directory if it does not exist', async () => {
84
+ const expectedResult = {
85
+ stdout: 'command output',
86
+ stderr: '',
87
+ };
88
+ mockExistsSync.mockReturnValue(false);
89
+ mockExecAsync.mockResolvedValue(expectedResult);
90
+ const result = await runCommandInDir(mockDirectory, mockCommand);
91
+ expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
92
+ expect(mockMkdirSync).toHaveBeenCalledWith(mockDirectory);
93
+ expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
94
+ expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
95
+ cwd: mockResolvedPath,
96
+ env: expect.any(Object),
97
+ }));
98
+ expect(result).toEqual(expectedResult);
99
+ });
100
+ it('should propagate execAsync errors', async () => {
101
+ const error = new Error('Command failed');
102
+ mockExistsSync.mockReturnValue(true);
103
+ mockExecAsync.mockRejectedValue(error);
104
+ await expect(runCommandInDir(mockDirectory, mockCommand)).rejects.toThrow('Command failed');
105
+ expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
106
+ cwd: mockResolvedPath,
107
+ env: expect.any(Object),
108
+ }));
109
+ });
110
+ it('should handle stderr in results', async () => {
111
+ const expectedResult = {
112
+ stdout: 'some output',
113
+ stderr: 'warning message',
114
+ };
115
+ mockExistsSync.mockReturnValue(true);
116
+ mockExecAsync.mockResolvedValue(expectedResult);
117
+ const result = await runCommandInDir(mockDirectory, mockCommand);
118
+ expect(result.stdout).toBe('some output');
119
+ expect(result.stderr).toBe('warning message');
120
+ });
121
+ it('should add --disable-usage-tracking flag to hs commands', async () => {
122
+ const hsCommand = 'hs project upload';
123
+ const expectedResult = {
124
+ stdout: 'success',
125
+ stderr: '',
126
+ };
127
+ mockExistsSync.mockReturnValue(true);
128
+ mockExecAsync.mockResolvedValue(expectedResult);
129
+ await runCommandInDir(mockDirectory, hsCommand);
130
+ expect(mockExecAsync).toHaveBeenCalledWith('hs project upload --disable-usage-tracking "true"', expect.objectContaining({
131
+ cwd: mockResolvedPath,
132
+ env: expect.any(Object),
133
+ }));
134
+ });
135
+ it('should not add --disable-usage-tracking flag to non-hs commands', async () => {
136
+ const nonHsCommand = 'npm install';
137
+ const expectedResult = {
138
+ stdout: 'success',
139
+ stderr: '',
140
+ };
141
+ mockExistsSync.mockReturnValue(true);
142
+ mockExecAsync.mockResolvedValue(expectedResult);
143
+ await runCommandInDir(mockDirectory, nonHsCommand);
144
+ expect(mockExecAsync).toHaveBeenCalledWith('npm install', expect.objectContaining({
145
+ cwd: mockResolvedPath,
146
+ env: expect.any(Object),
147
+ }));
148
+ });
149
+ it('should add --disable-usage-tracking flag to hs commands with existing flags', async () => {
150
+ const hsCommand = 'hs project upload --profile prod';
151
+ const expectedResult = {
152
+ stdout: 'success',
153
+ stderr: '',
154
+ };
155
+ mockExistsSync.mockReturnValue(true);
156
+ mockExecAsync.mockResolvedValue(expectedResult);
157
+ await runCommandInDir(mockDirectory, hsCommand);
158
+ expect(mockExecAsync).toHaveBeenCalledWith('hs project upload --profile prod --disable-usage-tracking "true"', expect.objectContaining({
159
+ cwd: mockResolvedPath,
160
+ env: expect.any(Object),
161
+ }));
162
+ });
163
+ it('should handle hs commands that start with whitespace', async () => {
164
+ const hsCommand = 'hs init';
165
+ const expectedResult = {
166
+ stdout: 'success',
167
+ stderr: '',
168
+ };
169
+ mockExistsSync.mockReturnValue(true);
170
+ mockExecAsync.mockResolvedValue(expectedResult);
171
+ await runCommandInDir(mockDirectory, hsCommand);
172
+ expect(mockExecAsync).toHaveBeenCalledWith('hs init --disable-usage-tracking "true"', expect.objectContaining({
173
+ cwd: mockResolvedPath,
174
+ env: expect.any(Object),
175
+ }));
176
+ });
177
+ it('should use npx -p @hubspot/cli when HUBSPOT_MCP_STANDALONE is true', async () => {
178
+ const originalEnv = process.env.HUBSPOT_MCP_STANDALONE;
179
+ process.env.HUBSPOT_MCP_STANDALONE = 'true';
180
+ const hsCommand = 'hs project upload';
181
+ const expectedResult = {
182
+ stdout: 'success',
183
+ stderr: '',
184
+ };
185
+ mockExistsSync.mockReturnValue(true);
186
+ mockExecAsync.mockResolvedValue(expectedResult);
187
+ await runCommandInDir(mockDirectory, hsCommand);
188
+ expect(mockExecAsync).toHaveBeenCalledWith('npx -y -p @hubspot/cli hs project upload --disable-usage-tracking "true"', expect.objectContaining({
189
+ cwd: mockResolvedPath,
190
+ env: expect.any(Object),
191
+ }));
192
+ // Restore original env
193
+ if (originalEnv === undefined) {
194
+ delete process.env.HUBSPOT_MCP_STANDALONE;
195
+ }
196
+ else {
197
+ process.env.HUBSPOT_MCP_STANDALONE = originalEnv;
198
+ }
199
+ });
200
+ it('should use regular hs command when HUBSPOT_MCP_STANDALONE is not set', async () => {
201
+ const originalEnv = process.env.HUBSPOT_MCP_STANDALONE;
202
+ delete process.env.HUBSPOT_MCP_STANDALONE;
203
+ const hsCommand = 'hs project upload';
204
+ const expectedResult = {
205
+ stdout: 'success',
206
+ stderr: '',
207
+ };
208
+ mockExistsSync.mockReturnValue(true);
209
+ mockExecAsync.mockResolvedValue(expectedResult);
210
+ await runCommandInDir(mockDirectory, hsCommand);
211
+ expect(mockExecAsync).toHaveBeenCalledWith('hs project upload --disable-usage-tracking "true"', expect.objectContaining({
212
+ cwd: mockResolvedPath,
213
+ env: expect.any(Object),
214
+ }));
215
+ // Restore original env
216
+ if (originalEnv !== undefined) {
217
+ process.env.HUBSPOT_MCP_STANDALONE = originalEnv;
218
+ }
219
+ });
220
+ it('should use npx -p @hubspot/cli for hs commands with flags in standalone mode', async () => {
221
+ const originalEnv = process.env.HUBSPOT_MCP_STANDALONE;
222
+ process.env.HUBSPOT_MCP_STANDALONE = 'true';
223
+ const hsCommand = 'hs project upload --profile prod';
224
+ const expectedResult = {
225
+ stdout: 'success',
226
+ stderr: '',
227
+ };
228
+ mockExistsSync.mockReturnValue(true);
229
+ mockExecAsync.mockResolvedValue(expectedResult);
230
+ await runCommandInDir(mockDirectory, hsCommand);
231
+ expect(mockExecAsync).toHaveBeenCalledWith('npx -y -p @hubspot/cli hs project upload --profile prod --disable-usage-tracking "true"', expect.objectContaining({
232
+ cwd: mockResolvedPath,
233
+ env: expect.any(Object),
234
+ }));
235
+ // Restore original env
236
+ if (originalEnv === undefined) {
237
+ delete process.env.HUBSPOT_MCP_STANDALONE;
238
+ }
239
+ else {
240
+ process.env.HUBSPOT_MCP_STANDALONE = originalEnv;
241
+ }
242
+ });
243
+ it('should use pinned CLI version when HUBSPOT_CLI_VERSION is set in standalone mode', async () => {
244
+ const originalStandaloneEnv = process.env.HUBSPOT_MCP_STANDALONE;
245
+ const originalVersionEnv = process.env.HUBSPOT_CLI_VERSION;
246
+ process.env.HUBSPOT_MCP_STANDALONE = 'true';
247
+ process.env.HUBSPOT_CLI_VERSION = '8.1.0';
248
+ const hsCommand = 'hs project upload';
249
+ const expectedResult = {
250
+ stdout: 'success',
251
+ stderr: '',
252
+ };
253
+ mockExistsSync.mockReturnValue(true);
254
+ mockExecAsync.mockResolvedValue(expectedResult);
255
+ await runCommandInDir(mockDirectory, hsCommand);
256
+ expect(mockExecAsync).toHaveBeenCalledWith('npx -y -p @hubspot/cli@8.1.0 hs project upload --disable-usage-tracking "true"', expect.objectContaining({
257
+ cwd: mockResolvedPath,
258
+ env: expect.any(Object),
259
+ }));
260
+ // Restore original env
261
+ if (originalStandaloneEnv === undefined) {
262
+ delete process.env.HUBSPOT_MCP_STANDALONE;
263
+ }
264
+ else {
265
+ process.env.HUBSPOT_MCP_STANDALONE = originalStandaloneEnv;
266
+ }
267
+ if (originalVersionEnv === undefined) {
268
+ delete process.env.HUBSPOT_CLI_VERSION;
269
+ }
270
+ else {
271
+ process.env.HUBSPOT_CLI_VERSION = originalVersionEnv;
272
+ }
273
+ });
274
+ });
45
275
  });
@@ -1,3 +1,8 @@
1
1
  import { exec } from 'node:child_process';
2
2
  export declare const execAsync: typeof exec.__promisify__;
3
3
  export declare function addFlag(command: string, flagName: string, value: string | number | boolean | string[]): string;
4
+ export interface CommandResults {
5
+ stderr: string;
6
+ stdout: string;
7
+ }
8
+ export declare function runCommandInDir(directory: string, command: string): Promise<CommandResults>;
@@ -1,3 +1,5 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
1
3
  import util from 'util';
2
4
  import { exec } from 'node:child_process';
3
5
  export const execAsync = util.promisify(exec);
@@ -7,3 +9,25 @@ export function addFlag(command, flagName, value) {
7
9
  }
8
10
  return `${command} --${flagName} "${value}"`;
9
11
  }
12
+ export async function runCommandInDir(directory, command) {
13
+ if (!fs.existsSync(directory)) {
14
+ fs.mkdirSync(directory);
15
+ }
16
+ let finalCommand = command;
17
+ if (command.startsWith('hs ')) {
18
+ // Check if running in standalone mode
19
+ if (process.env.HUBSPOT_MCP_STANDALONE === 'true') {
20
+ const cliPackage = process.env.HUBSPOT_CLI_VERSION
21
+ ? `@hubspot/cli@${process.env.HUBSPOT_CLI_VERSION}`
22
+ : '@hubspot/cli';
23
+ finalCommand = command.replace(/^hs /, `npx -y -p ${cliPackage} hs `);
24
+ }
25
+ finalCommand = addFlag(finalCommand, 'disable-usage-tracking', true);
26
+ }
27
+ return execAsync(finalCommand, {
28
+ cwd: path.resolve(directory),
29
+ env: {
30
+ ...process.env,
31
+ },
32
+ });
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.0.8-experimental.1",
3
+ "version": "8.0.8-experimental.2",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
@@ -119,12 +119,6 @@
119
119
  "registry": "https://registry.npmjs.org/"
120
120
  },
121
121
  "resolutions": {
122
- "eslint-visitor-keys": "4.2.0",
123
- "react": "19.2.3",
124
- "react-dom": "19.2.3"
125
- },
126
- "overrides": {
127
- "react": "19.2.3",
128
- "react-dom": "19.2.3"
122
+ "eslint-visitor-keys": "4.2.0"
129
123
  }
130
124
  }
@@ -1,12 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { sanitizeFileName, untildify } from '@hubspot/local-dev-lib/path';
3
3
  import { useApp, useFocus, useInput } from 'ink';
4
+ import open from 'open';
4
5
  import { useCallback, useEffect, useReducer } from 'react';
5
6
  import { commands } from '../../../lang/en.js';
6
- import { createProjectAction, trackGetStartedUsage, uploadAndDeployAction, } from '../../../lib/getStartedV2Actions.js';
7
+ import { createProjectAction, pollAppInstallation, trackGetStartedUsage, uploadAndDeployAction, } from '../../../lib/getStartedV2Actions.js';
8
+ import { validateProjectDirectory } from '../../../lib/prompts/projectNameAndDestPrompt.js';
7
9
  import { uiAccountDescription } from '../../../lib/ui/index.js';
8
10
  import { ACTION_STATUSES, GET_STARTED_FLOW_STEPS, } from '../../lib/constants.js';
9
11
  import { flowReducer } from './reducer.js';
12
+ import { InstallationScreen } from './screens/InstallationScreen.js';
10
13
  import { ProjectSetupScreen } from './screens/ProjectSetupScreen.js';
11
14
  import { UploadScreen } from './screens/UploadScreen.js';
12
15
  import { getProject } from './selectors.js';
@@ -44,6 +47,7 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
44
47
  statuses: {
45
48
  create: initialName ? ACTION_STATUSES.RUNNING : ACTION_STATUSES.IDLE,
46
49
  upload: ACTION_STATUSES.IDLE,
50
+ installApp: ACTION_STATUSES.IDLE,
47
51
  },
48
52
  });
49
53
  const [state, dispatch] = useReducer(flowReducer, getInitialState());
@@ -68,6 +72,16 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
68
72
  dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.DEST_INPUT });
69
73
  }, []);
70
74
  const handleDestSubmit = useCallback(async () => {
75
+ const validationResult = validateProjectDirectory(project.destination);
76
+ if (validationResult !== true) {
77
+ dispatch({
78
+ type: 'SET_DEST_ERROR',
79
+ payload: typeof validationResult === 'string'
80
+ ? validationResult
81
+ : commands.getStarted.v2.unknownError,
82
+ });
83
+ return;
84
+ }
71
85
  dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.CREATING });
72
86
  try {
73
87
  await createProjectAction({
@@ -108,6 +122,57 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
108
122
  dispatch({ type: 'UPLOAD_ERROR', payload: errorMessage });
109
123
  }
110
124
  }, [derivedAccountId, project.destination]);
125
+ const handlePollInstallation = useCallback(async () => {
126
+ const uploadApp = project.uploadResult?.app;
127
+ const projectId = project.uploadResult?.projectId;
128
+ if (!projectId || !uploadApp?.uid) {
129
+ dispatch({
130
+ type: 'INSTALL_APP_ERROR',
131
+ payload: commands.getStarted.v2.unknownError,
132
+ });
133
+ return;
134
+ }
135
+ try {
136
+ await pollAppInstallation({
137
+ accountId: derivedAccountId,
138
+ projectId,
139
+ appUid: uploadApp.uid,
140
+ requiredScopes: uploadApp.config?.auth?.requiredScopes,
141
+ optionalScopes: uploadApp.config?.auth?.optionalScopes,
142
+ onTimeout: () => {
143
+ dispatch({ type: 'SET_POLLING_TIMED_OUT', payload: true });
144
+ },
145
+ });
146
+ dispatch({ type: 'INSTALL_APP_DONE' });
147
+ }
148
+ catch (error) {
149
+ dispatch({
150
+ type: 'INSTALL_APP_ERROR',
151
+ payload: error instanceof Error
152
+ ? error.message
153
+ : commands.getStarted.v2.unknownError,
154
+ });
155
+ }
156
+ }, [project.uploadResult, derivedAccountId]);
157
+ const handleBrowserOpen = useCallback(async (shouldOpen) => {
158
+ await trackGetStartedUsage({
159
+ step: 'open-install-page',
160
+ type: shouldOpen ? 'opened' : 'declined',
161
+ }, derivedAccountId);
162
+ if (shouldOpen && project.uploadResult?.installUrl) {
163
+ try {
164
+ await open(project.uploadResult.installUrl, { url: true });
165
+ }
166
+ catch (error) {
167
+ dispatch({
168
+ type: 'SET_BROWSER_FAILED_URL',
169
+ payload: project.uploadResult.installUrl,
170
+ });
171
+ }
172
+ }
173
+ dispatch({ type: 'START_INSTALL_APP' });
174
+ await handlePollInstallation();
175
+ }, [project.uploadResult, derivedAccountId, handlePollInstallation]);
111
176
  const handleNameChange = useCallback((value) => {
112
177
  dispatch({ type: 'SET_PROJECT_NAME', payload: value });
113
178
  }, []);
@@ -116,7 +181,8 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
116
181
  }, []);
117
182
  useInput((_, key) => {
118
183
  const hasError = state.statuses.create === ACTION_STATUSES.ERROR ||
119
- state.statuses.upload === ACTION_STATUSES.ERROR;
184
+ state.statuses.upload === ACTION_STATUSES.ERROR ||
185
+ state.statuses.installApp === ACTION_STATUSES.ERROR;
120
186
  if (hasError) {
121
187
  exit();
122
188
  return;
@@ -126,11 +192,22 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
126
192
  if (state.step === GET_STARTED_FLOW_STEPS.COMPLETE) {
127
193
  handleUploadStart();
128
194
  }
195
+ else if (state.step === GET_STARTED_FLOW_STEPS.OPEN_APP_PROMPT) {
196
+ handleBrowserOpen(true);
197
+ }
198
+ else if (state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP &&
199
+ state.statuses.installApp === ACTION_STATUSES.DONE) {
200
+ // Ready for card setup - will be handled in PR3
201
+ exit();
202
+ }
129
203
  });
130
204
  if (state.step === GET_STARTED_FLOW_STEPS.UPLOADING ||
131
205
  state.step === GET_STARTED_FLOW_STEPS.OPEN_APP_PROMPT) {
132
206
  return _jsx(UploadScreen, { state: state, accountName: accountName });
133
207
  }
208
+ if (state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP) {
209
+ return _jsx(InstallationScreen, { state: state, accountName: accountName });
210
+ }
134
211
  // Show project setup screen for initial flow
135
212
  return (_jsx(ProjectSetupScreen, { state: state, onSelectOption: handleSelect, onNameChange: handleNameChange, onNameSubmit: handleNameSubmit, onDestChange: handleDestChange, onDestSubmit: handleDestSubmit }));
136
213
  }
@@ -14,6 +14,7 @@ export type AppState = {
14
14
  export type ActionStatuses = {
15
15
  create: ActionStatus;
16
16
  upload: ActionStatus;
17
+ installApp: ActionStatus;
17
18
  };
18
19
  export type FlowState = {
19
20
  step: FlowStep;
@@ -21,6 +22,9 @@ export type FlowState = {
21
22
  app: AppState;
22
23
  statuses: ActionStatuses;
23
24
  error?: string;
25
+ destError?: string;
26
+ browserFailedUrl?: string;
27
+ pollingTimedOut?: boolean;
24
28
  };
25
29
  type FlowAction = {
26
30
  type: 'SET_STEP';
@@ -37,6 +41,9 @@ type FlowAction = {
37
41
  } | {
38
42
  type: 'SET_ERROR';
39
43
  payload: string;
44
+ } | {
45
+ type: 'SET_DEST_ERROR';
46
+ payload: string;
40
47
  } | {
41
48
  type: 'CLEAR_ERROR';
42
49
  } | {
@@ -54,6 +61,19 @@ type FlowAction = {
54
61
  } | {
55
62
  type: 'UPLOAD_ERROR';
56
63
  payload: string;
64
+ } | {
65
+ type: 'START_INSTALL_APP';
66
+ } | {
67
+ type: 'INSTALL_APP_DONE';
68
+ } | {
69
+ type: 'INSTALL_APP_ERROR';
70
+ payload: string;
71
+ } | {
72
+ type: 'SET_BROWSER_FAILED_URL';
73
+ payload: string;
74
+ } | {
75
+ type: 'SET_POLLING_TIMED_OUT';
76
+ payload: boolean;
57
77
  };
58
78
  export declare function flowReducer(state: FlowState, action: FlowAction): FlowState;
59
79
  export {};
@@ -17,6 +17,13 @@ export function flowReducer(state, action) {
17
17
  return {
18
18
  ...state,
19
19
  project: { ...state.project, destination: action.payload },
20
+ destError: undefined,
21
+ };
22
+ case 'SET_DEST_ERROR':
23
+ return {
24
+ ...state,
25
+ step: GET_STARTED_FLOW_STEPS.DEST_INPUT,
26
+ destError: action.payload,
20
27
  };
21
28
  case 'SET_ERROR':
22
29
  return { ...state, error: action.payload };
@@ -66,6 +73,35 @@ export function flowReducer(state, action) {
66
73
  statuses: { ...state.statuses, upload: ACTION_STATUSES.ERROR },
67
74
  error: action.payload,
68
75
  };
76
+ case 'START_INSTALL_APP':
77
+ return {
78
+ ...state,
79
+ step: GET_STARTED_FLOW_STEPS.INSTALLING_APP,
80
+ statuses: { ...state.statuses, installApp: ACTION_STATUSES.RUNNING },
81
+ error: undefined,
82
+ pollingTimedOut: false,
83
+ };
84
+ case 'INSTALL_APP_DONE':
85
+ return {
86
+ ...state,
87
+ statuses: { ...state.statuses, installApp: ACTION_STATUSES.DONE },
88
+ };
89
+ case 'INSTALL_APP_ERROR':
90
+ return {
91
+ ...state,
92
+ statuses: { ...state.statuses, installApp: ACTION_STATUSES.ERROR },
93
+ error: action.payload,
94
+ };
95
+ case 'SET_BROWSER_FAILED_URL':
96
+ return {
97
+ ...state,
98
+ browserFailedUrl: action.payload,
99
+ };
100
+ case 'SET_POLLING_TIMED_OUT':
101
+ return {
102
+ ...state,
103
+ pollingTimedOut: action.payload,
104
+ };
69
105
  default:
70
106
  return state;
71
107
  }
@@ -0,0 +1,7 @@
1
+ import { FlowState } from '../reducer.js';
2
+ type InstallationScreenProps = {
3
+ state: FlowState;
4
+ accountName: string;
5
+ };
6
+ export declare function InstallationScreen({ state, accountName, }: InstallationScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { commands } from '../../../../lang/en.js';
4
+ import { ActionSection } from '../../ActionSection.js';
5
+ import { BoxWithTitle } from '../../BoxWithTitle.js';
6
+ import { INK_COLORS } from '../../../styles.js';
7
+ import { getProject } from '../selectors.js';
8
+ import { ACTION_STATUSES, GET_STARTED_FLOW_STEPS, } from '../../../lib/constants.js';
9
+ export function InstallationScreen({ state, accountName, }) {
10
+ const project = getProject(state);
11
+ const titleText = commands.getStarted.v2.startTitle;
12
+ // If we get to the installation screen, the app is uploaded and we have the name
13
+ const appName = project.uploadResult?.app?.config.name;
14
+ return (_jsx(BoxWithTitle, { flexGrow: 1, title: "hs get-started", borderColor: INK_COLORS.HUBSPOT_ORANGE, titleBackgroundColor: INK_COLORS.HUBSPOT_ORANGE, children: _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [_jsx(Text, { bold: true, children: titleText }), _jsx(Text, { children: commands.getStarted.v2.installInstructions }), _jsx(ActionSection, { status: state.statuses.installApp, statusText: commands.getStarted.v2.installingApp(appName, accountName) }), state.browserFailedUrl && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: INK_COLORS.WARNING_YELLOW, children: commands.getStarted.v2.browserFailedToOpen(state.browserFailedUrl) }) })), state.pollingTimedOut && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: INK_COLORS.WARNING_YELLOW, children: commands.getStarted.v2.pollingTimeout(2) }) })), state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP &&
15
+ state.statuses.installApp === ACTION_STATUSES.DONE && (_jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueSetup }))] }) }));
16
+ }
@@ -35,5 +35,6 @@ export function ProjectSetupScreen({ state, onSelectOption, onNameChange, onName
35
35
  return (_jsx(BoxWithTitle, { flexGrow: 1, title: "hs get-started", borderColor: INK_COLORS.HUBSPOT_ORANGE, titleBackgroundColor: INK_COLORS.HUBSPOT_ORANGE, children: _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [_jsx(Text, { bold: true, children: titleText }), state.step === GET_STARTED_FLOW_STEPS.SELECT ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: overviewText }), _jsx(Text, { children: projectsText }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: selectPrompt })] }), _jsx(SelectInput, { items: GET_STARTED_FLOW_OPTIONS, onSelect: onSelectOption })] })) : (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: `${selectPrompt}` }), _jsx(Text, { color: INK_COLORS.INFO_BLUE, children: state.app.selectedLabel })] })), _jsxs(ActionSection, { status: state.statuses.create, statusText: runningProjectCreateText, errorMessage: state.statuses.create === ACTION_STATUSES.ERROR
36
36
  ? `${state.error}\n\n${commands.getStarted.v2.pressKeyToExit}`
37
37
  : undefined, children: [state.step !== GET_STARTED_FLOW_STEPS.SELECT && (_jsx(InputField, { flag: "name", prompt: "Enter your project name", value: project.name, isEditing: state.step === GET_STARTED_FLOW_STEPS.NAME_INPUT, onChange: onNameChange, onSubmit: onNameSubmit })), state.step !== GET_STARTED_FLOW_STEPS.SELECT &&
38
- state.step !== GET_STARTED_FLOW_STEPS.NAME_INPUT && (_jsx(InputField, { flag: "dest", prompt: "Choose where to create the project", value: project.destination, isEditing: state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT, onChange: onDestChange, onSubmit: onDestSubmit }))] }), state.step === GET_STARTED_FLOW_STEPS.COMPLETE && (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueDeploy(state.app.selectedLabel) })] }))] }) }));
38
+ state.step !== GET_STARTED_FLOW_STEPS.NAME_INPUT && (_jsxs(_Fragment, { children: [_jsx(InputField, { flag: "dest", prompt: "Choose where to create the project", value: project.destination, isEditing: state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT, onChange: onDestChange, onSubmit: onDestSubmit }), state.destError &&
39
+ state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT && (_jsx(Text, { color: INK_COLORS.ALERT_RED, children: state.destError }))] }))] }), state.step === GET_STARTED_FLOW_STEPS.COMPLETE && (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueDeploy(state.app.selectedLabel) })] }))] }) }));
39
40
  }