@hubspot/cli 7.9.0 → 7.10.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.
Files changed (82) hide show
  1. package/commands/__tests__/project.test.js +2 -0
  2. package/commands/account/__tests__/rename.test.js +35 -0
  3. package/commands/account/createOverride.js +2 -12
  4. package/commands/account/removeOverride.js +2 -10
  5. package/commands/account/rename.d.ts +1 -1
  6. package/commands/account/rename.js +5 -2
  7. package/commands/cms/theme/preview.js +1 -4
  8. package/commands/config/set.js +1 -2
  9. package/commands/getStarted.js +13 -19
  10. package/commands/hubdb.d.ts +1 -1
  11. package/commands/project/__tests__/updateDeps.test.d.ts +1 -0
  12. package/commands/project/__tests__/updateDeps.test.js +142 -0
  13. package/commands/project/create.js +0 -1
  14. package/commands/project/dev/index.js +8 -1
  15. package/commands/project/listBuilds.js +7 -1
  16. package/commands/project/updateDeps.d.ts +6 -0
  17. package/commands/project/updateDeps.js +80 -0
  18. package/commands/project/upload.js +7 -1
  19. package/commands/project/validate.js +7 -1
  20. package/commands/project/watch.js +7 -2
  21. package/commands/project.js +2 -0
  22. package/commands/testAccount/__tests__/create.test.js +68 -0
  23. package/commands/testAccount/create.d.ts +8 -0
  24. package/commands/testAccount/create.js +134 -44
  25. package/commands/testAccount/importData.d.ts +1 -1
  26. package/lang/en.d.ts +3194 -3184
  27. package/lang/en.js +43 -8
  28. package/lib/__tests__/dependencyManagement.test.js +273 -1
  29. package/lib/commonOpts.js +2 -5
  30. package/lib/constants.d.ts +1 -0
  31. package/lib/constants.js +6 -0
  32. package/lib/dependencyManagement.d.ts +8 -2
  33. package/lib/dependencyManagement.js +75 -12
  34. package/lib/mcp/__tests__/setup.test.d.ts +1 -0
  35. package/lib/mcp/__tests__/setup.test.js +127 -0
  36. package/lib/mcp/setup.d.ts +4 -12
  37. package/lib/mcp/setup.js +34 -1
  38. package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
  39. package/lib/middleware/autoUpdateMiddleware.js +1 -0
  40. package/lib/npm.d.ts +3 -0
  41. package/lib/npm.js +6 -0
  42. package/lib/projects/__tests__/components.test.js +148 -24
  43. package/lib/projects/__tests__/platformVersion.test.js +5 -1
  44. package/lib/projects/__tests__/projects.test.js +13 -42
  45. package/lib/projects/components.js +76 -20
  46. package/lib/projects/config.js +5 -9
  47. package/lib/projects/platformVersion.js +1 -1
  48. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
  49. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
  50. package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
  51. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
  52. package/mcp-server/tools/cms/HsCreateFunctionTool.js +6 -0
  53. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
  54. package/mcp-server/tools/cms/HsCreateModuleTool.js +6 -0
  55. package/mcp-server/tools/cms/HsCreateTemplateTool.js +6 -0
  56. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
  57. package/mcp-server/tools/cms/HsFunctionLogsTool.js +4 -0
  58. package/mcp-server/tools/cms/HsListFunctionsTool.js +4 -0
  59. package/mcp-server/tools/cms/HsListTool.js +4 -0
  60. package/mcp-server/tools/index.js +2 -0
  61. package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -0
  62. package/mcp-server/tools/project/CreateProjectTool.js +6 -0
  63. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
  64. package/mcp-server/tools/project/CreateTestAccountTool.js +137 -0
  65. package/mcp-server/tools/project/DeployProjectTool.js +6 -0
  66. package/mcp-server/tools/project/DocFetchTool.js +4 -0
  67. package/mcp-server/tools/project/DocsSearchTool.js +4 -0
  68. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +4 -0
  69. package/mcp-server/tools/project/GetApplicationInfoTool.js +4 -0
  70. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -0
  71. package/mcp-server/tools/project/GuidedWalkthroughTool.js +4 -0
  72. package/mcp-server/tools/project/UploadProjectTools.d.ts +9 -3
  73. package/mcp-server/tools/project/UploadProjectTools.js +50 -4
  74. package/mcp-server/tools/project/ValidateProjectTool.js +4 -0
  75. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
  76. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +231 -0
  77. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
  78. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
  79. package/package.json +2 -2
  80. package/lang/en.lyaml +0 -1508
  81. package/lib/lang.d.ts +0 -8
  82. package/lib/lang.js +0 -72
