@hubspot/cli 7.10.0-beta.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.
- package/commands/account/__tests__/rename.test.js +35 -0
- package/commands/account/rename.d.ts +1 -1
- package/commands/account/rename.js +5 -2
- package/commands/config/set.js +1 -2
- package/commands/getStarted.js +8 -2
- package/commands/hubdb.d.ts +1 -1
- package/commands/project/dev/index.js +8 -1
- package/commands/project/listBuilds.js +7 -1
- package/commands/project/upload.js +7 -1
- package/commands/project/validate.js +7 -1
- package/commands/project/watch.js +7 -2
- package/commands/testAccount/__tests__/create.test.js +68 -0
- package/commands/testAccount/create.d.ts +8 -0
- package/commands/testAccount/create.js +133 -43
- package/commands/testAccount/importData.d.ts +1 -1
- package/lang/en.d.ts +3199 -3204
- package/lang/en.js +24 -3
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +6 -0
- package/lib/mcp/__tests__/setup.test.d.ts +1 -0
- package/lib/mcp/__tests__/setup.test.js +127 -0
- package/lib/mcp/setup.d.ts +4 -12
- package/lib/mcp/setup.js +34 -1
- package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
- package/lib/middleware/autoUpdateMiddleware.js +1 -0
- package/lib/projects/__tests__/components.test.js +148 -24
- package/lib/projects/__tests__/projects.test.js +13 -42
- package/lib/projects/components.js +76 -20
- package/lib/projects/config.js +5 -9
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +6 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsCreateModuleTool.js +6 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +6 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +4 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.js +4 -0
- package/mcp-server/tools/cms/HsListTool.js +4 -0
- package/mcp-server/tools/index.js +2 -0
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -0
- package/mcp-server/tools/project/CreateProjectTool.js +6 -0
- package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
- package/mcp-server/tools/project/CreateTestAccountTool.js +137 -0
- package/mcp-server/tools/project/DeployProjectTool.js +6 -0
- package/mcp-server/tools/project/DocFetchTool.js +4 -0
- package/mcp-server/tools/project/DocsSearchTool.js +4 -0
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +4 -0
- package/mcp-server/tools/project/GetApplicationInfoTool.js +4 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +4 -0
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +4 -0
- package/mcp-server/tools/project/UploadProjectTools.js +6 -0
- package/mcp-server/tools/project/ValidateProjectTool.js +4 -0
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +231 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
- package/package.json +1 -1
|
@@ -1,83 +1,54 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
5
4
|
import { validateProjectConfig } from '../../projects/config.js';
|
|
6
|
-
import
|
|
5
|
+
import ProjectValidationError from '../../errors/ProjectValidationError.js';
|
|
7
6
|
vi.mock('../../ui/logger.js');
|
|
8
7
|
describe('lib/projects', () => {
|
|
9
8
|
describe('validateProjectConfig()', () => {
|
|
10
9
|
let projectDir;
|
|
11
|
-
let exitMock;
|
|
12
10
|
beforeAll(() => {
|
|
13
11
|
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'projects-'));
|
|
14
12
|
fs.mkdirSync(path.join(projectDir, 'src'));
|
|
15
13
|
});
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
// @ts-expect-error - Mocking process.exit
|
|
18
|
-
exitMock = vi
|
|
19
|
-
.spyOn(process, 'exit')
|
|
20
|
-
.mockImplementation(() => undefined);
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
exitMock.mockRestore();
|
|
24
|
-
});
|
|
25
14
|
it('rejects undefined configuration', () => {
|
|
26
15
|
// @ts-ignore Testing invalid input
|
|
27
|
-
validateProjectConfig(null, projectDir);
|
|
28
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
29
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*Unable to locate a project configuration file. Try running again from a project directory, or run*/));
|
|
16
|
+
expect(() => validateProjectConfig(null, projectDir)).toThrow(/.*Unable to locate a project configuration file. Try running again from a project directory, or run*/);
|
|
30
17
|
});
|
|
31
18
|
it('rejects configuration with missing name', () => {
|
|
32
19
|
// @ts-ignore Testing invalid input
|
|
33
|
-
validateProjectConfig({ srcDir: '.' }, projectDir);
|
|
34
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
35
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*missing required fields*/));
|
|
20
|
+
expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(/.*missing required fields*/);
|
|
36
21
|
});
|
|
37
22
|
it('rejects configuration with missing srcDir', () => {
|
|
23
|
+
expect(() =>
|
|
38
24
|
// @ts-ignore Testing invalid input
|
|
39
|
-
validateProjectConfig({ name: 'hello' }, projectDir);
|
|
40
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
41
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*missing required fields.*/));
|
|
25
|
+
validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(/.*missing required fields.*/);
|
|
42
26
|
});
|
|
43
27
|
describe('rejects configuration with srcDir outside project directory', () => {
|
|
44
28
|
it('for parent directory', () => {
|
|
45
|
-
validateProjectConfig({ name: 'hello', srcDir: '..', platformVersion: '' }, projectDir);
|
|
46
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
47
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('srcDir: ".."'));
|
|
29
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '..', platformVersion: '' }, projectDir)).toThrow(/srcDir: "\.\."/);
|
|
48
30
|
});
|
|
49
31
|
it('for root directory', () => {
|
|
50
|
-
validateProjectConfig({ name: 'hello', srcDir: '/', platformVersion: '' }, projectDir);
|
|
51
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
52
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('srcDir: "/"'));
|
|
32
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '/', platformVersion: '' }, projectDir)).toThrow(/srcDir: "\/"/);
|
|
53
33
|
});
|
|
54
34
|
it('for complicated directory', () => {
|
|
55
35
|
const srcDir = './src/././../src/../../src';
|
|
56
|
-
validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir);
|
|
57
|
-
expect(
|
|
58
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining(`srcDir: "${srcDir}"`));
|
|
36
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir)).toThrow(ProjectValidationError);
|
|
37
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir)).toThrow(/srcDir:/);
|
|
59
38
|
});
|
|
60
39
|
});
|
|
61
40
|
it('rejects configuration with srcDir that does not exist', () => {
|
|
62
|
-
validateProjectConfig({ name: 'hello', srcDir: 'foo', platformVersion: '' }, projectDir);
|
|
63
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
64
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*could not be found in.*/));
|
|
41
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: 'foo', platformVersion: '' }, projectDir)).toThrow(/.*could not be found in.*/);
|
|
65
42
|
});
|
|
66
43
|
describe('accepts configuration with valid srcDir', () => {
|
|
67
44
|
it('for current directory', () => {
|
|
68
|
-
validateProjectConfig({ name: 'hello', srcDir: '.', platformVersion: '' }, projectDir);
|
|
69
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
70
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
45
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '.', platformVersion: '' }, projectDir)).not.toThrow();
|
|
71
46
|
});
|
|
72
47
|
it('for relative directory', () => {
|
|
73
|
-
validateProjectConfig({ name: 'hello', srcDir: './src', platformVersion: '' }, projectDir);
|
|
74
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
75
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
48
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: './src', platformVersion: '' }, projectDir)).not.toThrow();
|
|
76
49
|
});
|
|
77
50
|
it('for implied relative directory', () => {
|
|
78
|
-
validateProjectConfig({ name: 'hello', srcDir: 'src', platformVersion: '' }, projectDir);
|
|
79
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
80
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
51
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: 'src', platformVersion: '' }, projectDir)).not.toThrow();
|
|
81
52
|
});
|
|
82
53
|
});
|
|
83
54
|
});
|
|
@@ -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
|
|
24
|
-
|
|
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]:
|
|
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]:
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|
package/lib/projects/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
return process.exit(EXIT_CODES.ERROR);
|
|
64
|
+
throw new ProjectValidationError(lib.projects.validateProjectConfig.srcDirNotFound(projectConfig.srcDir, projectDir));
|
|
69
65
|
}
|
|
70
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
108
|
-
name: '
|
|
109
|
-
message: lib.prompts.createDeveloperTestAccountConfigPrompt
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
choices:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
143
|
-
description
|
|
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
|
}
|