@hubspot/cli 8.0.0-beta.0 → 8.0.0-beta.1
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/__tests__/fetch.test.js +2 -1
- package/commands/cms/fetch.js +2 -1
- package/commands/project/__tests__/create.test.js +1 -1
- package/commands/project/__tests__/dev.test.js +39 -8
- package/commands/project/dev/index.js +18 -15
- package/commands/testAccount/create.js +1 -1
- package/lang/en.d.ts +4 -1
- package/lang/en.js +17 -14
- package/lib/errorHandlers/__tests__/index.test.d.ts +1 -0
- package/lib/errorHandlers/__tests__/index.test.js +278 -0
- package/lib/errorHandlers/index.js +11 -2
- package/lib/projects/__tests__/components.test.js +1 -1
- package/lib/projects/components.js +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +2 -2
- package/mcp-server/tools/project/CreateProjectTool.js +2 -2
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +2 -2
- package/mcp-server/tools/project/GetApplicationInfoTool.js +3 -3
- package/mcp-server/tools/project/UploadProjectTools.js +5 -2
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +3 -3
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +12 -0
- package/package.json +3 -3
- package/types/Cms.d.ts +1 -1
- package/types/Cms.js +2 -0
|
@@ -101,12 +101,13 @@ describe('commands/cms/fetch', () => {
|
|
|
101
101
|
await fetchCommand.handler(args);
|
|
102
102
|
expect(trackCommandUsageSpy).toHaveBeenCalledWith('fetch', { mode: 'publish' }, 123456);
|
|
103
103
|
});
|
|
104
|
-
it('should fetch file successfully', async () => {
|
|
104
|
+
it('should fetch file successfully with increased timeout', async () => {
|
|
105
105
|
await fetchCommand.handler(args);
|
|
106
106
|
expect(downloadFileOrFolderSpy).toHaveBeenCalledWith(123456, '/remote/path/file.js', './local/path', 'publish', expect.objectContaining({
|
|
107
107
|
assetVersion: undefined,
|
|
108
108
|
staging: undefined,
|
|
109
109
|
overwrite: undefined,
|
|
110
|
+
timeout: 60000,
|
|
110
111
|
}));
|
|
111
112
|
});
|
|
112
113
|
it('should use default dest if not provided', async () => {
|
package/commands/cms/fetch.js
CHANGED
|
@@ -24,11 +24,12 @@ async function handler(options) {
|
|
|
24
24
|
trackCommandUsage('fetch', { mode: cmsPublishMode }, derivedAccountId);
|
|
25
25
|
const { assetVersion, staging, overwrite } = options;
|
|
26
26
|
try {
|
|
27
|
-
// Fetch and write file/folder.
|
|
27
|
+
// Fetch and write file/folder with increased timeout for large themes.
|
|
28
28
|
await downloadFileOrFolder(derivedAccountId, src, resolveLocalPath(dest), cmsPublishMode, {
|
|
29
29
|
assetVersion: assetVersion !== undefined ? `${assetVersion}` : assetVersion,
|
|
30
30
|
staging,
|
|
31
31
|
overwrite,
|
|
32
|
+
timeout: 60_000,
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
catch (err) {
|
|
@@ -79,7 +79,7 @@ describe('commands/project/create', () => {
|
|
|
79
79
|
projectCreateCommand.builder(yargsMock);
|
|
80
80
|
const optionsCall = optionsSpy.mock.calls[0][0];
|
|
81
81
|
expect(optionsCall.auth).toEqual(expect.objectContaining({
|
|
82
|
-
describe: 'Authentication model for the
|
|
82
|
+
describe: 'Authentication model for the app.',
|
|
83
83
|
type: 'string',
|
|
84
84
|
choices: ['oauth', 'static'],
|
|
85
85
|
}));
|
|
@@ -2,6 +2,8 @@ import * as configLib from '@hubspot/local-dev-lib/config';
|
|
|
2
2
|
import * as projectConfigLib from '../../../lib/projects/config.js';
|
|
3
3
|
import * as platformVersionLib from '../../../lib/projects/platformVersion.js';
|
|
4
4
|
import * as projectProfilesLib from '../../../lib/projects/projectProfiles.js';
|
|
5
|
+
import * as projectParsingProfiles from '@hubspot/project-parsing-lib/profiles';
|
|
6
|
+
import * as promptUtilsLib from '../../../lib/prompts/promptUtils.js';
|
|
5
7
|
import * as usageTrackingLib from '../../../lib/usageTracking.js';
|
|
6
8
|
import * as errorHandlers from '../../../lib/errorHandlers/index.js';
|
|
7
9
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
@@ -10,9 +12,11 @@ import * as deprecatedFlowLib from '../dev/deprecatedFlow.js';
|
|
|
10
12
|
import * as unifiedFlowLib from '../dev/unifiedFlow.js';
|
|
11
13
|
import projectDevCommand from '../dev/index.js';
|
|
12
14
|
vi.mock('@hubspot/local-dev-lib/config');
|
|
15
|
+
vi.mock('@hubspot/project-parsing-lib/profiles');
|
|
13
16
|
vi.mock('../../../lib/projects/config.js');
|
|
14
17
|
vi.mock('../../../lib/projects/platformVersion.js');
|
|
15
18
|
vi.mock('../../../lib/projects/projectProfiles.js');
|
|
19
|
+
vi.mock('../../../lib/prompts/promptUtils.js');
|
|
16
20
|
vi.mock('../../../lib/errorHandlers/index.js');
|
|
17
21
|
vi.mock('../../../lib/ui/index.js');
|
|
18
22
|
vi.mock('../dev/deprecatedFlow.js');
|
|
@@ -23,6 +27,8 @@ const validateProjectConfigSpy = vi.spyOn(projectConfigLib, 'validateProjectConf
|
|
|
23
27
|
const isV2ProjectSpy = vi.spyOn(platformVersionLib, 'isV2Project');
|
|
24
28
|
const loadProfileSpy = vi.spyOn(projectProfilesLib, 'loadProfile');
|
|
25
29
|
const enforceProfileUsageSpy = vi.spyOn(projectProfilesLib, 'enforceProfileUsage');
|
|
30
|
+
const getAllHsProfilesSpy = vi.spyOn(projectParsingProfiles, 'getAllHsProfiles');
|
|
31
|
+
const listPromptSpy = vi.spyOn(promptUtilsLib, 'listPrompt');
|
|
26
32
|
const trackCommandUsageSpy = vi.spyOn(usageTrackingLib, 'trackCommandUsage');
|
|
27
33
|
const logErrorSpy = vi.spyOn(errorHandlers, 'logError');
|
|
28
34
|
const deprecatedProjectDevFlowSpy = vi.spyOn(deprecatedFlowLib, 'deprecatedProjectDevFlow');
|
|
@@ -46,6 +52,8 @@ describe('commands/project/dev', () => {
|
|
|
46
52
|
deprecatedProjectDevFlowSpy.mockResolvedValue(undefined);
|
|
47
53
|
unifiedProjectDevFlowSpy.mockResolvedValue(undefined);
|
|
48
54
|
enforceProfileUsageSpy.mockResolvedValue(undefined);
|
|
55
|
+
getAllHsProfilesSpy.mockResolvedValue([]);
|
|
56
|
+
listPromptSpy.mockResolvedValue('dev');
|
|
49
57
|
});
|
|
50
58
|
describe('command', () => {
|
|
51
59
|
it('should have the correct command structure', () => {
|
|
@@ -96,7 +104,11 @@ describe('commands/project/dev', () => {
|
|
|
96
104
|
},
|
|
97
105
|
projectDir: null,
|
|
98
106
|
});
|
|
99
|
-
|
|
107
|
+
// Make process.exit actually throw to stop execution
|
|
108
|
+
processExitSpy.mockImplementation((code) => {
|
|
109
|
+
throw new Error(`process.exit called with ${code}`);
|
|
110
|
+
});
|
|
111
|
+
await expect(projectDevCommand.handler(args)).rejects.toThrow('process.exit called');
|
|
100
112
|
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('project'));
|
|
101
113
|
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
102
114
|
});
|
|
@@ -160,18 +172,37 @@ describe('commands/project/dev', () => {
|
|
|
160
172
|
expect(logErrorSpy).toHaveBeenCalledWith(error);
|
|
161
173
|
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
162
174
|
});
|
|
163
|
-
it('should
|
|
175
|
+
it('should prompt for profile selection when profiles exist and no profile is specified', async () => {
|
|
176
|
+
getAllHsProfilesSpy.mockResolvedValue(['dev', 'prod']);
|
|
177
|
+
listPromptSpy.mockResolvedValue('dev');
|
|
178
|
+
loadProfileSpy.mockReturnValue({
|
|
179
|
+
accountId: 789012,
|
|
180
|
+
variables: {},
|
|
181
|
+
});
|
|
164
182
|
await projectDevCommand.handler(args);
|
|
165
|
-
expect(
|
|
183
|
+
expect(getAllHsProfilesSpy).toHaveBeenCalledWith('/test/project/src');
|
|
184
|
+
expect(listPromptSpy).toHaveBeenCalledWith(expect.any(String), {
|
|
185
|
+
choices: ['dev', 'prod'],
|
|
186
|
+
});
|
|
187
|
+
expect(loadProfileSpy).toHaveBeenCalledWith({
|
|
166
188
|
name: 'test-project',
|
|
167
189
|
srcDir: 'src',
|
|
168
190
|
platformVersion: 'v2',
|
|
169
|
-
}, '/test/project');
|
|
191
|
+
}, '/test/project', 'dev');
|
|
192
|
+
expect(trackCommandUsageSpy).toHaveBeenCalledWith('project-dev', {}, 789012);
|
|
170
193
|
});
|
|
171
|
-
it('should exit if profile
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
it('should exit if profile loading fails after selection', async () => {
|
|
195
|
+
getAllHsProfilesSpy.mockResolvedValue(['dev', 'prod']);
|
|
196
|
+
listPromptSpy.mockResolvedValue('dev');
|
|
197
|
+
const error = new Error('Failed to load profile');
|
|
198
|
+
loadProfileSpy.mockImplementation(() => {
|
|
199
|
+
throw error;
|
|
200
|
+
});
|
|
201
|
+
// Make process.exit actually throw to stop execution
|
|
202
|
+
processExitSpy.mockImplementation((code) => {
|
|
203
|
+
throw new Error(`process.exit called with ${code}`);
|
|
204
|
+
});
|
|
205
|
+
await expect(projectDevCommand.handler(args)).rejects.toThrow('process.exit called');
|
|
175
206
|
expect(logErrorSpy).toHaveBeenCalledWith(error);
|
|
176
207
|
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
177
208
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { trackCommandUsage } from '../../../lib/usageTracking.js';
|
|
2
2
|
import { getConfigAccountIfExists } from '@hubspot/local-dev-lib/config';
|
|
3
|
+
import { getAllHsProfiles, } from '@hubspot/project-parsing-lib/profiles';
|
|
3
4
|
import { getProjectConfig, validateProjectConfig, } from '../../../lib/projects/config.js';
|
|
4
5
|
import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
|
|
5
6
|
import { uiLine } from '../../../lib/ui/index.js';
|
|
@@ -7,10 +8,12 @@ import { deprecatedProjectDevFlow } from './deprecatedFlow.js';
|
|
|
7
8
|
import { unifiedProjectDevFlow } from './unifiedFlow.js';
|
|
8
9
|
import { isV2Project } from '../../../lib/projects/platformVersion.js';
|
|
9
10
|
import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
|
|
10
|
-
import { loadProfile
|
|
11
|
+
import { loadProfile } from '../../../lib/projects/projectProfiles.js';
|
|
11
12
|
import { commands } from '../../../lang/en.js';
|
|
12
13
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
13
14
|
import { logError } from '../../../lib/errorHandlers/index.js';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { listPrompt } from '../../../lib/prompts/promptUtils.js';
|
|
14
17
|
const command = 'dev';
|
|
15
18
|
const describe = commands.project.dev.describe;
|
|
16
19
|
function validateAccountFlags(testingAccount, projectAccount, userProvidedAccount, useV2) {
|
|
@@ -62,27 +65,27 @@ async function handler(args) {
|
|
|
62
65
|
else if (userProvidedAccount && derivedAccountId) {
|
|
63
66
|
targetProjectAccountId = derivedAccountId;
|
|
64
67
|
}
|
|
68
|
+
// Determine profile name: from flag or prompt
|
|
65
69
|
if (!targetProjectAccountId && isV2Project(projectConfig.platformVersion)) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
process.exit(EXIT_CODES.ERROR);
|
|
70
|
+
let profileName = args.profile;
|
|
71
|
+
if (!profileName) {
|
|
72
|
+
const existingProfiles = await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir));
|
|
73
|
+
if (existingProfiles.length !== 0) {
|
|
74
|
+
profileName = await listPrompt(commands.project.dev.prompts.selectProfile, {
|
|
75
|
+
choices: existingProfiles,
|
|
76
|
+
});
|
|
74
77
|
}
|
|
75
|
-
targetProjectAccountId = profile.accountId;
|
|
76
|
-
uiLogger.log('');
|
|
77
|
-
uiLogger.log(commands.project.dev.logs.profileProjectAccountExplanation(targetProjectAccountId, args.profile));
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
// A profile must be specified if this project has profiles configured
|
|
79
|
+
if (profileName) {
|
|
81
80
|
try {
|
|
82
|
-
|
|
81
|
+
profile = loadProfile(projectConfig, projectDir, profileName);
|
|
82
|
+
targetProjectAccountId = profile.accountId;
|
|
83
|
+
uiLogger.log('');
|
|
84
|
+
uiLogger.log(commands.project.dev.logs.profileProjectAccountExplanation(targetProjectAccountId, profileName));
|
|
83
85
|
}
|
|
84
86
|
catch (error) {
|
|
85
87
|
logError(error);
|
|
88
|
+
uiLine();
|
|
86
89
|
process.exit(EXIT_CODES.ERROR);
|
|
87
90
|
}
|
|
88
91
|
}
|
|
@@ -114,7 +114,7 @@ async function handler(args) {
|
|
|
114
114
|
process.exit(EXIT_CODES.ERROR);
|
|
115
115
|
}
|
|
116
116
|
SpinniesManager.succeed('createTestAccount', {
|
|
117
|
-
text: commands.testAccount.create.polling.success(testAccountConfig.accountName, resultJson.accountId),
|
|
117
|
+
text: commands.testAccount.create.polling.success(testAccountConfig.accountName, resultJson.accountId, derivedAccountId),
|
|
118
118
|
});
|
|
119
119
|
if (formatOutputAsJson) {
|
|
120
120
|
uiLogger.json(resultJson);
|
package/lang/en.d.ts
CHANGED
|
@@ -1369,6 +1369,9 @@ export declare const commands: {
|
|
|
1369
1369
|
examples: {
|
|
1370
1370
|
default: string;
|
|
1371
1371
|
};
|
|
1372
|
+
prompts: {
|
|
1373
|
+
selectProfile: string;
|
|
1374
|
+
};
|
|
1372
1375
|
options: {
|
|
1373
1376
|
profile: string;
|
|
1374
1377
|
projectAccount: string;
|
|
@@ -2160,7 +2163,7 @@ export declare const commands: {
|
|
|
2160
2163
|
polling: {
|
|
2161
2164
|
start: (testAccountName: string) => string;
|
|
2162
2165
|
syncing: string;
|
|
2163
|
-
success: (testAccountName: string, testAccountId: number) => string;
|
|
2166
|
+
success: (testAccountName: string, testAccountId: number, parentAccountId: number) => string;
|
|
2164
2167
|
createFailure: string;
|
|
2165
2168
|
};
|
|
2166
2169
|
options: {
|
package/lang/en.js
CHANGED
|
@@ -1219,7 +1219,7 @@ export const commands = {
|
|
|
1219
1219
|
windsurf: 'Windsurf',
|
|
1220
1220
|
vsCode: 'VSCode',
|
|
1221
1221
|
args: {
|
|
1222
|
-
client: 'Target
|
|
1222
|
+
client: 'Target apps to configure',
|
|
1223
1223
|
docsSearch: 'Should the docs search mcp server be installed',
|
|
1224
1224
|
},
|
|
1225
1225
|
success: (derivedTargets) => `You can now use the HubSpot CLI MCP Server in ${derivedTargets.join(', ')}. ${chalk.bold('You may need to restart these tools to apply the changes')}.`,
|
|
@@ -1261,7 +1261,7 @@ export const commands = {
|
|
|
1261
1261
|
},
|
|
1262
1262
|
prompts: {
|
|
1263
1263
|
targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
|
|
1264
|
-
targetsRequired: 'Must choose at least one
|
|
1264
|
+
targetsRequired: 'Must choose at least one app to configure.',
|
|
1265
1265
|
},
|
|
1266
1266
|
},
|
|
1267
1267
|
start: {
|
|
@@ -1385,6 +1385,9 @@ export const commands = {
|
|
|
1385
1385
|
examples: {
|
|
1386
1386
|
default: 'Start local dev for the current project',
|
|
1387
1387
|
},
|
|
1388
|
+
prompts: {
|
|
1389
|
+
selectProfile: '[--profile] Select a profile to use for local development',
|
|
1390
|
+
},
|
|
1388
1391
|
options: {
|
|
1389
1392
|
profile: 'The profile to target during local dev',
|
|
1390
1393
|
projectAccount: 'The id of the account to upload your project to. Must be used with --testing-account. Supported on platform versions 2025.2 and newer.',
|
|
@@ -1438,7 +1441,7 @@ export const commands = {
|
|
|
1438
1441
|
describe: 'How the app will be distributed.',
|
|
1439
1442
|
},
|
|
1440
1443
|
auth: {
|
|
1441
|
-
describe: 'Authentication model for the
|
|
1444
|
+
describe: 'Authentication model for the app.',
|
|
1442
1445
|
},
|
|
1443
1446
|
features: {
|
|
1444
1447
|
describe: `Features to include in the project. Only valid if project-base is ${PROJECT_WITH_APP}`,
|
|
@@ -1536,13 +1539,13 @@ export const commands = {
|
|
|
1536
1539
|
describe: "The path to the component type's location within the hubspot-project-components Github repo: https://github.com/HubSpot/hubspot-project-components",
|
|
1537
1540
|
},
|
|
1538
1541
|
distribution: {
|
|
1539
|
-
describe: 'The distribution method for the
|
|
1542
|
+
describe: 'The distribution method for the app.',
|
|
1540
1543
|
},
|
|
1541
1544
|
auth: {
|
|
1542
|
-
describe: 'The authentication type for the
|
|
1545
|
+
describe: 'The authentication type for the app.',
|
|
1543
1546
|
},
|
|
1544
1547
|
features: {
|
|
1545
|
-
describe: 'Which features to include with the
|
|
1548
|
+
describe: 'Which features to include with the app.',
|
|
1546
1549
|
},
|
|
1547
1550
|
},
|
|
1548
1551
|
creatingComponent: (projectName) => `Adding feature(s) to app [${chalk.bold(projectName)}]\n`,
|
|
@@ -2183,7 +2186,7 @@ export const commands = {
|
|
|
2183
2186
|
polling: {
|
|
2184
2187
|
start: (testAccountName) => `Creating test account "${chalk.bold(testAccountName)}"...`,
|
|
2185
2188
|
syncing: 'Test account created! Syncing account data... (may take a few minutes - you can exit and the sync will continue)',
|
|
2186
|
-
success: (testAccountName, testAccountId) => `Test account "${chalk.bold(testAccountName)}" successfully created with id
|
|
2189
|
+
success: (testAccountName, testAccountId, parentAccountId) => `Test account "${chalk.bold(testAccountName)}" successfully created with id ${chalk.bold(testAccountId)} under parent account ${uiAccountDescription(parentAccountId)}`,
|
|
2187
2190
|
createFailure: 'Failed to create test account.',
|
|
2188
2191
|
},
|
|
2189
2192
|
options: {
|
|
@@ -2206,7 +2209,7 @@ export const commands = {
|
|
|
2206
2209
|
},
|
|
2207
2210
|
createConfig: {
|
|
2208
2211
|
describe: 'Create a test account config file.',
|
|
2209
|
-
pathPrompt: '[--path] Enter the name of the
|
|
2212
|
+
pathPrompt: '[--path] Enter the name of the test account config file: ',
|
|
2210
2213
|
errors: {
|
|
2211
2214
|
pathError: 'Path is required',
|
|
2212
2215
|
pathFormatError: 'Path must end with .json',
|
|
@@ -2997,7 +3000,7 @@ export const lib = {
|
|
|
2997
3000
|
privateApp: `This project contains a private app. Local development of private apps is not supported in developer accounts. Change your default account using ${uiCommandReference('hs account use')}, or link a new account with ${uiAuthCommandReference()}.`,
|
|
2998
3001
|
},
|
|
2999
3002
|
validateAccountOption: {
|
|
3000
|
-
invalidPublicAppAccount: `This project contains a public app. The "--account" flag must point to a developer test account to develop this project locally. Alternatively, change your default account to an
|
|
3003
|
+
invalidPublicAppAccount: `This project contains a public app. The "--account" flag must point to a developer test account to develop this project locally. Alternatively, change your default account to an app developer account using ${uiCommandReference('hs account use')} and run ${uiCommandReference('hs project dev')} to set up a new developer test account.`,
|
|
3001
3004
|
invalidPrivateAppAccount: `This project contains a private app. The account specified with the "--account" flag points to a developer account, which do not support the local development of private apps. Update the "--account" flag to point to a standard, sandbox, or developer test account, or change your default account by running ${uiCommandReference('hs account use')}.`,
|
|
3002
3005
|
nonSandboxWarning: `Testing in a sandbox is strongly recommended. To switch the target account, select an option below or run ${uiCommandReference('hs account use')} before running the command again.`,
|
|
3003
3006
|
publicAppNonDeveloperTestAccountWarning: `Local development of public apps is only supported in ${chalk.bold('developer test accounts')}.`,
|
|
@@ -3059,7 +3062,7 @@ export const lib = {
|
|
|
3059
3062
|
prompt: {
|
|
3060
3063
|
marketPlaceDistribution: 'On the HubSpot marketplace',
|
|
3061
3064
|
privateDistribution: 'Privately',
|
|
3062
|
-
distribution: '[--distribution] Choose how to distribute your
|
|
3065
|
+
distribution: '[--distribution] Choose how to distribute your app:',
|
|
3063
3066
|
auth: '[--auth] Choose your authentication type:',
|
|
3064
3067
|
staticAuth: 'Static Auth',
|
|
3065
3068
|
oauth: 'OAuth',
|
|
@@ -3386,10 +3389,10 @@ export const lib = {
|
|
|
3386
3389
|
keepingCurrentDefault: (accountName) => `Account "${accountName}" will continue to be the default account`,
|
|
3387
3390
|
},
|
|
3388
3391
|
createDeveloperTestAccountConfigPrompt: {
|
|
3389
|
-
namePrompt: (withFlag = true) => `${withFlag ? '[--name] ' : ''}Enter the name of the
|
|
3390
|
-
descriptionPrompt: (withFlag = true) => `${withFlag ? '[--description] ' : ''}Enter the description of the
|
|
3392
|
+
namePrompt: (withFlag = true) => `${withFlag ? '[--name] ' : ''}Enter the name of the test account:`,
|
|
3393
|
+
descriptionPrompt: (withFlag = true) => `${withFlag ? '[--description] ' : ''}Enter the description of the test account:`,
|
|
3391
3394
|
useDefaultAccountLevelsPrompt: {
|
|
3392
|
-
message: 'Would you like to create a default
|
|
3395
|
+
message: 'Would you like to create a default test account, or customize your own?',
|
|
3393
3396
|
default: 'Default (All Hubs, ENTERPRISE)',
|
|
3394
3397
|
manual: 'Customize my own',
|
|
3395
3398
|
},
|
|
@@ -3405,7 +3408,7 @@ export const lib = {
|
|
|
3405
3408
|
errors: {
|
|
3406
3409
|
allHubsRequired: 'Select a tier for each hub',
|
|
3407
3410
|
tiersError: 'Cannot have more than one tier per hub',
|
|
3408
|
-
nameRequired: 'The name may not be blank. Please add a name for the
|
|
3411
|
+
nameRequired: 'The name may not be blank. Please add a name for the test account.',
|
|
3409
3412
|
},
|
|
3410
3413
|
},
|
|
3411
3414
|
accountNamePrompt: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { logError, debugError, ApiErrorContext, isErrorWithMessageOrReason, getErrorMessage, } from '../index.js';
|
|
2
|
+
import { uiLogger } from '../../ui/logger.js';
|
|
3
|
+
import { isHubSpotHttpError, isValidationError, } from '@hubspot/local-dev-lib/errors/index';
|
|
4
|
+
import { getConfig } from '@hubspot/local-dev-lib/config';
|
|
5
|
+
import { shouldSuppressError } from '../suppressError.js';
|
|
6
|
+
import { isProjectValidationError } from '../../errors/ProjectValidationError.js';
|
|
7
|
+
import { lib } from '../../../lang/en.js';
|
|
8
|
+
vi.mock('../../ui/logger.js');
|
|
9
|
+
vi.mock('@hubspot/local-dev-lib/errors/index');
|
|
10
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
11
|
+
vi.mock('../suppressError.js');
|
|
12
|
+
vi.mock('../../errors/ProjectValidationError.js');
|
|
13
|
+
describe('lib/errorHandlers/index', () => {
|
|
14
|
+
const uiLoggerErrorMock = uiLogger.error;
|
|
15
|
+
const uiLoggerDebugMock = uiLogger.debug;
|
|
16
|
+
const isHubSpotHttpErrorMock = isHubSpotHttpError;
|
|
17
|
+
const isValidationErrorMock = isValidationError;
|
|
18
|
+
const getConfigMock = getConfig;
|
|
19
|
+
const shouldSuppressErrorMock = shouldSuppressError;
|
|
20
|
+
const isProjectValidationErrorMock = isProjectValidationError;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
isHubSpotHttpErrorMock.mockReturnValue(false);
|
|
24
|
+
isValidationErrorMock.mockReturnValue(false);
|
|
25
|
+
shouldSuppressErrorMock.mockReturnValue(false);
|
|
26
|
+
isProjectValidationErrorMock.mockReturnValue(false);
|
|
27
|
+
getConfigMock.mockReturnValue({});
|
|
28
|
+
});
|
|
29
|
+
describe('logError', () => {
|
|
30
|
+
it('logs ProjectValidationError message and returns early', () => {
|
|
31
|
+
const error = {
|
|
32
|
+
message: 'Project validation failed',
|
|
33
|
+
name: 'ProjectValidationError',
|
|
34
|
+
};
|
|
35
|
+
isProjectValidationErrorMock.mockReturnValue(true);
|
|
36
|
+
logError(error);
|
|
37
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Project validation failed');
|
|
38
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
it('returns early when error should be suppressed', () => {
|
|
41
|
+
const error = new Error('Suppressed error');
|
|
42
|
+
shouldSuppressErrorMock.mockReturnValue(true);
|
|
43
|
+
logError(error);
|
|
44
|
+
expect(shouldSuppressErrorMock).toHaveBeenCalled();
|
|
45
|
+
expect(uiLoggerErrorMock).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
it('logs validation errors for HubSpotHttpError with validation errors', () => {
|
|
48
|
+
const mockError = {
|
|
49
|
+
formattedValidationErrors: vi
|
|
50
|
+
.fn()
|
|
51
|
+
.mockReturnValue('Formatted validation errors'),
|
|
52
|
+
updateContext: vi.fn(),
|
|
53
|
+
context: {},
|
|
54
|
+
};
|
|
55
|
+
isHubSpotHttpErrorMock.mockReturnValue(true);
|
|
56
|
+
isValidationErrorMock.mockReturnValue(true);
|
|
57
|
+
logError(mockError);
|
|
58
|
+
expect(mockError.formattedValidationErrors).toHaveBeenCalled();
|
|
59
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Formatted validation errors');
|
|
60
|
+
});
|
|
61
|
+
it('logs error message for errors with message property', () => {
|
|
62
|
+
const error = new Error('Something went wrong');
|
|
63
|
+
logError(error);
|
|
64
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Something went wrong');
|
|
65
|
+
});
|
|
66
|
+
it('logs error with both message and reason', () => {
|
|
67
|
+
const error = { message: 'Error message', reason: 'Error reason' };
|
|
68
|
+
logError(error);
|
|
69
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Error message Error reason');
|
|
70
|
+
});
|
|
71
|
+
it('logs unknown error message for errors without message or reason', () => {
|
|
72
|
+
const error = { foo: 'bar' };
|
|
73
|
+
logError(error);
|
|
74
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith(lib.errorHandlers.index.unknownErrorOccurred);
|
|
75
|
+
});
|
|
76
|
+
it('calls updateContext on HubSpotHttpError', () => {
|
|
77
|
+
const mockError = {
|
|
78
|
+
updateContext: vi.fn(),
|
|
79
|
+
context: {},
|
|
80
|
+
message: 'test',
|
|
81
|
+
};
|
|
82
|
+
isHubSpotHttpErrorMock.mockReturnValue(true);
|
|
83
|
+
logError(mockError);
|
|
84
|
+
expect(mockError.updateContext).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
describe('timeout error handling', () => {
|
|
87
|
+
it('shows config timeout message for direct ETIMEDOUT error matching default timeout', () => {
|
|
88
|
+
const mockError = {
|
|
89
|
+
code: 'ETIMEDOUT',
|
|
90
|
+
timeout: 15000,
|
|
91
|
+
updateContext: vi.fn(),
|
|
92
|
+
context: {},
|
|
93
|
+
message: 'Timeout',
|
|
94
|
+
};
|
|
95
|
+
isHubSpotHttpErrorMock.mockReturnValue(true);
|
|
96
|
+
getConfigMock.mockReturnValue({ httpTimeout: 15000 });
|
|
97
|
+
logError(mockError);
|
|
98
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledTimes(2);
|
|
99
|
+
expect(uiLoggerErrorMock).toHaveBeenNthCalledWith(1, 'Timeout');
|
|
100
|
+
expect(uiLoggerErrorMock).toHaveBeenNthCalledWith(2, lib.errorHandlers.index.configTimeoutErrorOccurred(15000, 'hs config set'));
|
|
101
|
+
});
|
|
102
|
+
it('shows generic timeout message for direct ETIMEDOUT error with custom timeout', () => {
|
|
103
|
+
const mockError = {
|
|
104
|
+
code: 'ETIMEDOUT',
|
|
105
|
+
timeout: 30000,
|
|
106
|
+
updateContext: vi.fn(),
|
|
107
|
+
context: {},
|
|
108
|
+
message: 'Timeout',
|
|
109
|
+
};
|
|
110
|
+
isHubSpotHttpErrorMock.mockReturnValue(true);
|
|
111
|
+
getConfigMock.mockReturnValue({ httpTimeout: 15000 });
|
|
112
|
+
logError(mockError);
|
|
113
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(uiLoggerErrorMock).toHaveBeenNthCalledWith(2, lib.errorHandlers.index.genericTimeoutErrorOccurred);
|
|
115
|
+
});
|
|
116
|
+
it('detects timeout error wrapped in error.cause', () => {
|
|
117
|
+
const causeError = {
|
|
118
|
+
code: 'ETIMEDOUT',
|
|
119
|
+
timeout: 15000,
|
|
120
|
+
name: 'HubSpotHttpError',
|
|
121
|
+
};
|
|
122
|
+
const wrapperError = new Error('Assets unavailable');
|
|
123
|
+
Object.defineProperty(wrapperError, 'cause', {
|
|
124
|
+
value: causeError,
|
|
125
|
+
writable: true,
|
|
126
|
+
});
|
|
127
|
+
isHubSpotHttpErrorMock.mockImplementation(err => {
|
|
128
|
+
return err === causeError;
|
|
129
|
+
});
|
|
130
|
+
getConfigMock.mockReturnValue({ httpTimeout: 15000 });
|
|
131
|
+
logError(wrapperError);
|
|
132
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Assets unavailable');
|
|
133
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith(lib.errorHandlers.index.configTimeoutErrorOccurred(15000, 'hs config set'));
|
|
134
|
+
});
|
|
135
|
+
it('shows generic timeout message for wrapped timeout with different timeout value', () => {
|
|
136
|
+
const causeError = {
|
|
137
|
+
code: 'ETIMEDOUT',
|
|
138
|
+
timeout: 60000,
|
|
139
|
+
name: 'HubSpotHttpError',
|
|
140
|
+
};
|
|
141
|
+
const wrapperError = new Error('Assets unavailable');
|
|
142
|
+
Object.defineProperty(wrapperError, 'cause', {
|
|
143
|
+
value: causeError,
|
|
144
|
+
writable: true,
|
|
145
|
+
});
|
|
146
|
+
isHubSpotHttpErrorMock.mockImplementation(err => {
|
|
147
|
+
return err === causeError;
|
|
148
|
+
});
|
|
149
|
+
getConfigMock.mockReturnValue({ httpTimeout: 15000 });
|
|
150
|
+
logError(wrapperError);
|
|
151
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(uiLoggerErrorMock).toHaveBeenNthCalledWith(2, lib.errorHandlers.index.genericTimeoutErrorOccurred);
|
|
153
|
+
});
|
|
154
|
+
it('does not show timeout message for non-timeout errors', () => {
|
|
155
|
+
const error = new Error('Regular error');
|
|
156
|
+
logError(error);
|
|
157
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(uiLoggerErrorMock).toHaveBeenCalledWith('Regular error');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('debugError', () => {
|
|
163
|
+
it('logs HubSpotHttpError using toString', () => {
|
|
164
|
+
const mockError = {
|
|
165
|
+
toString: vi.fn().mockReturnValue('HubSpotHttpError details'),
|
|
166
|
+
};
|
|
167
|
+
isHubSpotHttpErrorMock.mockReturnValue(true);
|
|
168
|
+
debugError(mockError);
|
|
169
|
+
expect(uiLoggerDebugMock).toHaveBeenCalledWith('HubSpotHttpError details');
|
|
170
|
+
});
|
|
171
|
+
it('logs regular error using lib.errorHandlers.index.errorOccurred', () => {
|
|
172
|
+
const error = new Error('Regular error');
|
|
173
|
+
debugError(error);
|
|
174
|
+
expect(uiLoggerDebugMock).toHaveBeenCalledWith(lib.errorHandlers.index.errorOccurred('Error: Regular error'));
|
|
175
|
+
});
|
|
176
|
+
it('logs error.cause when it is a HubSpotHttpError', () => {
|
|
177
|
+
const causeError = {
|
|
178
|
+
toString: vi.fn().mockReturnValue('Cause error details'),
|
|
179
|
+
};
|
|
180
|
+
const error = new Error('Wrapper error');
|
|
181
|
+
Object.defineProperty(error, 'cause', {
|
|
182
|
+
value: causeError,
|
|
183
|
+
writable: true,
|
|
184
|
+
});
|
|
185
|
+
isHubSpotHttpErrorMock.mockImplementation(err => {
|
|
186
|
+
return err === causeError;
|
|
187
|
+
});
|
|
188
|
+
debugError(error);
|
|
189
|
+
expect(causeError.toString).toHaveBeenCalled();
|
|
190
|
+
expect(uiLoggerDebugMock).toHaveBeenCalledWith('Cause error details');
|
|
191
|
+
});
|
|
192
|
+
it('logs error.cause using lib.errorHandlers.index.errorCause when not a HubSpotHttpError', () => {
|
|
193
|
+
const causeError = { customField: 'value' };
|
|
194
|
+
const error = new Error('Wrapper error');
|
|
195
|
+
Object.defineProperty(error, 'cause', {
|
|
196
|
+
value: causeError,
|
|
197
|
+
writable: true,
|
|
198
|
+
});
|
|
199
|
+
debugError(error);
|
|
200
|
+
expect(uiLoggerDebugMock).toHaveBeenCalledWith(expect.stringMatching(/^Cause:/));
|
|
201
|
+
});
|
|
202
|
+
it('logs context using lib.errorHandlers.index.errorContext when provided', () => {
|
|
203
|
+
const error = new Error('Error');
|
|
204
|
+
const context = new ApiErrorContext({
|
|
205
|
+
accountId: 123,
|
|
206
|
+
request: '/api/test',
|
|
207
|
+
});
|
|
208
|
+
debugError(error, context);
|
|
209
|
+
expect(uiLoggerDebugMock).toHaveBeenCalledWith(expect.stringMatching(/^Context:/));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('ApiErrorContext', () => {
|
|
213
|
+
it('creates context with all properties', () => {
|
|
214
|
+
const context = new ApiErrorContext({
|
|
215
|
+
accountId: 123,
|
|
216
|
+
request: '/api/test',
|
|
217
|
+
payload: '{"data": "value"}',
|
|
218
|
+
projectName: 'my-project',
|
|
219
|
+
});
|
|
220
|
+
expect(context.accountId).toBe(123);
|
|
221
|
+
expect(context.request).toBe('/api/test');
|
|
222
|
+
expect(context.payload).toBe('{"data": "value"}');
|
|
223
|
+
expect(context.projectName).toBe('my-project');
|
|
224
|
+
});
|
|
225
|
+
it('creates context with default values', () => {
|
|
226
|
+
const context = new ApiErrorContext();
|
|
227
|
+
expect(context.accountId).toBeUndefined();
|
|
228
|
+
expect(context.request).toBe('');
|
|
229
|
+
expect(context.payload).toBe('');
|
|
230
|
+
expect(context.projectName).toBe('');
|
|
231
|
+
});
|
|
232
|
+
it('creates context with partial properties', () => {
|
|
233
|
+
const context = new ApiErrorContext({
|
|
234
|
+
accountId: 456,
|
|
235
|
+
});
|
|
236
|
+
expect(context.accountId).toBe(456);
|
|
237
|
+
expect(context.request).toBe('');
|
|
238
|
+
expect(context.payload).toBe('');
|
|
239
|
+
expect(context.projectName).toBe('');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('isErrorWithMessageOrReason', () => {
|
|
243
|
+
it('returns true for object with message property', () => {
|
|
244
|
+
expect(isErrorWithMessageOrReason({ message: 'test' })).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
it('returns true for object with reason property', () => {
|
|
247
|
+
expect(isErrorWithMessageOrReason({ reason: 'test' })).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
it('returns true for object with both message and reason', () => {
|
|
250
|
+
expect(isErrorWithMessageOrReason({ message: 'msg', reason: 'rsn' })).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
it('returns true for Error instances', () => {
|
|
253
|
+
expect(isErrorWithMessageOrReason(new Error('test'))).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
it('returns false for null', () => {
|
|
256
|
+
expect(isErrorWithMessageOrReason(null)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
it('returns false for undefined', () => {
|
|
259
|
+
expect(isErrorWithMessageOrReason(undefined)).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
it('returns false for primitive values', () => {
|
|
262
|
+
expect(isErrorWithMessageOrReason('string')).toBe(false);
|
|
263
|
+
expect(isErrorWithMessageOrReason(123)).toBe(false);
|
|
264
|
+
expect(isErrorWithMessageOrReason(true)).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
it('returns false for object without message or reason', () => {
|
|
267
|
+
expect(isErrorWithMessageOrReason({ foo: 'bar' })).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
it('returns false for empty object', () => {
|
|
270
|
+
expect(isErrorWithMessageOrReason({})).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe('getErrorMessage', () => {
|
|
274
|
+
it('returns message from Error instance', () => {
|
|
275
|
+
expect(getErrorMessage(new Error('Error message'))).toBe('Error message');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -36,12 +36,21 @@ export function logError(error, context) {
|
|
|
36
36
|
// Unknown errors
|
|
37
37
|
uiLogger.error(lib.errorHandlers.index.unknownErrorOccurred);
|
|
38
38
|
}
|
|
39
|
+
let timeoutError = null;
|
|
39
40
|
if (isHubSpotHttpError(error) && error.code === 'ETIMEDOUT') {
|
|
41
|
+
timeoutError = error;
|
|
42
|
+
}
|
|
43
|
+
else if (error instanceof Error &&
|
|
44
|
+
isHubSpotHttpError(error.cause) &&
|
|
45
|
+
error.cause.code === 'ETIMEDOUT') {
|
|
46
|
+
timeoutError = error.cause;
|
|
47
|
+
}
|
|
48
|
+
if (timeoutError) {
|
|
40
49
|
const config = getConfig();
|
|
41
50
|
const defaultTimeout = config?.httpTimeout;
|
|
42
51
|
// Timeout was caused by the default timeout
|
|
43
|
-
if (
|
|
44
|
-
uiLogger.error(lib.errorHandlers.index.configTimeoutErrorOccurred(
|
|
52
|
+
if (timeoutError.timeout && defaultTimeout === timeoutError.timeout) {
|
|
53
|
+
uiLogger.error(lib.errorHandlers.index.configTimeoutErrorOccurred(timeoutError.timeout, 'hs config set'));
|
|
45
54
|
}
|
|
46
55
|
// Timeout was caused by a custom timeout set by the CLI or LDL
|
|
47
56
|
else {
|
|
@@ -190,7 +190,7 @@ export async function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMe
|
|
|
190
190
|
}
|
|
191
191
|
component.uid = uid;
|
|
192
192
|
if (component.type === AppKey && component.config) {
|
|
193
|
-
component.config.name = `${projectName}-
|
|
193
|
+
component.config.name = `${projectName}-App`;
|
|
194
194
|
}
|
|
195
195
|
fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
|
|
196
196
|
}
|
|
@@ -19,7 +19,7 @@ const inputSchema = {
|
|
|
19
19
|
z.literal(APP_DISTRIBUTION_TYPES.MARKETPLACE),
|
|
20
20
|
z.literal(APP_DISTRIBUTION_TYPES.PRIVATE),
|
|
21
21
|
]))
|
|
22
|
-
.describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your
|
|
22
|
+
.describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. '),
|
|
23
23
|
auth: z
|
|
24
24
|
.optional(z.union([
|
|
25
25
|
z.literal(APP_AUTH_TYPES.STATIC),
|
|
@@ -47,7 +47,7 @@ export class AddFeatureToProjectTool extends Tool {
|
|
|
47
47
|
command = addFlag(command, 'distribution', distribution);
|
|
48
48
|
}
|
|
49
49
|
else if (addApp) {
|
|
50
|
-
content.push(formatTextContent(`Ask the user how they would you like to distribute the
|
|
50
|
+
content.push(formatTextContent(`Ask the user how they would you like to distribute the app. Options are ${APP_DISTRIBUTION_TYPES.MARKETPLACE} and ${APP_DISTRIBUTION_TYPES.PRIVATE}`));
|
|
51
51
|
}
|
|
52
52
|
if (auth) {
|
|
53
53
|
command = addFlag(command, 'auth', auth);
|
|
@@ -25,7 +25,7 @@ const inputSchema = {
|
|
|
25
25
|
z.literal(APP_DISTRIBUTION_TYPES.MARKETPLACE),
|
|
26
26
|
z.literal(APP_DISTRIBUTION_TYPES.PRIVATE),
|
|
27
27
|
]))
|
|
28
|
-
.describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your
|
|
28
|
+
.describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. '),
|
|
29
29
|
auth: z
|
|
30
30
|
.optional(z.union([
|
|
31
31
|
z.literal(APP_AUTH_TYPES.STATIC),
|
|
@@ -63,7 +63,7 @@ export class CreateProjectTool extends Tool {
|
|
|
63
63
|
command = addFlag(command, 'distribution', distribution);
|
|
64
64
|
}
|
|
65
65
|
else if (projectBase === PROJECT_WITH_APP) {
|
|
66
|
-
content.push(formatTextContent(`Ask the user how they would you like to distribute the
|
|
66
|
+
content.push(formatTextContent(`Ask the user how they would you like to distribute the app? Options are ${APP_DISTRIBUTION_TYPES.MARKETPLACE} and ${APP_DISTRIBUTION_TYPES.PRIVATE}`));
|
|
67
67
|
}
|
|
68
68
|
if (auth) {
|
|
69
69
|
command = addFlag(command, 'auth', auth);
|
|
@@ -13,7 +13,7 @@ const inputSchema = {
|
|
|
13
13
|
appId: z
|
|
14
14
|
.string()
|
|
15
15
|
.regex(/^\d+$/, 'App ID must be a numeric string')
|
|
16
|
-
.describe('The numeric app ID as a string (e.g., "3003909"). Use get-
|
|
16
|
+
.describe('The numeric app ID as a string (e.g., "3003909"). Use get-apps-info to find available app IDs.'),
|
|
17
17
|
startDate: z
|
|
18
18
|
.string()
|
|
19
19
|
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be in YYYY-MM-DD format')
|
|
@@ -65,7 +65,7 @@ export class GetApiUsagePatternsByAppIdTool extends Tool {
|
|
|
65
65
|
register() {
|
|
66
66
|
return this.mcpServer.registerTool(toolName, {
|
|
67
67
|
title: 'Get API Usage Patterns by App ID',
|
|
68
|
-
description: 'Retrieves detailed API usage pattern analytics for a specific HubSpot
|
|
68
|
+
description: 'Retrieves detailed API usage pattern analytics for a specific HubSpot app. Requires an appId (string) to identify the target app. Optionally accepts startDate and endDate parameters in YYYY-MM-DD format to filter results within a specific time range. Returns patternSummaries object containing usage statistics including portalPercentage (percentage of portals using this pattern) and numOfPortals (total count of portals) for different usage patterns. This data helps analyze how the app is being used across different HubSpot portals and can inform optimization decisions.',
|
|
69
69
|
inputSchema,
|
|
70
70
|
annotations: {
|
|
71
71
|
readOnlyHint: true,
|
|
@@ -11,7 +11,7 @@ import { getErrorMessage } from '../../../lib/errorHandlers/index.js';
|
|
|
11
11
|
const inputSchema = { absoluteCurrentWorkingDirectory };
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13
13
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
14
|
-
const toolName = 'get-
|
|
14
|
+
const toolName = 'get-apps-info';
|
|
15
15
|
export class GetApplicationInfoTool extends Tool {
|
|
16
16
|
constructor(mcpServer) {
|
|
17
17
|
super(mcpServer);
|
|
@@ -44,8 +44,8 @@ export class GetApplicationInfoTool extends Tool {
|
|
|
44
44
|
}
|
|
45
45
|
register() {
|
|
46
46
|
return this.mcpServer.registerTool(toolName, {
|
|
47
|
-
title: 'Get
|
|
48
|
-
description: 'Retrieves a list of all HubSpot
|
|
47
|
+
title: 'Get Apps Information',
|
|
48
|
+
description: 'Retrieves a list of all HubSpot apps available in the current account. Returns an array of apps, where each app contains an appId (numeric identifier) and appName (string). This information is useful for identifying available apps before using other tools that require specific app IDs, such as getting API usage patterns. No input parameters are required - this tool fetches all apps from the HubSpot Insights API.',
|
|
49
49
|
inputSchema,
|
|
50
50
|
annotations: {
|
|
51
51
|
readOnlyHint: true,
|
|
@@ -62,12 +62,15 @@ export class UploadProjectTools extends Tool {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
const { stdout, stderr } = await runCommandInDir(absoluteProjectPath, command);
|
|
65
|
-
|
|
65
|
+
const response = await formatTextContents(stdout, stderr);
|
|
66
|
+
// Add reminder about cards needing to be added to views
|
|
67
|
+
response.content.push(formatTextContent('\nIMPORTANT: If this project contains cards, remember that uploading does NOT make them live automatically. Cards must be manually added to a view in HubSpot to become visible to users.'));
|
|
68
|
+
return response;
|
|
66
69
|
}
|
|
67
70
|
register() {
|
|
68
71
|
return this.mcpServer.registerTool(toolName, {
|
|
69
72
|
title: 'Upload HubSpot Project',
|
|
70
|
-
description: 'DO NOT run this tool unless the user specifies they would like to upload the project, it is potentially destructive. Uploads the HubSpot project in current working directory. If the project does not exist, it will be created. MUST be ran from within the project directory.',
|
|
73
|
+
description: 'DO NOT run this tool unless the user specifies they would like to upload the project, it is potentially destructive. Uploads the HubSpot project in current working directory. If the project does not exist, it will be created. MUST be ran from within the project directory. IMPORTANT: Uploading a project does NOT automatically make cards live or visible to users. Cards must be manually added to a view in HubSpot after upload to become visible.',
|
|
71
74
|
inputSchema,
|
|
72
75
|
annotations: {
|
|
73
76
|
readOnlyHint: false,
|
|
@@ -87,7 +87,7 @@ describe('mcp-server/tools/project/AddFeatureToProject', () => {
|
|
|
87
87
|
expect(result.content).toEqual([
|
|
88
88
|
{
|
|
89
89
|
type: 'text',
|
|
90
|
-
text: expect.stringContaining('Ask the user how they would you like to distribute the
|
|
90
|
+
text: expect.stringContaining('Ask the user how they would you like to distribute the app'),
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
type: 'text',
|
|
@@ -85,7 +85,7 @@ describe('mcp-server/tools/project/CreateProjectTool', () => {
|
|
|
85
85
|
expect(result.content).toEqual([
|
|
86
86
|
{
|
|
87
87
|
type: 'text',
|
|
88
|
-
text: expect.stringContaining('Ask the user how they would you like to distribute the
|
|
88
|
+
text: expect.stringContaining('Ask the user how they would you like to distribute the app?'),
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
type: 'text',
|
|
@@ -36,7 +36,7 @@ describe('mcp-server/tools/project/GetApiUsagePatternsByAppIdTool', () => {
|
|
|
36
36
|
const result = tool.register();
|
|
37
37
|
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-api-usage-patterns-by-app-id', expect.objectContaining({
|
|
38
38
|
title: 'Get API Usage Patterns by App ID',
|
|
39
|
-
description: expect.stringContaining('Retrieves detailed API usage pattern analytics for a specific HubSpot
|
|
39
|
+
description: expect.stringContaining('Retrieves detailed API usage pattern analytics for a specific HubSpot app'),
|
|
40
40
|
inputSchema: expect.objectContaining({
|
|
41
41
|
appId: expect.objectContaining({
|
|
42
42
|
describe: expect.any(Function),
|
|
@@ -34,9 +34,9 @@ describe('mcp-server/tools/project/GetApplicationInfoTool', () => {
|
|
|
34
34
|
describe('register', () => {
|
|
35
35
|
it('should register tool with correct parameters', () => {
|
|
36
36
|
const result = tool.register();
|
|
37
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-
|
|
38
|
-
title: 'Get
|
|
39
|
-
description: expect.stringContaining('Retrieves a list of all HubSpot
|
|
37
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-apps-info', expect.objectContaining({
|
|
38
|
+
title: 'Get Apps Information',
|
|
39
|
+
description: expect.stringContaining('Retrieves a list of all HubSpot apps available in the current account'),
|
|
40
40
|
inputSchema: expect.any(Object),
|
|
41
41
|
}), expect.any(Function));
|
|
42
42
|
expect(result).toBe(mockRegisteredTool);
|
|
@@ -69,6 +69,10 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
69
69
|
content: [
|
|
70
70
|
{ type: 'text', text: 'Project uploaded successfully' },
|
|
71
71
|
{ type: 'text', text: '' },
|
|
72
|
+
{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: '\nIMPORTANT: If this project contains cards, remember that uploading does NOT make them live automatically. Cards must be manually added to a view in HubSpot to become visible to users.',
|
|
75
|
+
},
|
|
72
76
|
],
|
|
73
77
|
});
|
|
74
78
|
});
|
|
@@ -81,6 +85,10 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
81
85
|
expect(result.content).toEqual([
|
|
82
86
|
{ type: 'text', text: 'Project uploaded with warnings' },
|
|
83
87
|
{ type: 'text', text: 'Warning: some files were ignored' },
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: '\nIMPORTANT: If this project contains cards, remember that uploading does NOT make them live automatically. Cards must be manually added to a view in HubSpot to become visible to users.',
|
|
91
|
+
},
|
|
84
92
|
]);
|
|
85
93
|
});
|
|
86
94
|
it('should handle upload errors', async () => {
|
|
@@ -136,6 +144,10 @@ describe('mcp-server/tools/project/UploadProjectTools', () => {
|
|
|
136
144
|
expect(result.content).toEqual([
|
|
137
145
|
{ type: 'text', text: '' },
|
|
138
146
|
{ type: 'text', text: '' },
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: '\nIMPORTANT: If this project contains cards, remember that uploading does NOT make them live automatically. Cards must be manually added to a view in HubSpot to become visible to users.',
|
|
150
|
+
},
|
|
139
151
|
]);
|
|
140
152
|
});
|
|
141
153
|
it('should work with different project paths', async () => {
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "8.0.0-beta.
|
|
3
|
+
"version": "8.0.0-beta.1",
|
|
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/cms-dev-server": "1.2.1",
|
|
10
|
-
"@hubspot/local-dev-lib": "5.1.
|
|
11
|
-
"@hubspot/project-parsing-lib": "0.11.
|
|
10
|
+
"@hubspot/local-dev-lib": "5.1.1",
|
|
11
|
+
"@hubspot/project-parsing-lib": "0.11.2",
|
|
12
12
|
"@hubspot/serverless-dev-runtime": "7.0.7",
|
|
13
13
|
"@hubspot/ui-extensions-dev-server": "1.1.3",
|
|
14
14
|
"@inquirer/prompts": "7.1.0",
|
package/types/Cms.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare const TEMPLATE_TYPES: readonly ["page-template", "email-template"
|
|
|
3
3
|
export type TemplateType = (typeof TEMPLATE_TYPES)[number];
|
|
4
4
|
export declare const HTTP_METHODS: readonly ["DELETE", "GET", "PATCH", "POST", "PUT"];
|
|
5
5
|
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
6
|
-
export declare const CONTENT_TYPES: readonly ["ANY", "LANDING_PAGE", "SITE_PAGE", "BLOG_POST", "BLOG_LISTING", "EMAIL", "KNOWLEDGE_BASE", "QUOTE_TEMPLATE", "CUSTOMER_PORTAL", "WEB_INTERACTIVE", "SUBSCRIPTION", "MEMBERSHIP"];
|
|
6
|
+
export declare const CONTENT_TYPES: readonly ["ANY", "LANDING_PAGE", "SITE_PAGE", "BLOG_POST", "BLOG_LISTING", "EMAIL", "KNOWLEDGE_BASE", "QUOTE_TEMPLATE", "QUOTE", "QUOTE_BLUEPRINT", "CUSTOMER_PORTAL", "WEB_INTERACTIVE", "SUBSCRIPTION", "MEMBERSHIP"];
|
|
7
7
|
export type ContentType = (typeof CONTENT_TYPES)[number];
|
|
8
8
|
export type CreateArgs = CommonArgs & ConfigArgs & {
|
|
9
9
|
branch?: string;
|