@@ -1,9 +1,51 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { coerceToValidUid, metafileExtension, } from '@hubspot/project-parsing-lib';
4
+ import { fileExists } from '../validation.js';
4
5
  import { uiLogger } from '../ui/logger.js';
5
6
  import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
6
7
  import { lib } from '../../lang/en.js';
8
+ import { debugError } from '../errorHandlers/index.js';
9
+ // Prefix for the metafile extension
10
+ const metafileExtensionPrefix = path.parse(metafileExtension).name;
11
+ function applyDifferentiatorToFilename(filename, differentiator, isHsMetaFile) {
12
+ const { name, ext, dir } = path.parse(filename);
13
+ if (isHsMetaFile) {
14
+ return path.join(dir, `${name.replace(metafileExtensionPrefix, '')}-${differentiator}${metafileExtension}`);
15
+ }
16
+ return path.join(dir, `${name}-${differentiator}${ext}`);
17
+ }
18
+ // Generates safe filename differentiators, avoiding collisions with existing filenames
19
+ // E.x. "NewCard.tsx" -> "NewCard-1.tsx"
20
+ function generateSafeFilenameDifferentiator(sourceFiles, hsMetaFiles) {
21
+ let differentiator = 1;
22
+ let isDifferentiatorUnique = false;
23
+ let maxAttempts = 10;
24
+ while (!isDifferentiatorUnique) {
25
+ differentiator++;
26
+ maxAttempts--;
27
+ try {
28
+ const isDifferentiatorUniqueForSourceFiles = sourceFiles.every(file => {
29
+ return !fileExists(applyDifferentiatorToFilename(file, differentiator, false));
30
+ });
31
+ const isDifferentiatorUniqueForHsMetaFiles = hsMetaFiles.every(file => {
32
+ return !fileExists(applyDifferentiatorToFilename(file, differentiator, true));
33
+ });
34
+ isDifferentiatorUnique =
35
+ isDifferentiatorUniqueForSourceFiles &&
36
+ isDifferentiatorUniqueForHsMetaFiles;
37
+ }
38
+ catch (error) {
39
+ uiLogger.debug(lib.projects.generateSafeFilenameDifferentiator.failedToCheckFiles);
40
+ maxAttempts = 0;
41
+ }
42
+ // If we've tried too many times, just use a timestamp
43
+ if (maxAttempts <= 0) {
44
+ return Date.now();
45
+ }
46
+ }
47
+ return differentiator;
48
+ }
7
49
  // Handles a collision between component source files
8
50
  export function handleComponentCollision({ dest, src, collisions }) {
9
51
  const hsMetaFiles = [];
@@ -20,19 +62,20 @@ export function handleComponentCollision({ dest, src, collisions }) {
20
62
  sourceFiles.push(collision);
21
63
  }
22
64
  });
