@hubspot/cli 7.6.0-beta.14 → 7.6.0-beta.16
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/mcp/setup.js +2 -1
- package/commands/mcp.js +1 -1
- package/commands/project/dev/unifiedFlow.js +4 -1
- package/commands/sandbox/delete.js +1 -1
- package/commands/testAccount.js +1 -1
- package/lang/en.js +1 -1
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/projects/__tests__/localDevProjectHelpers.test.js +4 -2
- package/lib/projects/create/__tests__/v3.test.js +75 -0
- package/lib/projects/create/v3.js +4 -2
- package/lib/projects/localDev/helpers/project.js +5 -1
- package/lib/prompts/projectAddPrompt.js +0 -1
- package/package.json +2 -2
package/commands/mcp/setup.js
CHANGED
|
@@ -6,8 +6,9 @@ import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
|
|
|
6
6
|
import { trackCommandUsage } from '../../lib/usageTracking.js';
|
|
7
7
|
import { hasFeature } from '../../lib/hasFeature.js';
|
|
8
8
|
import { FEATURES } from '../../lib/constants.js';
|
|
9
|
+
import { uiBetaTag } from '../../lib/ui/index.js';
|
|
9
10
|
const command = ['setup'];
|
|
10
|
-
const describe = commands.mcp.setup.describe;
|
|
11
|
+
const describe = uiBetaTag(commands.mcp.setup.describe, false);
|
|
11
12
|
async function handler(args) {
|
|
12
13
|
const { derivedAccountId } = args;
|
|
13
14
|
const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
|
package/commands/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ function mcpBuilder(yargs) {
|
|
|
9
9
|
yargs.command(startCommand).command(setupCommand).demandCommand(1, '');
|
|
10
10
|
return yargs;
|
|
11
11
|
}
|
|
12
|
-
const builder = makeYargsBuilder(mcpBuilder, command,
|
|
12
|
+
const builder = makeYargsBuilder(mcpBuilder, command, describe, {
|
|
13
13
|
useGlobalOptions: true,
|
|
14
14
|
});
|
|
15
15
|
const mcpCommand = {
|
|
@@ -9,7 +9,7 @@ import { logError } from '../../../lib/errorHandlers/index.js';
|
|
|
9
9
|
import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
|
|
10
10
|
import { ensureProjectExists } from '../../../lib/projects/ensureProjectExists.js';
|
|
11
11
|
import { createInitialBuildForNewProject, createNewProjectForLocalDev, compareLocalProjectToDeployed, } from '../../../lib/projects/localDev/helpers/project.js';
|
|
12
|
-
import { useExistingDevTestAccount, createDeveloperTestAccountForLocalDev, selectAccountTypePrompt, } from '../../../lib/projects/localDev/helpers/account.js';
|
|
12
|
+
import { useExistingDevTestAccount, createDeveloperTestAccountForLocalDev, selectAccountTypePrompt, createSandboxForLocalDev, } from '../../../lib/projects/localDev/helpers/account.js';
|
|
13
13
|
import { selectDeveloperTestTargetAccountPrompt, selectSandboxTargetAccountPrompt, } from '../../../lib/prompts/projectDevTargetAccountPrompt.js';
|
|
14
14
|
import SpinniesManager from '../../../lib/ui/SpinniesManager.js';
|
|
15
15
|
import LocalDevProcess from '../../../lib/projects/localDev/LocalDevProcess.js';
|
|
@@ -93,6 +93,9 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
|
|
|
93
93
|
const sandboxAccountPromptResponse = await selectSandboxTargetAccountPrompt(accounts, targetProjectAccountConfig);
|
|
94
94
|
targetTestingAccountId =
|
|
95
95
|
sandboxAccountPromptResponse.targetAccountId || undefined;
|
|
96
|
+
if (sandboxAccountPromptResponse.createNestedAccount) {
|
|
97
|
+
targetTestingAccountId = await createSandboxForLocalDev(targetProjectAccountId, targetProjectAccountConfig, env);
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
100
|
else {
|
|
98
101
|
targetTestingAccountId = targetProjectAccountId;
|
|
@@ -121,7 +121,7 @@ async function handler(args) {
|
|
|
121
121
|
const newDefaultAccount = await selectAccountFromConfig();
|
|
122
122
|
updateDefaultAccount(newDefaultAccount);
|
|
123
123
|
}
|
|
124
|
-
else {
|
|
124
|
+
else if (isDefaultAccount && force) {
|
|
125
125
|
// If force is specified, skip prompt and set the parent account id as the default account
|
|
126
126
|
updateDefaultAccount(parentAccountId);
|
|
127
127
|
}
|
package/commands/testAccount.js
CHANGED
|
@@ -5,7 +5,7 @@ import deleteTestAccountCommand from './testAccount/delete.js';
|
|
|
5
5
|
import { makeYargsBuilder } from '../lib/yargsUtils.js';
|
|
6
6
|
import { commands } from '../lang/en.js';
|
|
7
7
|
const command = ['test-account', 'test-accounts'];
|
|
8
|
-
const describe =
|
|
8
|
+
const describe = commands.testAccount.describe;
|
|
9
9
|
function testAccountBuilder(yargs) {
|
|
10
10
|
yargs
|
|
11
11
|
.command(createTestAccountCommand)
|
package/lang/en.js
CHANGED
|
@@ -1016,7 +1016,7 @@ export const commands = {
|
|
|
1016
1016
|
header: 'HubSpot projects local development',
|
|
1017
1017
|
placeholderAccountSelection: 'Using default account as target account (for now)',
|
|
1018
1018
|
accountTypeInformation: 'Testing in a developer test account is strongly recommended, but you can use a sandbox account if your plan allows you to create one.',
|
|
1019
|
-
learnMoreMessageV3: `Learn more about ${uiLink('HubSpot projects local dev', 'https://hubspot.
|
|
1019
|
+
learnMoreMessageV3: `Learn more about ${uiLink('HubSpot projects local dev', 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands#start-a-local-development-server')} | ${uiLink('HubSpot account types', 'https://developers.hubspot.com/docs/getting-started/account-types')}`,
|
|
1020
1020
|
learnMoreMessageLegacy: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
|
|
1021
1021
|
profileProjectAccountExplanation: (accountId, profileName) => `Using account ${uiAccountDescription(accountId)} from profile ${chalk.bold(profileName)} for project upload`,
|
|
1022
1022
|
defaultProjectAccountExplanation: (accountId) => `Using default account ${uiAccountDescription(accountId)} for project upload`,
|
package/lib/constants.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export declare const FEATURES: {
|
|
|
84
84
|
readonly APPS_HOME: "UIE:AppHome";
|
|
85
85
|
readonly MCP_ACCESS: "Developers:CLIMCPAccess";
|
|
86
86
|
readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
|
|
87
|
+
readonly AGENT_TOOLS: "ThirdPartyAgentTools";
|
|
87
88
|
};
|
|
88
89
|
export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
89
90
|
UPLOAD_SUCCESS: string;
|
package/lib/constants.js
CHANGED
|
@@ -76,6 +76,7 @@ export const FEATURES = {
|
|
|
76
76
|
APPS_HOME: 'UIE:AppHome',
|
|
77
77
|
MCP_ACCESS: 'Developers:CLIMCPAccess',
|
|
78
78
|
THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
|
|
79
|
+
AGENT_TOOLS: 'ThirdPartyAgentTools',
|
|
79
80
|
};
|
|
80
81
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
81
82
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
|
@@ -99,7 +99,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
|
|
|
99
99
|
it('should clean up temp directory even when errors occur', async () => {
|
|
100
100
|
// Mock downloadProject to throw an error after temp dir is created
|
|
101
101
|
downloadProject.mockRejectedValue(new Error('Download Error'));
|
|
102
|
-
await
|
|
102
|
+
const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
|
|
103
|
+
expect(result).toBe(false);
|
|
103
104
|
expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
|
|
104
105
|
});
|
|
105
106
|
it('should handle translateForLocalDev errors', async () => {
|
|
@@ -111,7 +112,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
|
|
|
111
112
|
extractZipArchive.mockResolvedValue(undefined);
|
|
112
113
|
// Mock translate to throw an error
|
|
113
114
|
translate.mockRejectedValue(new Error('Translation Error'));
|
|
114
|
-
await
|
|
115
|
+
const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
|
|
116
|
+
expect(result).toBe(false);
|
|
115
117
|
expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
|
|
116
118
|
});
|
|
117
119
|
});
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { calculateComponentTemplateChoices } from '../v3.js';
|
|
2
|
+
import { hasFeature } from '../../../hasFeature.js';
|
|
2
3
|
vi.mock('@hubspot/local-dev-lib/logger');
|
|
3
4
|
vi.mock('@hubspot/local-dev-lib/api/github');
|
|
5
|
+
vi.mock('../../../hasFeature.js');
|
|
6
|
+
const mockHasFeature = vi.mocked(hasFeature);
|
|
4
7
|
describe('lib/projects/create/v3', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockHasFeature.mockResolvedValue(true);
|
|
10
|
+
});
|
|
5
11
|
describe('calculateComponentTemplateChoices()', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockHasFeature.mockClear();
|
|
14
|
+
});
|
|
6
15
|
const mockComponents = [
|
|
7
16
|
{
|
|
8
17
|
label: 'Module Component',
|
|
@@ -162,5 +171,71 @@ describe('lib/projects/create/v3', () => {
|
|
|
162
171
|
// @ts-expect-error breaking stuff on purpose
|
|
163
172
|
projectMetadataWithoutComponents)).rejects.toThrow();
|
|
164
173
|
});
|
|
174
|
+
it('disables gated components when hasFeature returns false', async () => {
|
|
175
|
+
mockHasFeature.mockResolvedValue(false);
|
|
176
|
+
const gatedComponent = [
|
|
177
|
+
{
|
|
178
|
+
label: 'Workflow Action Tool',
|
|
179
|
+
path: 'workflow-action-tool',
|
|
180
|
+
type: 'workflow-action',
|
|
181
|
+
cliSelector: 'workflow-action-tool',
|
|
182
|
+
supportedAuthTypes: ['oauth'],
|
|
183
|
+
supportedDistributions: ['private'],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, mockProjectMetadataForChoices);
|
|
187
|
+
expect(choices).toHaveLength(3); // includes separators
|
|
188
|
+
expect(choices[1]).toEqual({
|
|
189
|
+
name: expect.stringContaining('Workflow Action Tool'),
|
|
190
|
+
value: gatedComponent[0],
|
|
191
|
+
disabled: expect.stringContaining('does not have access to this feature'),
|
|
192
|
+
});
|
|
193
|
+
expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
|
|
194
|
+
});
|
|
195
|
+
it('enables gated components when hasFeature returns true', async () => {
|
|
196
|
+
mockHasFeature.mockResolvedValue(true);
|
|
197
|
+
const gatedComponent = [
|
|
198
|
+
{
|
|
199
|
+
label: 'Workflow Action Tool',
|
|
200
|
+
path: 'workflow-action-tool',
|
|
201
|
+
type: 'workflow-action',
|
|
202
|
+
cliSelector: 'workflow-action-tool',
|
|
203
|
+
supportedAuthTypes: ['oauth'],
|
|
204
|
+
supportedDistributions: ['private'],
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
const projectMetadataWithWorkflowAction = {
|
|
208
|
+
hsMetaFiles: [],
|
|
209
|
+
components: {
|
|
210
|
+
'workflow-action': { count: 0, maxCount: 3, hsMetaFiles: [] },
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, projectMetadataWithWorkflowAction);
|
|
214
|
+
expect(choices).toHaveLength(1); // no disabled components
|
|
215
|
+
expect(choices[0]).toEqual({
|
|
216
|
+
name: 'Workflow Action Tool [workflow-action-tool]',
|
|
217
|
+
value: gatedComponent[0],
|
|
218
|
+
});
|
|
219
|
+
expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
|
|
220
|
+
});
|
|
221
|
+
it('handles non-gated components without calling hasFeature', async () => {
|
|
222
|
+
const nonGatedComponent = [
|
|
223
|
+
{
|
|
224
|
+
label: 'Regular Component',
|
|
225
|
+
path: 'regular',
|
|
226
|
+
type: 'module',
|
|
227
|
+
supportedAuthTypes: ['oauth'],
|
|
228
|
+
supportedDistributions: ['private'],
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
const choices = await calculateComponentTemplateChoices(nonGatedComponent, 'oauth', 'private', 123, mockProjectMetadataForChoices);
|
|
232
|
+
expect(choices).toHaveLength(1);
|
|
233
|
+
expect(choices[0]).toEqual({
|
|
234
|
+
name: 'Regular Component [module]',
|
|
235
|
+
value: nonGatedComponent[0],
|
|
236
|
+
});
|
|
237
|
+
// hasFeature should not be called for non-gated components
|
|
238
|
+
expect(mockHasFeature).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
165
240
|
});
|
|
166
241
|
});
|
|
@@ -52,6 +52,7 @@ export async function createV3App(providedAuth, providedDistribution) {
|
|
|
52
52
|
const componentTypeToGateMap = {
|
|
53
53
|
[AppEventsKey]: FEATURES.APP_EVENTS,
|
|
54
54
|
[PagesKey]: FEATURES.APPS_HOME,
|
|
55
|
+
'workflow-action-tool': FEATURES.AGENT_TOOLS,
|
|
55
56
|
};
|
|
56
57
|
export async function calculateComponentTemplateChoices(components, authType, distribution, accountId, projectMetadata) {
|
|
57
58
|
const enabledComponents = [];
|
|
@@ -81,8 +82,9 @@ export async function calculateComponentTemplateChoices(components, authType, di
|
|
|
81
82
|
!supportedDistributions.includes(distribution)) {
|
|
82
83
|
disabledReasons.push(commands.project.add.error.distributionNotAllowed(distribution));
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
const templateGate = componentTypeToGateMap[template.cliSelector || template.type];
|
|
86
|
+
if (templateGate) {
|
|
87
|
+
const isUngated = await hasFeature(accountId, templateGate);
|
|
86
88
|
if (!isUngated) {
|
|
87
89
|
disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature(accountId));
|
|
88
90
|
}
|
|
@@ -18,7 +18,7 @@ import SpinniesManager from '../../../ui/SpinniesManager.js';
|
|
|
18
18
|
import { EXIT_CODES } from '../../../enums/exitCodes.js';
|
|
19
19
|
import { handleProjectUpload } from '../../upload.js';
|
|
20
20
|
import { pollProjectBuildAndDeploy } from '../../pollProjectBuildAndDeploy.js';
|
|
21
|
-
import { logError } from '../../../errorHandlers/index.js';
|
|
21
|
+
import { debugError, logError } from '../../../errorHandlers/index.js';
|
|
22
22
|
import { ApiErrorContext } from '../../../errorHandlers/index.js';
|
|
23
23
|
// Prompt the user to create a new project if one doesn't exist on their target account
|
|
24
24
|
export async function createNewProjectForLocalDev(projectConfig, targetAccountId, shouldCreateWithoutConfirmation, hasPublicApps) {
|
|
@@ -163,6 +163,10 @@ export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountI
|
|
|
163
163
|
}, { profile });
|
|
164
164
|
return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
|
|
165
165
|
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
debugError(err);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
166
170
|
finally {
|
|
167
171
|
// Clean up temporary directory
|
|
168
172
|
if (tempDir && (await fs.pathExists(tempDir))) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "7.6.0-beta.
|
|
3
|
+
"version": "7.6.0-beta.16",
|
|
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
|
-
"@hubspot/local-dev-lib": "3.19.
|
|
9
|
+
"@hubspot/local-dev-lib": "3.19.1",
|
|
10
10
|
"@hubspot/project-parsing-lib": "0.8.5",
|
|
11
11
|
"@hubspot/serverless-dev-runtime": "7.0.6",
|
|
12
12
|
"@hubspot/theme-preview-dev-server": "0.0.10",
|