@hubspot/cli 7.7.23-experimental.0 → 7.7.25-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/account/auth.js +15 -4
- package/commands/auth.js +1 -1
- package/commands/mcp/start.d.ts +1 -0
- package/commands/mcp/start.js +12 -4
- package/commands/project/create.js +2 -2
- package/commands/project/validate.js +1 -0
- package/commands/sandbox/__tests__/create.test.js +207 -0
- package/commands/sandbox/create.d.ts +1 -1
- package/commands/sandbox/create.js +31 -16
- package/lang/en.d.ts +7 -3
- package/lang/en.js +12 -5
- package/lang/en.lyaml +4 -2
- package/lib/__tests__/buildAccount.test.js +62 -4
- package/lib/buildAccount.d.ts +4 -1
- package/lib/buildAccount.js +57 -2
- package/lib/commonOpts.js +25 -0
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +4 -0
- package/lib/errorHandlers/index.js +1 -3
- package/lib/errors/ProjectValidationError.d.ts +4 -0
- package/lib/errors/ProjectValidationError.js +9 -0
- package/lib/mcp/setup.d.ts +4 -0
- package/lib/mcp/setup.js +36 -0
- package/lib/projects/__tests__/LocalDevProcess.test.js +35 -0
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +170 -1
- package/lib/projects/add/v3AddComponent.js +2 -1
- package/lib/projects/create/index.js +2 -2
- package/lib/projects/create/v3.d.ts +0 -2
- package/lib/projects/create/v3.js +1 -3
- package/lib/projects/localDev/LocalDevProcess.d.ts +1 -0
- package/lib/projects/localDev/LocalDevProcess.js +3 -0
- package/lib/projects/localDev/LocalDevState.d.ts +1 -0
- package/lib/projects/localDev/LocalDevState.js +5 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -2
- package/lib/projects/localDev/LocalDevWebsocketServer.js +35 -29
- package/lib/projects/upload.js +5 -12
- package/lib/sandboxes.d.ts +4 -0
- package/lib/sandboxes.js +4 -0
- package/lib/ui/index.d.ts +6 -0
- package/lib/ui/index.js +3 -5
- package/mcp-server/tools/index.js +6 -4
- package/mcp-server/tools/project/{AddFeatureToProject.d.ts → AddFeatureToProjectTool.d.ts} +4 -4
- package/mcp-server/tools/project/{AddFeatureToProject.js → AddFeatureToProjectTool.js} +6 -14
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +4 -14
- package/mcp-server/tools/project/{DeployProject.d.ts → DeployProjectTool.d.ts} +1 -1
- package/mcp-server/tools/project/{DeployProject.js → DeployProjectTool.js} +2 -2
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +20 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +51 -0
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.js → AddFeatureToProjectTool.test.js} +7 -7
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +3 -4
- package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js} +4 -4
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +198 -0
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +1 -0
- package/mcp-server/tools/project/constants.js +11 -0
- package/mcp-server/utils/__tests__/command.test.js +76 -3
- package/mcp-server/utils/command.d.ts +6 -0
- package/mcp-server/utils/command.js +19 -0
- package/package.json +2 -2
- package/mcp-server/utils/__tests__/project.test.js +0 -79
- package/mcp-server/utils/project.d.ts +0 -5
- package/mcp-server/utils/project.js +0 -14
- /package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.d.ts → AddFeatureToProjectTool.test.d.ts} +0 -0
- /package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts} +0 -0
- /package/mcp-server/{utils/__tests__/project.test.d.ts → tools/project/__tests__/GetConfigValuesTool.test.d.ts} +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { CreateProjectTool, } from '../CreateProjectTool.js';
|
|
2
|
-
import { runCommandInDir } from '../../../utils/
|
|
2
|
+
import { runCommandInDir } from '../../../utils/command.js';
|
|
3
3
|
import { addFlag } from '../../../utils/command.js';
|
|
4
|
-
import { APP_DISTRIBUTION_TYPES } from '../../../../lib/constants.js';
|
|
5
|
-
import { EMPTY_PROJECT, PROJECT_WITH_APP, } from '../../../../lib/projects/create/v3.js';
|
|
4
|
+
import { APP_DISTRIBUTION_TYPES, EMPTY_PROJECT, PROJECT_WITH_APP, } from '../../../../lib/constants.js';
|
|
6
5
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
7
|
-
vi.mock('../../../utils/
|
|
6
|
+
vi.mock('../../../utils/command');
|
|
8
7
|
vi.mock('../../../utils/command');
|
|
9
8
|
vi.mock('../../../../lib/constants');
|
|
10
9
|
vi.mock('../../../../lib/projects/create/v3');
|
package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runCommandInDir } from '../../../utils/
|
|
1
|
+
import { DeployProjectTool } from '../DeployProjectTool.js';
|
|
2
|
+
import { runCommandInDir } from '../../../utils/command.js';
|
|
3
3
|
import { addFlag } from '../../../utils/command.js';
|
|
4
4
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
-
vi.mock('../../../utils/
|
|
5
|
+
vi.mock('../../../utils/command');
|
|
6
6
|
vi.mock('../../../utils/command');
|
|
7
7
|
vi.mock('../../../utils/toolUsageTracking');
|
|
8
8
|
const mockRunCommandInDir = runCommandInDir;
|
|
@@ -19,7 +19,7 @@ describe('mcp-server/tools/project/DeployProject', () => {
|
|
|
19
19
|
};
|
|
20
20
|
mockRegisteredTool = {};
|
|
21
21
|
mockMcpServer.registerTool.mockReturnValue(mockRegisteredTool);
|
|
22
|
-
tool = new
|
|
22
|
+
tool = new DeployProjectTool(mockMcpServer);
|
|
23
23
|
// Mock addFlag to simulate command building
|
|
24
24
|
mockAddFlag.mockImplementation((command, flag, value) => `${command} --${flag} "${value}"`);
|
|
25
25
|
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { GetConfigValuesTool } from '../GetConfigValuesTool.js';
|
|
2
|
+
import { getIntermediateRepresentationSchema, mapToInternalType, } from '@hubspot/project-parsing-lib';
|
|
3
|
+
import { getAccountId } from '@hubspot/local-dev-lib/config';
|
|
4
|
+
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
+
vi.mock('@hubspot/project-parsing-lib');
|
|
6
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
7
|
+
vi.mock('../../../utils/toolUsageTracking');
|
|
8
|
+
const mockGetIntermediateRepresentationSchema = getIntermediateRepresentationSchema;
|
|
9
|
+
const mockMapToInternalType = mapToInternalType;
|
|
10
|
+
const mockGetAccountId = getAccountId;
|
|
11
|
+
describe('mcp-server/tools/project/GetConfigValuesTool', () => {
|
|
12
|
+
let mockMcpServer;
|
|
13
|
+
let tool;
|
|
14
|
+
let mockRegisteredTool;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
// @ts-expect-error Not mocking the whole thing
|
|
18
|
+
mockMcpServer = {
|
|
19
|
+
registerTool: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
mockRegisteredTool = {};
|
|
22
|
+
mockMcpServer.registerTool.mockReturnValue(mockRegisteredTool);
|
|
23
|
+
tool = new GetConfigValuesTool(mockMcpServer);
|
|
24
|
+
});
|
|
25
|
+
describe('register', () => {
|
|
26
|
+
it('should register tool with correct parameters', () => {
|
|
27
|
+
const result = tool.register();
|
|
28
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-hubspot-project-feature-config-schema', {
|
|
29
|
+
title: 'Fetch the JSON Schema for component',
|
|
30
|
+
description: expect.stringContaining('Fetches and returns the JSON schema for the provided feature'),
|
|
31
|
+
inputSchema: expect.objectContaining({
|
|
32
|
+
platformVersion: expect.objectContaining({
|
|
33
|
+
describe: expect.any(Function),
|
|
34
|
+
}),
|
|
35
|
+
featureType: expect.objectContaining({
|
|
36
|
+
describe: expect.any(Function),
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
}, tool.handler);
|
|
40
|
+
expect(result).toBe(mockRegisteredTool);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('handler', () => {
|
|
44
|
+
const input = {
|
|
45
|
+
platformVersion: '2025.2',
|
|
46
|
+
featureType: 'card',
|
|
47
|
+
};
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockGetAccountId.mockReturnValue(123456789);
|
|
50
|
+
});
|
|
51
|
+
it('should return config schema when component type exists', async () => {
|
|
52
|
+
const mockSchema = {
|
|
53
|
+
'internal-card-type': {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
title: { type: 'string' },
|
|
57
|
+
description: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
mockGetIntermediateRepresentationSchema.mockResolvedValue(mockSchema);
|
|
62
|
+
mockMapToInternalType.mockReturnValue('internal-card-type');
|
|
63
|
+
const result = await tool.handler(input);
|
|
64
|
+
expect(mockGetIntermediateRepresentationSchema).toHaveBeenCalledWith({
|
|
65
|
+
platformVersion: '2025.2',
|
|
66
|
+
projectSourceDir: '',
|
|
67
|
+
accountId: 123456789,
|
|
68
|
+
});
|
|
69
|
+
expect(mockMapToInternalType).toHaveBeenCalledWith('card');
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: JSON.stringify({
|
|
75
|
+
config: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
title: { type: 'string' },
|
|
79
|
+
description: { type: 'string' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it('should return error message when component type does not exist in schema', async () => {
|
|
88
|
+
const mockSchema = {
|
|
89
|
+
'other-type': {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
mockGetIntermediateRepresentationSchema.mockResolvedValue(mockSchema);
|
|
95
|
+
mockMapToInternalType.mockReturnValue('internal-card-type');
|
|
96
|
+
const result = await tool.handler(input);
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: 'Unable to locate JSON schema for type card',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it('should return error message when getIntermediateRepresentationSchema throws', async () => {
|
|
107
|
+
mockGetIntermediateRepresentationSchema.mockRejectedValue(new Error('Schema fetch failed'));
|
|
108
|
+
mockMapToInternalType.mockReturnValue('internal-card-type');
|
|
109
|
+
const result = await tool.handler(input);
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: 'Unable to locate JSON schema for type card',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it('should return error message when mapToInternalType throws', async () => {
|
|
120
|
+
const mockSchema = {};
|
|
121
|
+
mockGetIntermediateRepresentationSchema.mockResolvedValue(mockSchema);
|
|
122
|
+
mockMapToInternalType.mockImplementation(() => {
|
|
123
|
+
throw new Error('Mapping failed');
|
|
124
|
+
});
|
|
125
|
+
const result = await tool.handler(input);
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: 'Unable to locate JSON schema for type card',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it('should handle null account id', async () => {
|
|
136
|
+
mockGetAccountId.mockReturnValue(null);
|
|
137
|
+
mockGetIntermediateRepresentationSchema.mockRejectedValue(new Error('No account ID'));
|
|
138
|
+
const result = await tool.handler(input);
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: 'Unable to locate JSON schema for type card',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it('should handle empty schema object', async () => {
|
|
149
|
+
const mockSchema = {};
|
|
150
|
+
mockGetIntermediateRepresentationSchema.mockResolvedValue(mockSchema);
|
|
151
|
+
mockMapToInternalType.mockReturnValue('internal-card-type');
|
|
152
|
+
const result = await tool.handler(input);
|
|
153
|
+
expect(result).toEqual({
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: 'Unable to locate JSON schema for type card',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
it('should handle complex nested schema structures', async () => {
|
|
163
|
+
const complexSchema = {
|
|
164
|
+
'internal-card-type': {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
title: { type: 'string', maxLength: 100 },
|
|
168
|
+
metadata: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
author: { type: 'string' },
|
|
172
|
+
tags: {
|
|
173
|
+
type: 'array',
|
|
174
|
+
items: { type: 'string' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['author'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
required: ['title'],
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
mockGetIntermediateRepresentationSchema.mockResolvedValue(complexSchema);
|
|
184
|
+
mockMapToInternalType.mockReturnValue('internal-card-type');
|
|
185
|
+
const result = await tool.handler(input);
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: 'text',
|
|
190
|
+
text: JSON.stringify({
|
|
191
|
+
config: complexSchema['internal-card-type'],
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UploadProjectTools } from '../UploadProjectTools.js';
|
|
2
|
-
import { runCommandInDir } from '../../../utils/
|
|
2
|
+
import { runCommandInDir } from '../../../utils/command.js';
|
|
3
3
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
4
|
-
vi.mock('../../../utils/
|
|
4
|
+
vi.mock('../../../utils/command');
|
|
5
5
|
vi.mock('../../../utils/toolUsageTracking');
|
|
6
6
|
const mockRunCommandInDir = runCommandInDir;
|
|
7
7
|
describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ValidateProjectTool, } from '../ValidateProjectTool.js';
|
|
2
|
-
import { runCommandInDir } from '../../../utils/
|
|
2
|
+
import { runCommandInDir } from '../../../utils/command.js';
|
|
3
3
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
4
|
-
vi.mock('../../../utils/
|
|
4
|
+
vi.mock('../../../utils/command');
|
|
5
5
|
vi.mock('../../../utils/toolUsageTracking');
|
|
6
6
|
const mockRunCommandInDir = runCommandInDir;
|
|
7
7
|
describe('mcp-server/tools/project/ValidateProjectTool', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
export declare const absoluteProjectPath: z.ZodString;
|
|
3
3
|
export declare const absoluteCurrentWorkingDirectory: z.ZodString;
|
|
4
|
+
export declare const features: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"card">, z.ZodLiteral<"settings">, z.ZodLiteral<"app-function">, z.ZodLiteral<"webhooks">, z.ZodLiteral<"workflow-action">]>, "many">>;
|
|
@@ -5,3 +5,14 @@ export const absoluteProjectPath = z
|
|
|
5
5
|
export const absoluteCurrentWorkingDirectory = z
|
|
6
6
|
.string()
|
|
7
7
|
.describe('The absolute path to the current working directory.');
|
|
8
|
+
export const features = z
|
|
9
|
+
.array(z
|
|
10
|
+
.union([
|
|
11
|
+
z.literal('card'),
|
|
12
|
+
z.literal('settings'),
|
|
13
|
+
z.literal('app-function'),
|
|
14
|
+
z.literal('webhooks'),
|
|
15
|
+
z.literal('workflow-action'),
|
|
16
|
+
])
|
|
17
|
+
.describe('The features to include in the project, multiple options can be selected'))
|
|
18
|
+
.optional();
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { addFlag } from '../command.js';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { addFlag, runCommandInDir, execAsync } from '../command.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
vi.mock('../command');
|
|
5
|
+
vi.mock('fs');
|
|
6
|
+
vi.mock('path');
|
|
7
|
+
const mockExecAsync = execAsync;
|
|
8
|
+
const mockExistsSync = fs.existsSync;
|
|
9
|
+
const mockMkdirSync = fs.mkdirSync;
|
|
10
|
+
const mockResolve = path.resolve;
|
|
4
11
|
describe('mcp-server/utils/command', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
5
15
|
describe('addFlag', () => {
|
|
6
16
|
it('should add string flag to command', () => {
|
|
7
17
|
const result = addFlag('hs project create', 'name', 'test-project');
|
|
@@ -42,4 +52,67 @@ describe('mcp-server/utils/command', () => {
|
|
|
42
52
|
expect(result).toBe('hs project create --features "card with spaces" "settings"');
|
|
43
53
|
});
|
|
44
54
|
});
|
|
55
|
+
describe('runCommandInDir', () => {
|
|
56
|
+
const mockDirectory = '/test/directory';
|
|
57
|
+
const mockCommand = 'npm install';
|
|
58
|
+
const mockResolvedPath = '/resolved/test/directory';
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
mockResolve.mockReturnValue(mockResolvedPath);
|
|
61
|
+
});
|
|
62
|
+
it('should run command in existing directory', async () => {
|
|
63
|
+
const expectedResult = {
|
|
64
|
+
stdout: 'command output',
|
|
65
|
+
stderr: '',
|
|
66
|
+
};
|
|
67
|
+
mockExistsSync.mockReturnValue(true);
|
|
68
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
69
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
70
|
+
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
71
|
+
expect(mockMkdirSync).not.toHaveBeenCalled();
|
|
72
|
+
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
73
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
74
|
+
cwd: mockResolvedPath,
|
|
75
|
+
env: expect.any(Object),
|
|
76
|
+
}));
|
|
77
|
+
expect(result).toEqual(expectedResult);
|
|
78
|
+
});
|
|
79
|
+
it('should create directory if it does not exist', async () => {
|
|
80
|
+
const expectedResult = {
|
|
81
|
+
stdout: 'command output',
|
|
82
|
+
stderr: '',
|
|
83
|
+
};
|
|
84
|
+
mockExistsSync.mockReturnValue(false);
|
|
85
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
86
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
87
|
+
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
88
|
+
expect(mockMkdirSync).toHaveBeenCalledWith(mockDirectory);
|
|
89
|
+
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
90
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
91
|
+
cwd: mockResolvedPath,
|
|
92
|
+
env: expect.any(Object),
|
|
93
|
+
}));
|
|
94
|
+
expect(result).toEqual(expectedResult);
|
|
95
|
+
});
|
|
96
|
+
it('should propagate execAsync errors', async () => {
|
|
97
|
+
const error = new Error('Command failed');
|
|
98
|
+
mockExistsSync.mockReturnValue(true);
|
|
99
|
+
mockExecAsync.mockRejectedValue(error);
|
|
100
|
+
await expect(runCommandInDir(mockDirectory, mockCommand)).rejects.toThrow('Command failed');
|
|
101
|
+
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
102
|
+
cwd: mockResolvedPath,
|
|
103
|
+
env: expect.any(Object),
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
it('should handle stderr in results', async () => {
|
|
107
|
+
const expectedResult = {
|
|
108
|
+
stdout: 'some output',
|
|
109
|
+
stderr: 'warning message',
|
|
110
|
+
};
|
|
111
|
+
mockExistsSync.mockReturnValue(true);
|
|
112
|
+
mockExecAsync.mockResolvedValue(expectedResult);
|
|
113
|
+
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
114
|
+
expect(result.stdout).toBe('some output');
|
|
115
|
+
expect(result.stderr).toBe('warning message');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
45
118
|
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
|
+
export interface CommandResults {
|
|
3
|
+
stderr: string;
|
|
4
|
+
stdout: string;
|
|
5
|
+
}
|
|
2
6
|
export declare const execAsync: typeof exec.__promisify__;
|
|
7
|
+
export declare function wrapCommand(commandString: string): string;
|
|
3
8
|
export declare function addFlag(command: string, flagName: string, value: string | number | boolean | string[]): string;
|
|
9
|
+
export declare function runCommandInDir(directory: string, command: string): Promise<CommandResults>;
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import util from 'util';
|
|
2
2
|
import { exec } from 'node:child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
3
5
|
export const execAsync = util.promisify(exec);
|
|
6
|
+
export function wrapCommand(commandString) {
|
|
7
|
+
if (process.env.HUBSPOT_MCP_STANDALONE_MODE) {
|
|
8
|
+
return `npx -y --package=@hubspot/cli@7.7.25-experimental.0 ${commandString}`;
|
|
9
|
+
}
|
|
10
|
+
return commandString;
|
|
11
|
+
}
|
|
4
12
|
export function addFlag(command, flagName, value) {
|
|
5
13
|
if (Array.isArray(value)) {
|
|
6
14
|
return `${command} --${flagName} ${value.map(item => `"${item}"`).join(' ')}`;
|
|
7
15
|
}
|
|
8
16
|
return `${command} --${flagName} "${value}"`;
|
|
9
17
|
}
|
|
18
|
+
export async function runCommandInDir(directory, command) {
|
|
19
|
+
if (!fs.existsSync(directory)) {
|
|
20
|
+
fs.mkdirSync(directory);
|
|
21
|
+
}
|
|
22
|
+
return execAsync(wrapCommand(command), {
|
|
23
|
+
cwd: path.resolve(directory),
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.25-experimental.0",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@hubspot/local-dev-lib": "3.16.0",
|
|
10
|
-
"@hubspot/project-parsing-lib": "0.
|
|
10
|
+
"@hubspot/project-parsing-lib": "0.6.0",
|
|
11
11
|
"@hubspot/serverless-dev-runtime": "7.0.6",
|
|
12
12
|
"@hubspot/theme-preview-dev-server": "0.0.10",
|
|
13
13
|
"@hubspot/ui-extensions-dev-server": "0.9.4",
|
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
vi.mock('fs');
|
|
7
|
-
vi.mock('path');
|
|
8
|
-
const mockExecAsync = execAsync;
|
|
9
|
-
const mockExistsSync = fs.existsSync;
|
|
10
|
-
const mockMkdirSync = fs.mkdirSync;
|
|
11
|
-
const mockResolve = path.resolve;
|
|
12
|
-
describe('mcp-server/utils/project', () => {
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
vi.clearAllMocks();
|
|
15
|
-
});
|
|
16
|
-
describe('runCommandInDir', () => {
|
|
17
|
-
const mockDirectory = '/test/directory';
|
|
18
|
-
const mockCommand = 'npm install';
|
|
19
|
-
const mockResolvedPath = '/resolved/test/directory';
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
mockResolve.mockReturnValue(mockResolvedPath);
|
|
22
|
-
});
|
|
23
|
-
it('should run command in existing directory', async () => {
|
|
24
|
-
const expectedResult = {
|
|
25
|
-
stdout: 'command output',
|
|
26
|
-
stderr: '',
|
|
27
|
-
};
|
|
28
|
-
mockExistsSync.mockReturnValue(true);
|
|
29
|
-
mockExecAsync.mockResolvedValue(expectedResult);
|
|
30
|
-
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
31
|
-
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
32
|
-
expect(mockMkdirSync).not.toHaveBeenCalled();
|
|
33
|
-
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
34
|
-
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
35
|
-
cwd: mockResolvedPath,
|
|
36
|
-
env: expect.any(Object),
|
|
37
|
-
}));
|
|
38
|
-
expect(result).toEqual(expectedResult);
|
|
39
|
-
});
|
|
40
|
-
it('should create directory if it does not exist', async () => {
|
|
41
|
-
const expectedResult = {
|
|
42
|
-
stdout: 'command output',
|
|
43
|
-
stderr: '',
|
|
44
|
-
};
|
|
45
|
-
mockExistsSync.mockReturnValue(false);
|
|
46
|
-
mockExecAsync.mockResolvedValue(expectedResult);
|
|
47
|
-
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
48
|
-
expect(mockExistsSync).toHaveBeenCalledWith(mockDirectory);
|
|
49
|
-
expect(mockMkdirSync).toHaveBeenCalledWith(mockDirectory);
|
|
50
|
-
expect(mockResolve).toHaveBeenCalledWith(mockDirectory);
|
|
51
|
-
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
52
|
-
cwd: mockResolvedPath,
|
|
53
|
-
env: expect.any(Object),
|
|
54
|
-
}));
|
|
55
|
-
expect(result).toEqual(expectedResult);
|
|
56
|
-
});
|
|
57
|
-
it('should propagate execAsync errors', async () => {
|
|
58
|
-
const error = new Error('Command failed');
|
|
59
|
-
mockExistsSync.mockReturnValue(true);
|
|
60
|
-
mockExecAsync.mockRejectedValue(error);
|
|
61
|
-
await expect(runCommandInDir(mockDirectory, mockCommand)).rejects.toThrow('Command failed');
|
|
62
|
-
expect(mockExecAsync).toHaveBeenCalledWith(mockCommand, expect.objectContaining({
|
|
63
|
-
cwd: mockResolvedPath,
|
|
64
|
-
env: expect.any(Object),
|
|
65
|
-
}));
|
|
66
|
-
});
|
|
67
|
-
it('should handle stderr in results', async () => {
|
|
68
|
-
const expectedResult = {
|
|
69
|
-
stdout: 'some output',
|
|
70
|
-
stderr: 'warning message',
|
|
71
|
-
};
|
|
72
|
-
mockExistsSync.mockReturnValue(true);
|
|
73
|
-
mockExecAsync.mockResolvedValue(expectedResult);
|
|
74
|
-
const result = await runCommandInDir(mockDirectory, mockCommand);
|
|
75
|
-
expect(result.stdout).toBe('some output');
|
|
76
|
-
expect(result.stderr).toBe('warning message');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { 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
|
-
return execAsync(command, {
|
|
9
|
-
cwd: path.resolve(directory),
|
|
10
|
-
env: {
|
|
11
|
-
...process.env,
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
}
|
|
File without changes
|
/package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|