23
- const sourceFilenameMapping = sourceFiles.reduce((acc, filename) => {
24
- const { name, ext, dir } = path.parse(filename);
65
+ const filenameDifferentiator = generateSafeFilenameDifferentiator(sourceFiles, hsMetaFiles);
66
+ // Exclude markdown files fromthe rename process because they should not be duplicated
67
+ const sourceFilenameMapping = sourceFiles
68
+ .filter(filename => !filename.endsWith('.md'))
69
+ .reduce((acc, filename) => {
25
70
  return {
26
71
  ...acc,
27
- [filename]: path.join(dir, `${name}-${Date.now()}${ext}`),
72
+ [filename]: applyDifferentiatorToFilename(filename, filenameDifferentiator, false),
28
73
  };
29
74
  }, {});
30
- const metafileExtensionPrefix = path.parse(metafileExtension).name;
31
75
  const metaFilenameMapping = hsMetaFiles.reduce((acc, filename) => {
32
- const { name, dir } = path.parse(filename);
33
76
  return {
34
77
  ...acc,
35
- [filename]: path.join(dir, `${name.replace(metafileExtensionPrefix, '')}-${Date.now()}${metafileExtension}`),
78
+ [filename]: applyDifferentiatorToFilename(filename, filenameDifferentiator, true),
36
79
  };
37
80
  }, {});
38
81
  // Update the metafiles that might contain references to the old filenames
@@ -81,22 +124,35 @@ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFile
81
124
  uiLogger.log('');
82
125
  uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.header);
83
126
  for (const hsMetaFile of hsMetaFilePaths) {
84
- const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
85
- let uid = coerceToValidUid(`${component.type}-${projectName}`) || component.uid;
86
- if (existingUids.includes(uid)) {
87
- uid =
88
- coerceToValidUid(`${component.type}-${Date.now()}-${projectName}`) ||
89
- component.uid;
127
+ try {
128
+ const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
129
+ const getBaseUid = () => {
130
+ const customUid = coerceToValidUid(`${projectName}_${component.type}`);
131
+ if (customUid) {
132
+ return customUid.replace(/-/g, '_');
133
+ }
134
+ return component.uid;
135
+ };
136
+ let uid = getBaseUid();
137
+ let differentiator = 1;
138
+ while (existingUids.includes(uid)) {
139
+ differentiator++;
140
+ uid = `${getBaseUid()}-${differentiator}`;
141
+ }
142
+ component.uid = uid;
143
+ if (component.type === AppKey && component.config) {
144
+ component.config.name = `${projectName}-Application`;
145
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
146
+ }
147
+ else {
148
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
149
+ }
150
+ fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
90
151
  }
91
- component.uid = uid;
92
- if (component.type === AppKey && component.config) {
93
- component.config.name = `${projectName}-Application`;
94
- uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
95
- }
96
- else {
97
- uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
152
+ catch (error) {
153
+ debugError(error);
154
+ uiLogger.error(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.failedToUpdate(hsMetaFile));
98
155
  }
99
- fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
100
156
  }
101
157
  uiLogger.log('');
102
158
  }
@@ -4,8 +4,8 @@ import findup from 'findup-sync';
4
4
  import { getAbsoluteFilePath, getCwd } from '@hubspot/local-dev-lib/path';
5
5
  import { PROJECT_CONFIG_FILE } from '../constants.js';
6
6
  import { lib } from '../../lang/en.js';
7
- import { EXIT_CODES } from '../enums/exitCodes.js';
8
7
  import { uiLogger } from '../ui/logger.js';
8
+ import ProjectValidationError from '../errors/ProjectValidationError.js';
9
9
  export function writeProjectConfig(configPath, config) {
10
10
  try {
11
11
  fs.ensureFileSync(configPath);
@@ -50,21 +50,17 @@ export async function getProjectConfig(dir) {
50
50
  }
51
51
  export function validateProjectConfig(projectConfig, projectDir) {
52
52
  if (!projectConfig || !projectDir) {
53
- uiLogger.error(lib.projects.validateProjectConfig.configNotFound);
54
- return process.exit(EXIT_CODES.ERROR);
53
+ throw new ProjectValidationError(lib.projects.validateProjectConfig.configNotFound);
55
54
  }
56
55
  if (!projectConfig.name || !projectConfig.srcDir) {
57
- uiLogger.error(lib.projects.validateProjectConfig.configMissingFields);
58
- return process.exit(EXIT_CODES.ERROR);
56
+ throw new ProjectValidationError(lib.projects.validateProjectConfig.configMissingFields);
59
57
  }
60
58
  const resolvedPath = path.resolve(projectDir, projectConfig.srcDir);
61
59
  if (!resolvedPath.startsWith(projectDir)) {
62
60
  const projectConfigFile = path.relative('.', path.join(projectDir, PROJECT_CONFIG_FILE));
63
- uiLogger.error(lib.projects.validateProjectConfig.srcOutsideProjectDir(projectConfigFile, projectConfig.srcDir));
64
- return process.exit(EXIT_CODES.ERROR);
61
+ throw new ProjectValidationError(lib.projects.validateProjectConfig.srcOutsideProjectDir(projectConfigFile, projectConfig.srcDir));
65
62
  }
66
63
  if (!fs.existsSync(resolvedPath)) {
67
- uiLogger.error(lib.projects.validateProjectConfig.srcDirNotFound(projectConfig.srcDir, projectDir));
68
- return process.exit(EXIT_CODES.ERROR);
64
+ throw new ProjectValidationError(lib.projects.validateProjectConfig.srcDirNotFound(projectConfig.srcDir, projectDir));
69
65
  }
70
66
  }
@@ -5,6 +5,6 @@ export function isV2Project(platformVersion) {
5
5
  if (platformVersion.toLowerCase() === 'unstable') {
6
6
  return true;
7
7
  }
8
- const [year, minor] = platformVersion.split('.');
8
+ const [year, minor] = platformVersion.split(/[.-]/);
9
9
  return Number(year) >= 2025 && Number(minor) >= 2;
10
10
  }
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createDeveloperTestAccountConfigPrompt } from '../createDeveloperTestAccountConfigPrompt.js';
3
+ import * as promptUtils from '../promptUtils.js';
4
+ vi.mock('../promptUtils.js');
5
+ describe('createDeveloperTestAccountConfigPrompt', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ describe('with name and description provided via args', () => {
10
+ it('should skip name and description prompts when provided', async () => {
11
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
12
+ mockPromptUser.mockResolvedValueOnce({}); // name/description prompts skipped
13
+ mockPromptUser.mockResolvedValueOnce({
14
+ useDefaultAccountLevels: 'default',
15
+ }); // tier selection
16
+ const result = await createDeveloperTestAccountConfigPrompt({
17
+ name: 'TestAccount',
18
+ description: 'Test description',
19
+ });
20
+ expect(result).toEqual({
21
+ accountName: 'TestAccount',
22
+ description: 'Test description',
23
+ marketingLevel: 'ENTERPRISE',
24
+ opsLevel: 'ENTERPRISE',
25
+ serviceLevel: 'ENTERPRISE',
26
+ salesLevel: 'ENTERPRISE',
27
+ contentLevel: 'ENTERPRISE',
28
+ });
29
+ expect(mockPromptUser).toHaveBeenCalledTimes(2);
30
+ });
31
+ });
32
+ describe('with tier flags provided', () => {
33
+ it('should skip tier prompts and use provided values with defaults', async () => {
34
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
35
+ mockPromptUser.mockResolvedValueOnce({}); // name/description prompts skipped
36
+ const result = await createDeveloperTestAccountConfigPrompt({
37
+ name: 'TestAccount',
38
+ description: 'Test',
39
+ marketingLevel: 'PROFESSIONAL',
40
+ salesLevel: 'STARTER',
41
+ });
42
+ expect(result).toEqual({
43
+ accountName: 'TestAccount',
44
+ description: 'Test',
45
+ marketingLevel: 'PROFESSIONAL',
46
+ opsLevel: 'ENTERPRISE',
47
+ serviceLevel: 'ENTERPRISE',
48
+ salesLevel: 'STARTER',
49
+ contentLevel: 'ENTERPRISE',
50
+ });
51
+ // Should only call promptUser once (for name/description which are skipped)
52
+ expect(mockPromptUser).toHaveBeenCalledTimes(1);
53
+ });
54
+ it('should default unprovided tiers to ENTERPRISE', async () => {
55
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
56
+ mockPromptUser.mockResolvedValueOnce({
57
+ description: 'Test',
58
+ }); // description prompt (name provided via args, description not provided)
59
+ const result = await createDeveloperTestAccountConfigPrompt({
60
+ name: 'TestAccount',
61
+ contentLevel: 'FREE',
62
+ });
63
+ expect(result).toEqual({
64
+ accountName: 'TestAccount',
65
+ description: 'Test',
66
+ marketingLevel: 'ENTERPRISE',
67
+ opsLevel: 'ENTERPRISE',
68
+ serviceLevel: 'ENTERPRISE',
69
+ salesLevel: 'ENTERPRISE',
70
+ contentLevel: 'FREE',
71
+ });
72
+ });
73
+ });
74
+ describe('with no flags provided', () => {
75
+ it('should prompt for name, description, and tier selection', async () => {
76
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
77
+ // First call: name/description prompts
78
+ mockPromptUser.mockResolvedValueOnce({
79
+ accountName: 'PromptedAccount',
80
+ description: 'Prompted description',
81
+ });
82
+ // Second call: tier selection
83
+ mockPromptUser.mockResolvedValueOnce({
84
+ useDefaultAccountLevels: 'default',
85
+ });
86
+ const result = await createDeveloperTestAccountConfigPrompt({});
87
+ expect(result).toEqual({
88
+ accountName: 'PromptedAccount',
89
+ description: 'Prompted description',
90
+ marketingLevel: 'ENTERPRISE',
91
+ opsLevel: 'ENTERPRISE',
92
+ serviceLevel: 'ENTERPRISE',
93
+ salesLevel: 'ENTERPRISE',
94
+ contentLevel: 'ENTERPRISE',
95
+ });
96
+ expect(mockPromptUser).toHaveBeenCalledTimes(2);
97
+ });
98
+ it('should allow manual tier selection', async () => {
99
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
100
+ mockPromptUser.mockResolvedValueOnce({
101
+ accountName: 'TestAccount',
102
+ description: 'Test',
103
+ }); // name/description
104
+ mockPromptUser.mockResolvedValueOnce({
105
+ useDefaultAccountLevels: 'manual',
106
+ }); // tier choice
107
+ mockPromptUser.mockResolvedValueOnce({
108
+ testAccountLevels: [
109
+ { hub: 'MARKETING', tier: 'PROFESSIONAL' },
110
+ { hub: 'OPS', tier: 'STARTER' },
111
+ { hub: 'SERVICE', tier: 'ENTERPRISE' },
112
+ { hub: 'SALES', tier: 'FREE' },
113
+ { hub: 'CONTENT', tier: 'ENTERPRISE' },
114
+ ],
115
+ }); // manual tier selection
116
+ const result = await createDeveloperTestAccountConfigPrompt({});
117
+ expect(result).toEqual({
118
+ accountName: 'TestAccount',
119
+ description: 'Test',
120
+ marketingLevel: 'PROFESSIONAL',
121
+ opsLevel: 'STARTER',
122
+ serviceLevel: 'ENTERPRISE',
123
+ salesLevel: 'FREE',
124
+ contentLevel: 'ENTERPRISE',
125
+ });
126
+ expect(mockPromptUser).toHaveBeenCalledTimes(3);
127
+ });
128
+ });
129
+ describe('with only name provided', () => {
130
+ it('should skip name prompt but show description and tier prompts', async () => {
131
+ const mockPromptUser = vi.mocked(promptUtils.promptUser);
132
+ mockPromptUser.mockResolvedValueOnce({
133
+ description: 'Prompted description',
134
+ }); // description prompt (name skipped)
135
+ mockPromptUser.mockResolvedValueOnce({
136
+ useDefaultAccountLevels: 'default',
137
+ }); // tier selection
138
+ const result = await createDeveloperTestAccountConfigPrompt({
139
+ name: 'TestAccount',
140
+ });
141
+ expect(result).toEqual({
142
+ accountName: 'TestAccount',
143
+ description: 'Prompted description',
144
+ marketingLevel: 'ENTERPRISE',
145
+ opsLevel: 'ENTERPRISE',
146
+ serviceLevel: 'ENTERPRISE',
147
+ salesLevel: 'ENTERPRISE',
148
+ contentLevel: 'ENTERPRISE',
149
+ });
150
+ expect(mockPromptUser).toHaveBeenCalledTimes(2);
151
+ });
152
+ });
153
+ });
@@ -14,5 +14,10 @@ export type HubConfig = {
14
14
  export declare function createDeveloperTestAccountConfigPrompt(args?: {
15
15
  name?: string;
16
16
  description?: string;
17
+ marketingLevel?: AccountLevel;
18
+ opsLevel?: AccountLevel;
19
+ serviceLevel?: AccountLevel;
20
+ salesLevel?: AccountLevel;
21
+ contentLevel?: AccountLevel;
17
22
  }, supportFlags?: boolean): Promise<DeveloperTestAccountConfig>;
18
23
  export {};
@@ -49,15 +49,17 @@ const TEST_ACCOUNT_TIERS = [
49
49
  new Separator(),
50
50
  ];
51
51
  export async function createDeveloperTestAccountConfigPrompt(args = {}, supportFlags = true) {
52
- const { name, description } = args;
53
- let accountName = name;
54
- let accountDescription = description;
55
- let accountLevelsArray = [];
56
- if (!accountName) {
57
- const namePromptResult = await promptUser({
52
+ const hasAnyTierLevels = !!(args.marketingLevel ||
53
+ args.opsLevel ||
54
+ args.serviceLevel ||
55
+ args.salesLevel ||
56
+ args.contentLevel);
57
+ const result = await promptUser([
58
+ {
58
59
  name: 'accountName',
59
60
  message: lib.prompts.createDeveloperTestAccountConfigPrompt.namePrompt(supportFlags),
60
61
  type: 'input',
62
+ when: !args.name,
61
63
  validate: value => {
62
64
  if (!value) {
63
65
  return lib.prompts.createDeveloperTestAccountConfigPrompt.errors
@@ -65,58 +67,67 @@ export async function createDeveloperTestAccountConfigPrompt(args = {}, supportF
65
67
  }
66
68
  return true;
67
69
  },
68
- });
69
- accountName = namePromptResult.accountName;
70
- }
71
- if (!accountDescription) {
72
- const descriptionPromptResult = await promptUser({
70
+ },
71
+ {
73
72
  name: 'description',
74
73
  message: lib.prompts.createDeveloperTestAccountConfigPrompt.descriptionPrompt(supportFlags),
75
74
  type: 'input',
76
- });
77
- accountDescription = descriptionPromptResult.description;
78
- }
79
- const useDefaultAccountLevelsPromptResult = await promptUser({
80
- name: 'useDefaultAccountLevels',
81
- message: lib.prompts.createDeveloperTestAccountConfigPrompt
82
- .useDefaultAccountLevelsPrompt.message,
83
- type: 'list',
84
- choices: [
85
- {
86
- name: lib.prompts.createDeveloperTestAccountConfigPrompt
87
- .useDefaultAccountLevelsPrompt.default,
88
- value: 'default',
89
- },
90
- {
91
- name: lib.prompts.createDeveloperTestAccountConfigPrompt
92
- .useDefaultAccountLevelsPrompt.manual,
93
- value: 'manual',
94
- },
95
- ],
96
- });
97
- if (useDefaultAccountLevelsPromptResult.useDefaultAccountLevels === 'default') {
98
- accountLevelsArray = [
99
- { hub: 'MARKETING', tier: AccountTiers.ENTERPRISE },
100
- { hub: 'OPS', tier: AccountTiers.ENTERPRISE },
101
- { hub: 'SERVICE', tier: AccountTiers.ENTERPRISE },
102
- { hub: 'SALES', tier: AccountTiers.ENTERPRISE },
103
- { hub: 'CONTENT', tier: AccountTiers.ENTERPRISE },
104
- ];
75
+ when: !args.description,
76
+ },
77
+ ]);
78
+ const accountName = args.name || result.accountName;
79
+ const description = args.description || result.description;
80
+ let accountLevels = {};
81
+ if (hasAnyTierLevels) {
82
+ accountLevels = {
83
+ marketingLevel: args.marketingLevel || 'ENTERPRISE',
84
+ opsLevel: args.opsLevel || 'ENTERPRISE',
85
+ serviceLevel: args.serviceLevel || 'ENTERPRISE',
86
+ salesLevel: args.salesLevel || 'ENTERPRISE',
87
+ contentLevel: args.contentLevel || 'ENTERPRISE',
88
+ };
105
89
  }
106
90
  else {
107
- const accountLevelsPromptResult = await promptUser({
108
- name: 'testAccountLevels',
109
- message: lib.prompts.createDeveloperTestAccountConfigPrompt.tiersPrompt,
110
- type: 'checkbox',
111
- pageSize: 13,
112
- choices: TEST_ACCOUNT_TIERS,
113
- loop: false,
114
- validate: choices => {
115
- if (choices?.length < Object.keys(hubs).length) {
116
- return lib.prompts.createDeveloperTestAccountConfigPrompt.errors
117
- .allHubsRequired;
118
- }
119
- else {
91
+ const tierChoiceResult = await promptUser({
92
+ name: 'useDefaultAccountLevels',
93
+ message: lib.prompts.createDeveloperTestAccountConfigPrompt
94
+ .useDefaultAccountLevelsPrompt.message,
95
+ type: 'list',
96
+ choices: [
97
+ {
98
+ name: lib.prompts.createDeveloperTestAccountConfigPrompt
99
+ .useDefaultAccountLevelsPrompt.default,
100
+ value: 'default',
101
+ },
102
+ {
103
+ name: lib.prompts.createDeveloperTestAccountConfigPrompt
104
+ .useDefaultAccountLevelsPrompt.manual,
105
+ value: 'manual',
106
+ },
107
+ ],
108
+ });
109
+ if (tierChoiceResult.useDefaultAccountLevels === 'default') {
110
+ accountLevels = {
111
+ marketingLevel: 'ENTERPRISE',
112
+ opsLevel: 'ENTERPRISE',
113
+ serviceLevel: 'ENTERPRISE',
114
+ salesLevel: 'ENTERPRISE',
115
+ contentLevel: 'ENTERPRISE',
116
+ };
117
+ }
118
+ else {
119
+ const tierResult = await promptUser({
120
+ name: 'testAccountLevels',
121
+ message: lib.prompts.createDeveloperTestAccountConfigPrompt.tiersPrompt,
122
+ type: 'checkbox',
123
+ pageSize: 13,
124
+ choices: TEST_ACCOUNT_TIERS,
125
+ loop: false,
126
+ validate: choices => {
127
+ if (choices?.length < Object.keys(hubs).length) {
128
+ return lib.prompts.createDeveloperTestAccountConfigPrompt.errors
129
+ .allHubsRequired;
130
+ }
120
131
  const hubMap = {};
121
132
  for (const choice of choices) {
122
133
  const { hub } = choice.value;
@@ -126,21 +137,20 @@ export async function createDeveloperTestAccountConfigPrompt(args = {}, supportF
126
137
  }
127
138
  hubMap[hub] = true;
128
139
  }
129
- }
130
- return true;
131
- },
132
- });
133
- accountLevelsArray = accountLevelsPromptResult.testAccountLevels;
140
+ return true;
141
+ },
142
+ });
143
+ accountLevels = tierResult.testAccountLevels.reduce((acc, level) => {
144
+ const { hub: hubName, tier: hubTier } = level;
145
+ const hubLevel = hubs[hubName];
146
+ acc[hubLevel] = hubTier;
147
+ return acc;
148
+ }, {});
149
+ }
134
150
  }
135
- const accountLevels = accountLevelsArray.reduce((acc, level) => {
136
- const { hub: hubName, tier: hubTier } = level;
137
- const hubLevel = hubs[hubName];
138
- acc[hubLevel] = hubTier;
139
- return acc;
140
- }, {});
141
151
  return {
142
- accountName: accountName,
143
- description: accountDescription,
152
+ accountName,
153
+ description,
144
154
  ...accountLevels,
145
155
  };
146
156
  }
@@ -91,6 +91,12 @@ export class HsCreateFunctionTool extends Tool {
91
91
  title: 'Create HubSpot CMS Serverless Function',
92
92
  description: `Creates a new HubSpot CMS serverless function using the hs create function command. Functions can be created non-interactively by specifying functionsFolder, filename, and endpointPath. Supports all HTTP methods (${HTTP_METHODS.join(', ')}).`,
93
93
  inputSchema,
94
+ annotations: {
95
+ readOnlyHint: false,
96
+ destructiveHint: false,
97
+ idempotentHint: false,
98
+ openWorldHint: false,
99
+ },
94
100
  }, this.handler);
95
101
  }
96
102
  }
@@ -13,20 +13,20 @@ declare const inputSchemaZodObject: z.ZodObject<{
13
13
  }, "strip", z.ZodTypeAny, {
14
14
  absoluteCurrentWorkingDirectory: string;
15
15
  dest?: string | undefined;
16
+ global?: boolean | undefined;
16
17
  moduleLabel?: string | undefined;
17
18
  reactType?: boolean | undefined;
18
- global?: boolean | undefined;
19
- availableForNewContent?: boolean | undefined;
20
19
  contentTypes?: string | undefined;
20
+ availableForNewContent?: boolean | undefined;
21
21
  userSuppliedName?: string | undefined;
22
22
  }, {
23
23
  absoluteCurrentWorkingDirectory: string;
24
24
  dest?: string | undefined;
25
+ global?: boolean | undefined;
25
26
  moduleLabel?: string | undefined;
26
27
  reactType?: boolean | undefined;
27
- global?: boolean | undefined;
28
- availableForNewContent?: boolean | undefined;
29
28
  contentTypes?: string | undefined;
29
+ availableForNewContent?: boolean | undefined;
30
30
  userSuppliedName?: string | undefined;
31
31
  }>;
32
32
  export type HsCreateModuleInputSchema = z.infer<typeof inputSchemaZodObject>;
@@ -113,6 +113,12 @@ export class HsCreateModuleTool extends Tool {
113
113
  title: 'Create HubSpot CMS Module',
114
114
  description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
115
115
  inputSchema,
116
+ annotations: {
117
+ readOnlyHint: false,
118
+ destructiveHint: false,
119
+ idempotentHint: false,
120
+ openWorldHint: false,
121
+ },
116
122
  }, this.handler);
117
123
  }
