@hubspot/cli 8.0.8-experimental.5 → 8.0.8-experimental.7
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/commands/cms/theme/preview.js +11 -64
- package/commands/mcp/__tests__/start.test.js +1 -8
- package/commands/mcp/start.js +1 -0
- package/lang/en.d.ts +0 -7
- package/lang/en.js +1 -8
- package/lib/__tests__/commandSuggestion.test.js +0 -2
- package/lib/cms/devServerProcess.d.ts +13 -0
- package/lib/cms/devServerProcess.js +122 -0
- package/lib/commandSuggestion.js +7 -1
- package/lib/getStartedV2Actions.d.ts +0 -13
- package/lib/getStartedV2Actions.js +0 -53
- package/lib/mcp/__tests__/setup.test.js +0 -15
- package/lib/mcp/setup.d.ts +0 -1
- package/lib/mcp/setup.js +30 -77
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +2 -1
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +2 -2
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -20
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +11 -7
- package/mcp-server/tools/project/CreateProjectTool.d.ts +4 -24
- package/mcp-server/tools/project/CreateProjectTool.js +11 -6
- package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +8 -5
- package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
- package/mcp-server/tools/project/GetBuildLogsTool.js +7 -6
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
- package/mcp-server/tools/project/GetBuildStatusTool.js +4 -3
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +1 -6
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +6 -1
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +32 -0
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -10
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +1 -12
- package/mcp-server/tools/project/constants.js +16 -12
- package/mcp-server/utils/__tests__/command.test.js +3 -233
- package/mcp-server/utils/__tests__/project.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/project.test.js +140 -0
- package/mcp-server/utils/command.d.ts +0 -5
- package/mcp-server/utils/command.js +0 -24
- package/mcp-server/utils/project.d.ts +5 -0
- package/mcp-server/utils/project.js +18 -0
- package/package.json +1 -1
- package/ui/components/getStarted/GetStartedFlow.js +2 -79
- package/ui/components/getStarted/reducer.d.ts +0 -20
- package/ui/components/getStarted/reducer.js +0 -36
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +1 -2
- package/ui/lib/constants.d.ts +0 -1
- package/ui/lib/constants.js +0 -1
- package/ui/components/getStarted/screens/InstallationScreen.d.ts +0 -7
- package/ui/components/getStarted/screens/InstallationScreen.js +0 -16
|
@@ -6,19 +6,23 @@ export const absoluteCurrentWorkingDirectory = z
|
|
|
6
6
|
.string()
|
|
7
7
|
.describe('The absolute path to the current working directory.');
|
|
8
8
|
export const features = z
|
|
9
|
-
.array(z.
|
|
10
|
-
'card',
|
|
11
|
-
'settings',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'
|
|
9
|
+
.array(z.union([
|
|
10
|
+
z.literal('card'),
|
|
11
|
+
z.literal('settings'),
|
|
12
|
+
z
|
|
13
|
+
.literal('app-function')
|
|
14
|
+
.describe('Also known as a public serverless function'),
|
|
15
|
+
z.literal('webhooks'),
|
|
16
|
+
z
|
|
17
|
+
.literal('workflow-action')
|
|
18
|
+
.describe('Also known as a custom workflow action.'),
|
|
19
|
+
z.literal('workflow-action-tool').describe('Also known as agent tools.'),
|
|
20
|
+
z.literal('app-object'),
|
|
21
|
+
z.literal('app-event'),
|
|
22
|
+
z.literal('scim'),
|
|
23
|
+
z.literal('page'),
|
|
20
24
|
]))
|
|
21
|
-
.describe('The features to include in the project, multiple options can be selected
|
|
25
|
+
.describe('The features to include in the project, multiple options can be selected')
|
|
22
26
|
.optional();
|
|
23
27
|
export const docsSearchQuery = z
|
|
24
28
|
.string()
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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);
|
|
1
|
+
import { addFlag } from '../command.js';
|
|
2
|
+
vi.mock('child_process');
|
|
3
|
+
vi.mock('util');
|
|
18
4
|
describe('mcp-server/utils/command', () => {
|
|
19
5
|
describe('addFlag', () => {
|
|
20
6
|
it('should add string flag to command', () => {
|
|
@@ -56,220 +42,4 @@ describe('mcp-server/utils/command', () => {
|
|
|
56
42
|
expect(result).toBe('hs project create --features "card with spaces" "settings"');
|
|
57
43
|
});
|
|
58
44
|
});
|
|
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
|
-
});
|
|
275
45
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { runCommandInDir } from '../project.js';
|
|
2
|
+
import { execAsync } from '../command.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
vi.mock('../command', () => ({
|
|
6
|
+
execAsync: vi.fn(),
|
|
7
|
+
addFlag: vi.fn((command, flagName, value) => {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return `${command} --${flagName} ${value.map(item => `"${item}"`).join(' ')}`;
|
|
10
|
+
}
|
|
11
|
+
return `${command} --${flagName} "${value}"`;
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('fs');
|
|
15
|
+
vi.mock('path');
|
|
16
|
+
const mockExecAsync = execAsync;
|
|
17
|
+
const mockExistsSync = fs.existsSync;
|
|
18
|
+
const mockMkdirSync = fs.mkdirSync;
|
|
19
|
+
const mockResolve = path.resolve;
|
|
20
|
+
describe('mcp-server/utils/project', () => {
|
|
21
|
+
describe('runCommandInDir', () => {
|
|
22
|
+
const mockDirectory = '/test/directory';
|
|
23
|
+
const mockCommand = 'npm install';
|
|
24
|
+
const mockResolvedPath = '/resolved/test/directory';
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockResolve.mockReturnValue(mockResolvedPath);
|
|
27
|
+
});
|
|
28
|
+
it('should run command in existing directory', async () => {
|
|
29
|
+
const expectedResult = {
|
|
30
|
+
stdout: 'command output',
|
|
31
|
+
stderr: '',
|
|
32
|
+
};
|
|
33
|
+
mockExistsSync.mockReturnValue(true);
|
|
34
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
35
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
36
|
+
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
37
|
+
expect(mockMkdirSync).not.toHaveBeenCalled();
|
|
38
|
+
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
39
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
40
|
+
cwd: mockResolvedPath,
|
|
41
|
+
env: expect.any(Object),
|
|
42
|
+
}));
|
|
43
|
+
expect(result).toEqual(expectedResult);
|
|
44
|
+
});
|
|
45
|
+
it('should create directory if it does not exist', async () => {
|
|
46
|
+
const expectedResult = {
|
|
47
|
+
stdout: 'command output',
|
|
48
|
+
stderr: '',
|
|
49
|
+
};
|
|
50
|
+
mockExistsSync.mockReturnValue(false);
|
|
51
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
52
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
53
|
+
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
54
|
+
expect(mockMkdirSync).toHaveBeenCalledWith(mockDirectory);
|
|
55
|
+
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
56
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
57
|
+
cwd: mockResolvedPath,
|
|
58
|
+
env: expect.any(Object),
|
|
59
|
+
}));
|
|
60
|
+
expect(result).toEqual(expectedResult);
|
|
61
|
+
});
|
|
62
|
+
it('should propagate execAsync errors', async () => {
|
|
63
|
+
const error = new Error('Command failed');
|
|
64
|
+
mockExistsSync.mockReturnValue(true);
|
|
65
|
+
mockExecAsync.mockRejectedValue(error);
|
|
66
|
+
await expect(runCommandInDir(mockDirectory, mockCommand)).rejects.toThrow('Command failed');
|
|
67
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
68
|
+
cwd: mockResolvedPath,
|
|
69
|
+
env: expect.any(Object),
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
it('should handle stderr in results', async () => {
|
|
73
|
+
const expectedResult = {
|
|
74
|
+
stdout: 'some output',
|
|
75
|
+
stderr: 'warning message',
|
|
76
|
+
};
|
|
77
|
+
mockExistsSync.mockReturnValue(true);
|
|
78
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
79
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
80
|
+
expect(result.stdout).toBe('some output');
|
|
81
|
+
expect(result.stderr).toBe('warning message');
|
|
82
|
+
});
|
|
83
|
+
it('should add --disable-usage-tracking flag to hs commands', async () => {
|
|
84
|
+
const hsCommand = 'hs project upload';
|
|
85
|
+
const expectedResult = {
|
|
86
|
+
stdout: 'success',
|
|
87
|
+
stderr: '',
|
|
88
|
+
};
|
|
89
|
+
mockExistsSync.mockReturnValue(true);
|
|
90
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
91
|
+
await runCommandInDir(mockDirectory, hsCommand);
|
|
92
|
+
expect(mockExecAsync).toHaveBeenCalledWith('hs project upload --disable-usage-tracking "true"', expect.objectContaining({
|
|
93
|
+
cwd: mockResolvedPath,
|
|
94
|
+
env: expect.any(Object),
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
it('should not add --disable-usage-tracking flag to non-hs commands', async () => {
|
|
98
|
+
const nonHsCommand = 'npm install';
|
|
99
|
+
const expectedResult = {
|
|
100
|
+
stdout: 'success',
|
|
101
|
+
stderr: '',
|
|
102
|
+
};
|
|
103
|
+
mockExistsSync.mockReturnValue(true);
|
|
104
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
105
|
+
await runCommandInDir(mockDirectory, nonHsCommand);
|
|
106
|
+
expect(mockExecAsync).toHaveBeenCalledWith('npm install', expect.objectContaining({
|
|
107
|
+
cwd: mockResolvedPath,
|
|
108
|
+
env: expect.any(Object),
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
it('should add --disable-usage-tracking flag to hs commands with existing flags', async () => {
|
|
112
|
+
const hsCommand = 'hs project upload --profile prod';
|
|
113
|
+
const expectedResult = {
|
|
114
|
+
stdout: 'success',
|
|
115
|
+
stderr: '',
|
|
116
|
+
};
|
|
117
|
+
mockExistsSync.mockReturnValue(true);
|
|
118
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
119
|
+
await runCommandInDir(mockDirectory, hsCommand);
|
|
120
|
+
expect(mockExecAsync).toHaveBeenCalledWith('hs project upload --profile prod --disable-usage-tracking "true"', expect.objectContaining({
|
|
121
|
+
cwd: mockResolvedPath,
|
|
122
|
+
env: expect.any(Object),
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
it('should handle hs commands that start with whitespace', async () => {
|
|
126
|
+
const hsCommand = 'hs init';
|
|
127
|
+
const expectedResult = {
|
|
128
|
+
stdout: 'success',
|
|
129
|
+
stderr: '',
|
|
130
|
+
};
|
|
131
|
+
mockExistsSync.mockReturnValue(true);
|
|
132
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
133
|
+
await runCommandInDir(mockDirectory, hsCommand);
|
|
134
|
+
expect(mockExecAsync).toHaveBeenCalledWith('hs init --disable-usage-tracking "true"', expect.objectContaining({
|
|
135
|
+
cwd: mockResolvedPath,
|
|
136
|
+
env: expect.any(Object),
|
|
137
|
+
}));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,8 +1,3 @@
|
|
|
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,5 +1,3 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
1
|
import util from 'util';
|
|
4
2
|
import { exec } from 'node:child_process';
|
|
5
3
|
export const execAsync = util.promisify(exec);
|
|
@@ -9,25 +7,3 @@ export function addFlag(command, flagName, value) {
|
|
|
9
7
|
}
|
|
10
8
|
return `${command} --${flagName} "${value}"`;
|
|
11
9
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { addFlag, execAsync } from './command.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
export async function runCommandInDir(directory, command) {
|
|
5
|
+
if (!fs.existsSync(directory)) {
|
|
6
|
+
fs.mkdirSync(directory);
|
|
7
|
+
}
|
|
8
|
+
let finalCommand = command;
|
|
9
|
+
if (command.startsWith('hs ')) {
|
|
10
|
+
finalCommand = addFlag(finalCommand, 'disable-usage-tracking', true);
|
|
11
|
+
}
|
|
12
|
+
return execAsync(finalCommand, {
|
|
13
|
+
cwd: path.resolve(directory),
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
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';
|
|
5
4
|
import { useCallback, useEffect, useReducer } from 'react';
|
|
6
5
|
import { commands } from '../../../lang/en.js';
|
|
7
|
-
import { createProjectAction,
|
|
8
|
-
import { validateProjectDirectory } from '../../../lib/prompts/projectNameAndDestPrompt.js';
|
|
6
|
+
import { createProjectAction, trackGetStartedUsage, uploadAndDeployAction, } from '../../../lib/getStartedV2Actions.js';
|
|
9
7
|
import { uiAccountDescription } from '../../../lib/ui/index.js';
|
|
10
8
|
import { ACTION_STATUSES, GET_STARTED_FLOW_STEPS, } from '../../lib/constants.js';
|
|
11
9
|
import { flowReducer } from './reducer.js';
|
|
12
|
-
import { InstallationScreen } from './screens/InstallationScreen.js';
|
|
13
10
|
import { ProjectSetupScreen } from './screens/ProjectSetupScreen.js';
|
|
14
11
|
import { UploadScreen } from './screens/UploadScreen.js';
|
|
15
12
|
import { getProject } from './selectors.js';
|
|
@@ -47,7 +44,6 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
47
44
|
statuses: {
|
|
48
45
|
create: initialName ? ACTION_STATUSES.RUNNING : ACTION_STATUSES.IDLE,
|
|
49
46
|
upload: ACTION_STATUSES.IDLE,
|
|
50
|
-
installApp: ACTION_STATUSES.IDLE,
|
|
51
47
|
},
|
|
52
48
|
});
|
|
53
49
|
const [state, dispatch] = useReducer(flowReducer, getInitialState());
|
|
@@ -72,16 +68,6 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
72
68
|
dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.DEST_INPUT });
|
|
73
69
|
}, []);
|
|
74
70
|
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
|
-
}
|
|
85
71
|
dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.CREATING });
|
|
86
72
|
try {
|
|
87
73
|
await createProjectAction({
|
|
@@ -122,57 +108,6 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
122
108
|
dispatch({ type: 'UPLOAD_ERROR', payload: errorMessage });
|
|
123
109
|
}
|
|
124
110
|
}, [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]);
|
|
176
111
|
const handleNameChange = useCallback((value) => {
|
|
177
112
|
dispatch({ type: 'SET_PROJECT_NAME', payload: value });
|
|
178
113
|
}, []);
|
|
@@ -181,8 +116,7 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
181
116
|
}, []);
|
|
182
117
|
useInput((_, key) => {
|
|
183
118
|
const hasError = state.statuses.create === ACTION_STATUSES.ERROR ||
|
|
184
|
-
state.statuses.upload === ACTION_STATUSES.ERROR
|
|
185
|
-
state.statuses.installApp === ACTION_STATUSES.ERROR;
|
|
119
|
+
state.statuses.upload === ACTION_STATUSES.ERROR;
|
|
186
120
|
if (hasError) {
|
|
187
121
|
exit();
|
|
188
122
|
return;
|
|
@@ -192,22 +126,11 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
192
126
|
if (state.step === GET_STARTED_FLOW_STEPS.COMPLETE) {
|
|
193
127
|
handleUploadStart();
|
|
194
128
|
}
|
|
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
|
-
}
|
|
203
129
|
});
|
|
204
130
|
if (state.step === GET_STARTED_FLOW_STEPS.UPLOADING ||
|
|
205
131
|
state.step === GET_STARTED_FLOW_STEPS.OPEN_APP_PROMPT) {
|
|
206
132
|
return _jsx(UploadScreen, { state: state, accountName: accountName });
|
|
207
133
|
}
|
|
208
|
-
if (state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP) {
|
|
209
|
-
return _jsx(InstallationScreen, { state: state, accountName: accountName });
|
|
210
|
-
}
|
|
211
134
|
// Show project setup screen for initial flow
|
|
212
135
|
return (_jsx(ProjectSetupScreen, { state: state, onSelectOption: handleSelect, onNameChange: handleNameChange, onNameSubmit: handleNameSubmit, onDestChange: handleDestChange, onDestSubmit: handleDestSubmit }));
|
|
213
136
|
}
|