@hubspot/cli 7.9.0-experimental.0 → 7.10.0-beta.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.
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import { TextContentResponse, Tool } from '../../types.js';
|
|
2
|
-
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
1
|
import z from 'zod';
|
|
2
|
+
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { TextContentResponse, Tool } from '../../types.js';
|
|
4
4
|
declare const inputSchemaZodObject: z.ZodObject<{
|
|
5
5
|
absoluteProjectPath: z.ZodString;
|
|
6
6
|
absoluteCurrentWorkingDirectory: z.ZodString;
|
|
7
|
+
uploadMessage: z.ZodString;
|
|
8
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
7
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
uploadMessage: string;
|
|
8
11
|
absoluteProjectPath: string;
|
|
9
12
|
absoluteCurrentWorkingDirectory: string;
|
|
13
|
+
profile?: string | undefined;
|
|
10
14
|
}, {
|
|
15
|
+
uploadMessage: string;
|
|
11
16
|
absoluteProjectPath: string;
|
|
12
17
|
absoluteCurrentWorkingDirectory: string;
|
|
18
|
+
profile?: string | undefined;
|
|
13
19
|
}>;
|
|
14
20
|
type InputSchemaType = z.infer<typeof inputSchemaZodObject>;
|
|
15
21
|
export declare class UploadProjectTools extends Tool<InputSchemaType> {
|
|
16
22
|
constructor(mcpServer: McpServer);
|
|
17
|
-
handler({ absoluteProjectPath, absoluteCurrentWorkingDirectory, }: InputSchemaType): Promise<TextContentResponse>;
|
|
23
|
+
handler({ absoluteProjectPath, absoluteCurrentWorkingDirectory, profile, uploadMessage, }: InputSchemaType): Promise<TextContentResponse>;
|
|
18
24
|
register(): RegisteredTool;
|
|
19
25
|
}
|
|
20
26
|
export {};
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
import { getAllHsProfiles } from '@hubspot/project-parsing-lib';
|
|
4
|
+
import { getProjectConfig } from '../../../lib/projects/config.js';
|
|
1
5
|
import { Tool } from '../../types.js';
|
|
2
6
|
import { runCommandInDir } from '../../utils/project.js';
|
|
3
7
|
import { absoluteCurrentWorkingDirectory, absoluteProjectPath, } from './constants.js';
|
|
4
|
-
import
|
|
5
|
-
import { formatTextContents } from '../../utils/content.js';
|
|
8
|
+
import { formatTextContent, formatTextContents } from '../../utils/content.js';
|
|
6
9
|
import { trackToolUsage } from '../../utils/toolUsageTracking.js';
|
|
10
|
+
import { addFlag } from '../../utils/command.js';
|
|
7
11
|
const inputSchema = {
|
|
8
12
|
absoluteProjectPath,
|
|
9
13
|
absoluteCurrentWorkingDirectory,
|
|
14
|
+
uploadMessage: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('A 1 sentence message that concisely describes the changes that are being uploaded.'),
|
|
17
|
+
profile: z
|
|
18
|
+
.optional(z.string())
|
|
19
|
+
.describe('CRITICAL: If the user has not explicitly specified a profile name, you MUST ask them which profile to use. NEVER automatically choose a profile based on files you see in the directory (e.g., seeing "hsprofile.prod.json" does NOT mean you should use "prod"). The profile to be used for the upload. All projects configured to use profiles must specify a profile when uploading. Profile files have the following format: "hsprofile.<profile>.json".'),
|
|
10
20
|
};
|
|
11
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
12
22
|
const inputSchemaZodObject = z.object({
|
|
@@ -17,9 +27,39 @@ export class UploadProjectTools extends Tool {
|
|
|
17
27
|
constructor(mcpServer) {
|
|
18
28
|
super(mcpServer);
|
|
19
29
|
}
|
|
20
|
-
async handler({ absoluteProjectPath, absoluteCurrentWorkingDirectory, }) {
|
|
30
|
+
async handler({ absoluteProjectPath, absoluteCurrentWorkingDirectory, profile, uploadMessage, }) {
|
|
21
31
|
await trackToolUsage(toolName);
|
|
22
|
-
|
|
32
|
+
let command = addFlag('hs project upload', 'force-create', true);
|
|
33
|
+
const content = [];
|
|
34
|
+
if (uploadMessage) {
|
|
35
|
+
command = addFlag(command, 'message', uploadMessage);
|
|
36
|
+
}
|
|
37
|
+
if (profile) {
|
|
38
|
+
command = addFlag(command, 'profile', profile);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
let hasProfiles = false;
|
|
42
|
+
try {
|
|
43
|
+
const { projectConfig } = await getProjectConfig(absoluteProjectPath);
|
|
44
|
+
if (projectConfig) {
|
|
45
|
+
const profiles = await getAllHsProfiles(path.join(absoluteProjectPath, projectConfig.srcDir));
|
|
46
|
+
hasProfiles = profiles.length > 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// If any of these checks fail, the safest thing to do is to assume there are no profiles.
|
|
51
|
+
hasProfiles = false;
|
|
52
|
+
}
|
|
53
|
+
if (hasProfiles) {
|
|
54
|
+
content.push(formatTextContent(`Ask the user which profile they would like to use for the upload.`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (content.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { stdout, stderr } = await runCommandInDir(absoluteProjectPath, command);
|
|
23
63
|
return formatTextContents(absoluteCurrentWorkingDirectory, stdout, stderr);
|
|
24
64
|
}
|
|
25
65
|
register() {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { UploadProjectTools } from '../UploadProjectTools.js';
|
|
2
|
+
import { getAllHsProfiles } from '@hubspot/project-parsing-lib';
|
|
3
|
+
import { getProjectConfig } from '../../../../lib/projects/config.js';
|
|
2
4
|
import { runCommandInDir } from '../../../utils/project.js';
|
|
3
5
|
import { mcpFeedbackRequest } from '../../../utils/feedbackTracking.js';
|
|
4
6
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
7
|
+
vi.mock('@hubspot/project-parsing-lib');
|
|
8
|
+
vi.mock('../../../../lib/projects/config.js');
|
|
5
9
|
vi.mock('../../../utils/project');
|
|
6
10
|
vi.mock('../../../utils/toolUsageTracking');
|
|
7
11
|
vi.mock('../../../utils/feedbackTracking');
|
|
8
12
|
const mockMcpFeedbackRequest = mcpFeedbackRequest;
|
|
9
13
|
const mockRunCommandInDir = runCommandInDir;
|
|
14
|
+
const mockGetProjectConfig = getProjectConfig;
|
|
15
|
+
const mockGetAllHsProfiles = getAllHsProfiles;
|
|
10
16
|
describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
11
17
|
let mockMcpServer;
|
|
12
18
|
let tool;
|
|
@@ -20,6 +26,15 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
20
26
|
mockRegisteredTool = {};
|
|
21
27
|
mockMcpServer.registerTool.mockReturnValue(mockRegisteredTool);
|
|
22
28
|
mockMcpFeedbackRequest.mockResolvedValue('');
|
|
29
|
+
mockGetProjectConfig.mockResolvedValue({
|
|
30
|
+
projectConfig: {
|
|
31
|
+
srcDir: 'src',
|
|
32
|
+
name: 'test-project',
|
|
33
|
+
platformVersion: '2025.2',
|
|
34
|
+
},
|
|
35
|
+
projectDir: '/test/project',
|
|
36
|
+
});
|
|
37
|
+
mockGetAllHsProfiles.mockResolvedValue([]);
|
|
23
38
|
tool = new UploadProjectTools(mockMcpServer);
|
|
24
39
|
});
|
|
25
40
|
describe('register', () => {
|
|
@@ -37,6 +52,7 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
37
52
|
const input = {
|
|
38
53
|
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
39
54
|
absoluteProjectPath: '/test/project',
|
|
55
|
+
uploadMessage: 'Test upload message',
|
|
40
56
|
};
|
|
41
57
|
it('should upload project successfully', async () => {
|
|
42
58
|
mockRunCommandInDir.mockResolvedValue({
|
|
@@ -44,7 +60,9 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
44
60
|
stderr: '',
|
|
45
61
|
});
|
|
46
62
|
const result = await tool.handler(input);
|
|
47
|
-
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', 'hs project upload
|
|
63
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('hs project upload'));
|
|
64
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--force-create'));
|
|
65
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--message "Test upload message"'));
|
|
48
66
|
expect(result).toEqual({
|
|
49
67
|
content: [
|
|
50
68
|
{ type: 'text', text: 'Project uploaded successfully' },
|
|
@@ -68,13 +86,44 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
68
86
|
mockRunCommandInDir.mockRejectedValue(error);
|
|
69
87
|
await expect(tool.handler(input)).rejects.toThrow('Upload failed');
|
|
70
88
|
});
|
|
71
|
-
it('should use force-create
|
|
89
|
+
it('should use force-create and message flags', async () => {
|
|
72
90
|
mockRunCommandInDir.mockResolvedValue({
|
|
73
91
|
stdout: 'Project created and uploaded',
|
|
74
92
|
stderr: '',
|
|
75
93
|
});
|
|
76
94
|
await tool.handler(input);
|
|
77
|
-
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', 'hs project upload
|
|
95
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('hs project upload'));
|
|
96
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--force-create'));
|
|
97
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--message "Test upload message"'));
|
|
98
|
+
});
|
|
99
|
+
it('should use profiles', async () => {
|
|
100
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
101
|
+
stdout: 'Project created and uploaded',
|
|
102
|
+
stderr: '',
|
|
103
|
+
});
|
|
104
|
+
await tool.handler({
|
|
105
|
+
...input,
|
|
106
|
+
profile: 'dev',
|
|
107
|
+
});
|
|
108
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('hs project upload'));
|
|
109
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--force-create'));
|
|
110
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--message "Test upload message"'));
|
|
111
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/project', expect.stringContaining('--profile "dev"'));
|
|
112
|
+
});
|
|
113
|
+
it('should prompt for profile if not specified and the project requires them', async () => {
|
|
114
|
+
mockGetAllHsProfiles.mockResolvedValue(['prod', 'dev']);
|
|
115
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
116
|
+
stdout: 'Project created and uploaded',
|
|
117
|
+
stderr: '',
|
|
118
|
+
});
|
|
119
|
+
const result = await tool.handler(input);
|
|
120
|
+
expect(mockRunCommandInDir).not.toHaveBeenCalled();
|
|
121
|
+
expect(result.content).toEqual([
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: 'Ask the user which profile they would like to use for the upload.',
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
78
127
|
});
|
|
79
128
|
it('should handle empty stdout and stderr', async () => {
|
|
80
129
|
mockRunCommandInDir.mockResolvedValue({
|
|
@@ -95,9 +144,12 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
95
144
|
const differentInput = {
|
|
96
145
|
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
97
146
|
absoluteProjectPath: '/different/path/to/project',
|
|
147
|
+
uploadMessage: 'Different test upload message',
|
|
98
148
|
};
|
|
99
149
|
await tool.handler(differentInput);
|
|
100
|
-
expect(mockRunCommandInDir).toHaveBeenCalledWith('/different/path/to/project', 'hs project upload
|
|
150
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/different/path/to/project', expect.stringContaining('hs project upload'));
|
|
151
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/different/path/to/project', expect.stringContaining('--force-create'));
|
|
152
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/different/path/to/project', expect.stringContaining('--message "Different test upload message"'));
|
|
101
153
|
});
|
|
102
154
|
it('should handle very long output', async () => {
|
|
103
155
|
const longOutput = 'A'.repeat(10000);
|
package/package.json
CHANGED