118
124
  }
@@ -70,6 +70,12 @@ export class HsCreateTemplateTool extends Tool {
70
70
  title: 'Create HubSpot CMS Template',
71
71
  description: `Creates a new HubSpot CMS template using the hs create template command. Templates can be created non-interactively by specifying templateType. Supports all template types including: ${TEMPLATE_TYPES.join(', ')}.`,
72
72
  inputSchema,
73
+ annotations: {
74
+ readOnlyHint: false,
75
+ destructiveHint: false,
76
+ idempotentHint: false,
77
+ openWorldHint: false,
78
+ },
73
79
  }, this.handler);
74
80
  }
75
81
  }
@@ -12,16 +12,16 @@ declare const inputSchemaZodObject: z.ZodObject<{
12
12
  endpoint: string;
13
13
  absoluteCurrentWorkingDirectory: string;
14
14
  account?: string | undefined;
15
- limit?: number | undefined;
16
- compact?: boolean | undefined;
17
15
  latest?: boolean | undefined;
16
+ compact?: boolean | undefined;
17
+ limit?: number | undefined;
18
18
  }, {
19
19
  endpoint: string;
20
20
  absoluteCurrentWorkingDirectory: string;
21
21
  account?: string | undefined;
22
- limit?: number | undefined;
23
- compact?: boolean | undefined;
24
22
  latest?: boolean | undefined;
23
+ compact?: boolean | undefined;
24
+ limit?: number | undefined;
25
25
  }>;
26
26
  export type HsFunctionLogsInputSchema = z.infer<typeof inputSchemaZodObject>;
27
27
  export declare class HsFunctionLogsTool extends Tool<HsFunctionLogsInputSchema> {
@@ -64,6 +64,10 @@ export class HsFunctionLogsTool extends Tool {
64
64
  title: 'Get HubSpot CMS serverless function logs for an endpoint',
65
65
  description: 'Retrieve logs for HubSpot CMS serverless functions. Use this tool to help debug issues with serverless functions by reading the production logs. Supports various options like latest, compact, and limiting results. Use after listing functions with list-cms-serverless-functions to get the endpoint path.',
66
66
  inputSchema,
67
+ annotations: {
68
+ readOnlyHint: true,
69
+ openWorldHint: true,
70
+ },
67
71
  }, this.handler);
68
72
  }
69
73
  }
@@ -53,6 +53,10 @@ export class HsListFunctionsTool extends Tool {
53
53
  title: 'List HubSpot CMS Serverless Functions',
54
54
  description: 'Get a list of all serverless functions deployed in a HubSpot portal/account. Shows function routes, HTTP methods, secrets, and timestamps.',
55
55
  inputSchema,
56
+ annotations: {
57
+ readOnlyHint: true,
58
+ openWorldHint: true,
59
+ },
56
60
  }, this.handler);
57
61
  }
58
62
  }
@@ -53,6 +53,10 @@ export class HsListTool extends Tool {
53
53
  title: 'List HubSpot CMS Directory Contents',
54
54
  description: 'List remote contents of a HubSpot CMS directory.',
55
55
  inputSchema,
56
+ annotations: {
57
+ readOnlyHint: true,
58
+ openWorldHint: true,
59
+ },
56
60
  }, this.handler);
57
61
  }
58
62
  }
@@ -15,11 +15,13 @@ import { HsCreateTemplateTool } from './cms/HsCreateTemplateTool.js';
15
15
  import { HsCreateFunctionTool } from './cms/HsCreateFunctionTool.js';
16
16
  import { HsListFunctionsTool } from './cms/HsListFunctionsTool.js';
17
17
  import { HsFunctionLogsTool } from './cms/HsFunctionLogsTool.js';
18
+ import { CreateTestAccountTool } from './project/CreateTestAccountTool.js';
18
19
  export function registerProjectTools(mcpServer) {
19
20
  return [
20
21
  new UploadProjectTools(mcpServer).register(),
21
22
  new CreateProjectTool(mcpServer).register(),
22
23
  new GuidedWalkthroughTool(mcpServer).register(),
24
+ new CreateTestAccountTool(mcpServer).register(),
23
25
  new DeployProjectTool(mcpServer).register(),
24
26
  new AddFeatureToProjectTool(mcpServer).register(),
25
27
  new ValidateProjectTool(mcpServer).register(),