@hubspot/cli 7.8.0-experimental.0 → 7.8.1-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +0 -2
- package/commands/getStarted.d.ts +1 -1
- package/commands/getStarted.js +64 -16
- package/commands/mcp/setup.js +8 -0
- package/commands/project/dev/unifiedFlow.js +1 -1
- package/commands/project/migrate.js +30 -21
- package/lang/en.d.ts +4 -1
- package/lang/en.js +5 -1
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/app/__tests__/migrate.test.js +14 -51
- package/lib/app/migrate.d.ts +2 -8
- package/lib/app/migrate.js +5 -80
- package/lib/constants.d.ts +3 -0
- package/lib/constants.js +3 -0
- package/lib/dependencyManagement.d.ts +0 -5
- package/lib/dependencyManagement.js +0 -9
- package/lib/hasFeature.js +6 -0
- package/lib/links.d.ts +1 -0
- package/lib/links.js +10 -3
- package/lib/mcp/setup.js +1 -1
- package/lib/projects/create/v3.js +3 -2
- package/lib/projects/localDev/helpers/project.d.ts +2 -2
- package/lib/projects/localDev/helpers/project.js +5 -6
- package/lib/theme/__tests__/migrate.test.d.ts +1 -0
- package/lib/theme/__tests__/migrate.test.js +233 -0
- package/lib/theme/migrate.d.ts +13 -0
- package/lib/theme/migrate.js +90 -0
- package/lib/ui/SpinniesManager.js +105 -8
- package/lib/usageTracking.js +2 -2
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -2
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +5 -5
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/DocFetchTool.js +2 -2
- package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocsSearchTool.js +7 -7
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +11 -5
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
- package/mcp-server/tools/project/UploadProjectTools.js +2 -2
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- 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__/DeployProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
- package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +9 -3
- package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
- package/mcp-server/utils/cliConfig.d.ts +1 -0
- package/mcp-server/utils/cliConfig.js +12 -0
- package/package.json +2 -7
- package/ui/components/HorizontalSelectPrompt.js +1 -1
- package/commands/getStartedV2.d.ts +0 -9
- package/commands/getStartedV2.js +0 -39
- package/ui/components/Ascii.d.ts +0 -10
- package/ui/components/Ascii.js +0 -11
- package/ui/views/GetStarted.d.ts +0 -7
- package/ui/views/GetStarted.js +0 -157
package/bin/cli.js
CHANGED
|
@@ -42,7 +42,6 @@ import appCommand from '../commands/app.js';
|
|
|
42
42
|
import testAccountCommands from '../commands/testAccount.js';
|
|
43
43
|
import getStartedCommand from '../commands/getStarted.js';
|
|
44
44
|
import mcpCommand from '../commands/mcp.js';
|
|
45
|
-
import getStartedV2Command from '../commands/getStartedV2.js';
|
|
46
45
|
function getTerminalWidth() {
|
|
47
46
|
const width = yargs().terminalWidth();
|
|
48
47
|
if (width >= 100)
|
|
@@ -93,7 +92,6 @@ const argv = yargs(process.argv.slice(2))
|
|
|
93
92
|
type: 'boolean',
|
|
94
93
|
})
|
|
95
94
|
.check(performChecks)
|
|
96
|
-
.command(getStartedV2Command)
|
|
97
95
|
.command(authCommand)
|
|
98
96
|
.command(initCommand)
|
|
99
97
|
.command(logsCommand)
|
package/commands/getStarted.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AccountArgs, YargsCommandModule, CommonArgs, ConfigArgs, EnvironmentArgs } from '../types/Yargs.js';
|
|
2
2
|
export declare const command = "get-started";
|
|
3
3
|
export declare const describe: undefined;
|
|
4
|
-
|
|
4
|
+
type GetStartedArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
|
|
5
5
|
name?: string;
|
|
6
6
|
dest?: string;
|
|
7
7
|
};
|
package/commands/getStarted.js
CHANGED
|
@@ -4,6 +4,7 @@ import open from 'open';
|
|
|
4
4
|
import { getCwd } from '@hubspot/local-dev-lib/path';
|
|
5
5
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
6
6
|
import { commands } from '../lang/en.js';
|
|
7
|
+
import { trackCommandMetadataUsage, trackCommandUsage, } from '../lib/usageTracking.js';
|
|
7
8
|
import { EXIT_CODES } from '../lib/enums/exitCodes.js';
|
|
8
9
|
import { makeYargsBuilder } from '../lib/yargsUtils.js';
|
|
9
10
|
import { promptUser } from '../lib/prompts/promptUtils.js';
|
|
@@ -27,9 +28,8 @@ export const describe = undefined;
|
|
|
27
28
|
async function handler(args) {
|
|
28
29
|
const { derivedAccountId } = args;
|
|
29
30
|
const env = getEnv(derivedAccountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD;
|
|
31
|
+
await trackCommandUsage('get-started', {}, derivedAccountId);
|
|
30
32
|
const accountName = uiAccountDescription(derivedAccountId);
|
|
31
|
-
// TODO: Put this in constants.ts once we have a defined place for the template before INBOUND
|
|
32
|
-
const templateSource = 'robrown-hubspot/hubspot-project-components-ua-app-objects-beta';
|
|
33
33
|
uiInfoSection(commands.getStarted.startTitle, () => {
|
|
34
34
|
uiLogger.log(commands.getStarted.startDescription);
|
|
35
35
|
uiLogger.log(commands.getStarted.guideOverview(accountName));
|
|
@@ -52,6 +52,8 @@ async function handler(args) {
|
|
|
52
52
|
default: GET_STARTED_OPTIONS.APP,
|
|
53
53
|
},
|
|
54
54
|
]);
|
|
55
|
+
// Track user's initial choice
|
|
56
|
+
await trackCommandMetadataUsage('get-started', { step: 'select-option', type: selectedOption }, derivedAccountId);
|
|
55
57
|
if (selectedOption === GET_STARTED_OPTIONS.CMS) {
|
|
56
58
|
uiLogger.log(' ');
|
|
57
59
|
uiLogger.log(commands.getStarted.designManager);
|
|
@@ -64,6 +66,11 @@ async function handler(args) {
|
|
|
64
66
|
message: commands.getStarted.openDesignManagerPrompt,
|
|
65
67
|
},
|
|
66
68
|
]);
|
|
69
|
+
// Track Design Manager browser action
|
|
70
|
+
await trackCommandMetadataUsage('get-started', {
|
|
71
|
+
step: 'open-design-manager',
|
|
72
|
+
type: shouldOpen ? 'opened' : 'declined',
|
|
73
|
+
}, derivedAccountId);
|
|
67
74
|
if (shouldOpen) {
|
|
68
75
|
uiLogger.log('');
|
|
69
76
|
openLink(derivedAccountId, 'design-manager');
|
|
@@ -74,36 +81,37 @@ async function handler(args) {
|
|
|
74
81
|
else {
|
|
75
82
|
uiLogger.log(' ');
|
|
76
83
|
uiLogger.log(commands.getStarted.logs.appSelected);
|
|
77
|
-
// 1. Fetch project templates
|
|
78
|
-
let latestRepoReleaseTag;
|
|
79
84
|
const { dest, name } = await projectNameAndDestPrompt(args);
|
|
80
|
-
// Specific template for get-started command
|
|
81
|
-
const projectTemplate = {
|
|
82
|
-
name: 'private-app-get-started-template',
|
|
83
|
-
label: 'CRM getting started project with private apps',
|
|
84
|
-
path: 'projects/private-app-get-started-template',
|
|
85
|
-
};
|
|
86
|
-
// 3. Create the project files
|
|
87
85
|
const projectDest = path.resolve(getCwd(), dest);
|
|
88
86
|
const { projectConfig: existingProjectConfig, projectDir: existingProjectDir, } = await getProjectConfig(projectDest);
|
|
89
87
|
if (existingProjectConfig &&
|
|
90
88
|
existingProjectDir &&
|
|
91
89
|
projectDest.startsWith(existingProjectDir)) {
|
|
90
|
+
// Track nested project error
|
|
91
|
+
await trackCommandMetadataUsage('get-started', {
|
|
92
|
+
successful: false,
|
|
93
|
+
step: 'project-creation',
|
|
94
|
+
}, derivedAccountId);
|
|
92
95
|
uiLogger.log(' ');
|
|
93
96
|
uiLogger.error(commands.project.create.errors.cannotNestProjects(existingProjectDir));
|
|
94
97
|
process.exit(EXIT_CODES.ERROR);
|
|
95
98
|
}
|
|
96
|
-
const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
|
|
97
99
|
// 4. Clone the project template from GitHub
|
|
98
|
-
// This is temporary until we have the UA template in the main repo
|
|
99
100
|
try {
|
|
100
|
-
await cloneGithubRepo(
|
|
101
|
-
sourceDir:
|
|
102
|
-
tag: latestRepoReleaseTag,
|
|
101
|
+
await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDest, {
|
|
102
|
+
sourceDir: '2025.2/private-app-get-started-template',
|
|
103
103
|
hideLogs: true,
|
|
104
104
|
});
|
|
105
|
+
await trackCommandMetadataUsage('get-started', {
|
|
106
|
+
successful: true,
|
|
107
|
+
step: 'github-clone',
|
|
108
|
+
}, derivedAccountId);
|
|
105
109
|
}
|
|
106
110
|
catch (err) {
|
|
111
|
+
await trackCommandMetadataUsage('get-started', {
|
|
112
|
+
successful: false,
|
|
113
|
+
step: 'github-clone',
|
|
114
|
+
}, derivedAccountId);
|
|
107
115
|
debugError(err);
|
|
108
116
|
uiLogger.log(' ');
|
|
109
117
|
uiLogger.error(commands.project.create.errors.failedToDownloadProject);
|
|
@@ -122,6 +130,11 @@ async function handler(args) {
|
|
|
122
130
|
uiLogger.log(' ');
|
|
123
131
|
uiLogger.log(commands.getStarted.prompts.projectCreated.description);
|
|
124
132
|
uiLogger.log(' ');
|
|
133
|
+
// Track successful project creation
|
|
134
|
+
await trackCommandMetadataUsage('get-started', {
|
|
135
|
+
successful: true,
|
|
136
|
+
step: 'project-creation',
|
|
137
|
+
}, derivedAccountId);
|
|
125
138
|
// 5. Install dependencies
|
|
126
139
|
const installLocations = await getProjectPackageJsonLocations(projectDest);
|
|
127
140
|
try {
|
|
@@ -147,11 +160,21 @@ async function handler(args) {
|
|
|
147
160
|
default: true,
|
|
148
161
|
},
|
|
149
162
|
]);
|
|
163
|
+
// Track upload decision
|
|
164
|
+
await trackCommandMetadataUsage('get-started', {
|
|
165
|
+
step: 'upload-decision',
|
|
166
|
+
type: shouldUpload ? 'upload' : 'skip',
|
|
167
|
+
}, derivedAccountId);
|
|
150
168
|
if (shouldUpload) {
|
|
151
169
|
try {
|
|
152
170
|
// Get the project config for the newly created project
|
|
153
171
|
const { projectConfig: newProjectConfig, projectDir: newProjectDir } = await getProjectConfig(projectDest);
|
|
154
172
|
if (!newProjectConfig || !newProjectDir) {
|
|
173
|
+
// Track config file not found error
|
|
174
|
+
await trackCommandMetadataUsage('get-started', {
|
|
175
|
+
successful: false,
|
|
176
|
+
step: 'config-file-not-found',
|
|
177
|
+
}, derivedAccountId);
|
|
155
178
|
uiLogger.log(' ');
|
|
156
179
|
uiLogger.error(commands.getStarted.errors.configFileNotFound);
|
|
157
180
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -172,11 +195,21 @@ async function handler(args) {
|
|
|
172
195
|
skipValidation: false,
|
|
173
196
|
});
|
|
174
197
|
if (uploadError) {
|
|
198
|
+
// Track upload failure
|
|
199
|
+
await trackCommandMetadataUsage('get-started', {
|
|
200
|
+
successful: false,
|
|
201
|
+
step: 'upload',
|
|
202
|
+
}, derivedAccountId);
|
|
175
203
|
uiLogger.log(' ');
|
|
176
204
|
uiLogger.error(commands.getStarted.errors.uploadFailed);
|
|
177
205
|
debugError(uploadError);
|
|
178
206
|
}
|
|
179
207
|
else if (result) {
|
|
208
|
+
// Track successful upload completion
|
|
209
|
+
await trackCommandMetadataUsage('get-started', {
|
|
210
|
+
successful: true,
|
|
211
|
+
step: 'upload',
|
|
212
|
+
}, derivedAccountId);
|
|
180
213
|
uiLogger.log(' ');
|
|
181
214
|
uiLogger.success(commands.getStarted.logs.uploadSuccess);
|
|
182
215
|
const { data: { results }, } = await fetchPublicAppsForPortal(derivedAccountId);
|
|
@@ -192,6 +225,11 @@ async function handler(args) {
|
|
|
192
225
|
message: commands.getStarted.openInstallUrl,
|
|
193
226
|
},
|
|
194
227
|
]);
|
|
228
|
+
// Track Developer Overview browser action
|
|
229
|
+
await trackCommandMetadataUsage('get-started', {
|
|
230
|
+
step: 'open-distribution-page',
|
|
231
|
+
type: shouldOpenOverview ? 'opened' : 'declined',
|
|
232
|
+
}, derivedAccountId);
|
|
195
233
|
if (shouldOpenOverview) {
|
|
196
234
|
open(getStaticAuthAppInstallUrl({
|
|
197
235
|
targetAccountId: derivedAccountId,
|
|
@@ -207,6 +245,11 @@ async function handler(args) {
|
|
|
207
245
|
}
|
|
208
246
|
}
|
|
209
247
|
catch (err) {
|
|
248
|
+
// Track upload exception
|
|
249
|
+
await trackCommandMetadataUsage('get-started', {
|
|
250
|
+
successful: false,
|
|
251
|
+
step: 'upload',
|
|
252
|
+
}, derivedAccountId);
|
|
210
253
|
uiLogger.log(' ');
|
|
211
254
|
uiLogger.error(commands.getStarted.errors.uploadFailed);
|
|
212
255
|
debugError(err);
|
|
@@ -214,6 +257,11 @@ async function handler(args) {
|
|
|
214
257
|
}
|
|
215
258
|
}
|
|
216
259
|
}
|
|
260
|
+
// Track successful completion of get-started command
|
|
261
|
+
await trackCommandMetadataUsage('get-started', {
|
|
262
|
+
successful: true,
|
|
263
|
+
step: 'command-completed',
|
|
264
|
+
}, derivedAccountId);
|
|
217
265
|
process.exit(EXIT_CODES.SUCCESS);
|
|
218
266
|
}
|
|
219
267
|
function getStartedBuilder(yargs) {
|
package/commands/mcp/setup.js
CHANGED
|
@@ -4,9 +4,17 @@ import { commands } from '../../lang/en.js';
|
|
|
4
4
|
import { uiLogger } from '../../lib/ui/logger.js';
|
|
5
5
|
import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
|
|
6
6
|
import { trackCommandUsage } from '../../lib/usageTracking.js';
|
|
7
|
+
import { hasFeature } from '../../lib/hasFeature.js';
|
|
8
|
+
import { FEATURES } from '../../lib/constants.js';
|
|
7
9
|
const command = ['setup', 'update'];
|
|
8
10
|
const describe = undefined; // Leave hidden for now
|
|
9
11
|
async function handler(args) {
|
|
12
|
+
const { derivedAccountId } = args;
|
|
13
|
+
const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
|
|
14
|
+
if (!hasMcpAccess) {
|
|
15
|
+
uiLogger.error(commands.mcp.setup.errors.needsMcpAccess(derivedAccountId));
|
|
16
|
+
process.exit(EXIT_CODES.ERROR);
|
|
17
|
+
}
|
|
10
18
|
try {
|
|
11
19
|
await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
12
20
|
}
|
|
@@ -109,7 +109,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
|
|
|
109
109
|
let project = uploadedProject;
|
|
110
110
|
SpinniesManager.init();
|
|
111
111
|
if (projectExists && project) {
|
|
112
|
-
await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes);
|
|
112
|
+
await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes, args.profile);
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
115
|
project = await createNewProjectForLocalDev(projectConfig, targetProjectAccountId, false, false);
|
|
@@ -8,8 +8,9 @@ import { uiCommandReference } from '../../lib/ui/index.js';
|
|
|
8
8
|
import { commands, lib } from '../../lang/en.js';
|
|
9
9
|
import { uiLogger } from '../../lib/ui/logger.js';
|
|
10
10
|
import { logInBox } from '../../lib/ui/boxen.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { getHasMigratableThemes, migrateThemes2025_2, } from '../../lib/theme/migrate.js';
|
|
12
|
+
import { hasFeature } from '../../lib/hasFeature.js';
|
|
13
|
+
import { FEATURES } from '../../lib/constants.js';
|
|
13
14
|
const { v2025_2 } = PLATFORM_VERSIONS;
|
|
14
15
|
const command = 'migrate';
|
|
15
16
|
const describe = undefined; // commands.project.migrate.describe
|
|
@@ -21,28 +22,36 @@ async function handler(args) {
|
|
|
21
22
|
return process.exit(EXIT_CODES.ERROR);
|
|
22
23
|
}
|
|
23
24
|
if (projectConfig?.projectConfig) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
await renderInline(getWarningBox({
|
|
32
|
-
title: lib.migrate.projectMigrationWarningTitle,
|
|
33
|
-
message: lib.migrate.projectMigrationWarning,
|
|
34
|
-
}));
|
|
35
|
-
}
|
|
25
|
+
await logInBox({
|
|
26
|
+
contents: lib.migrate.projectMigrationWarning,
|
|
27
|
+
options: { title: lib.migrate.projectMigrationWarningTitle },
|
|
28
|
+
});
|
|
36
29
|
}
|
|
37
30
|
const { derivedAccountId } = args;
|
|
38
31
|
try {
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes(projectConfig);
|
|
33
|
+
if (hasMigratableThemes) {
|
|
34
|
+
const hasThemeMigrationAccess = await hasFeature(derivedAccountId, FEATURES.THEME_MIGRATION_2025_2);
|
|
35
|
+
if (!hasThemeMigrationAccess) {
|
|
36
|
+
uiLogger.error(commands.project.migrate.errors.noThemeMigrationAccess(derivedAccountId));
|
|
37
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
38
|
+
}
|
|
39
|
+
await migrateThemes2025_2(derivedAccountId, {
|
|
40
|
+
...args,
|
|
41
|
+
platformVersion: unstable
|
|
42
|
+
? PLATFORM_VERSIONS.unstable
|
|
43
|
+
: platformVersion,
|
|
44
|
+
}, migratableThemesCount, projectConfig);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await migrateApp2025_2(derivedAccountId, {
|
|
48
|
+
...args,
|
|
49
|
+
name: projectConfig?.projectConfig?.name,
|
|
50
|
+
platformVersion: unstable
|
|
51
|
+
? PLATFORM_VERSIONS.unstable
|
|
52
|
+
: platformVersion,
|
|
53
|
+
}, projectConfig);
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
catch (error) {
|
|
48
57
|
logError(error);
|
package/lang/en.d.ts
CHANGED
|
@@ -872,6 +872,7 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
|
|
|
872
872
|
};
|
|
873
873
|
readonly success: (derivedTargets: string[]) => string;
|
|
874
874
|
readonly errors: {
|
|
875
|
+
readonly needsMcpAccess: (accountId?: number) => string;
|
|
875
876
|
readonly needsNode20: "This feature requires node >=20";
|
|
876
877
|
readonly errorParsingJsonFIle: (filename: string, errorMessage: string) => string;
|
|
877
878
|
};
|
|
@@ -1136,6 +1137,7 @@ ${string}`;
|
|
|
1136
1137
|
readonly describe: "Migrate an existing project to the new version of the projects framework.";
|
|
1137
1138
|
readonly errors: {
|
|
1138
1139
|
readonly noProjectConfig: (command: string) => string;
|
|
1140
|
+
readonly noThemeMigrationAccess: (accountId?: number) => string;
|
|
1139
1141
|
};
|
|
1140
1142
|
readonly examples: {
|
|
1141
1143
|
readonly default: "Migrate an existing project to the new version of the projects framework.";
|
|
@@ -2601,7 +2603,7 @@ export declare const lib: {
|
|
|
2601
2603
|
readonly checking: "Checking if your deployed build is up to date...";
|
|
2602
2604
|
readonly upToDate: "Deployed build is up to date.";
|
|
2603
2605
|
readonly notUpToDate: "Your project contains undeployed local changes.";
|
|
2604
|
-
readonly notUpToDateExplanation:
|
|
2606
|
+
readonly notUpToDateExplanation: (profile?: string) => string;
|
|
2605
2607
|
};
|
|
2606
2608
|
readonly createNewProjectForLocalDev: {
|
|
2607
2609
|
readonly projectMustExistExplanation: (projectName: string, accountId: number) => string;
|
|
@@ -3388,6 +3390,7 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
3388
3390
|
readonly sourceContentsMoved: (newLocation: string) => string;
|
|
3389
3391
|
readonly projectMigrationWarningTitle: "Important: Migrating to platformVersion 2025.2 is irreversible";
|
|
3390
3392
|
readonly projectMigrationWarning: string;
|
|
3393
|
+
readonly exitWithoutMigrating: "Exiting without migrating";
|
|
3391
3394
|
readonly success: {
|
|
3392
3395
|
readonly downloadedProject: (projectName: string, projectDest: string) => string;
|
|
3393
3396
|
readonly themesMigrationSuccess: (platformVersion: string) => string;
|
package/lang/en.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constant
|
|
|
5
5
|
import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
|
|
6
6
|
import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
|
|
7
7
|
import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, getAppAllowlistUrl, } from '../lib/projects/urls.js';
|
|
8
|
+
import { getProductUpdatesUrl } from '../lib/links.js';
|
|
8
9
|
import { APP_DISTRIBUTION_TYPES, APP_AUTH_TYPES, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
|
|
9
10
|
import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
|
|
10
11
|
export const commands = {
|
|
@@ -875,6 +876,7 @@ export const commands = {
|
|
|
875
876
|
},
|
|
876
877
|
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')}.`,
|
|
877
878
|
errors: {
|
|
879
|
+
needsMcpAccess: (accountId) => `You must opt in to the developer MCP beta to use this feature on ${uiAccountDescription(accountId)}. Try again with a different account or ${uiLink('join the beta now', getProductUpdatesUrl('239890', accountId))}`,
|
|
878
880
|
needsNode20: `This feature requires node >=20`,
|
|
879
881
|
errorParsingJsonFIle: (filename, errorMessage) => `Unable to update ${chalk.bold(filename)} due to invalid JSON: ${errorMessage}`,
|
|
880
882
|
},
|
|
@@ -1135,6 +1137,7 @@ export const commands = {
|
|
|
1135
1137
|
describe: 'Migrate an existing project to the new version of the projects framework.',
|
|
1136
1138
|
errors: {
|
|
1137
1139
|
noProjectConfig: (command) => `No project detected. Please run this command again from a project directory. If you are trying to migrate an app, run ${command}`,
|
|
1140
|
+
noThemeMigrationAccess: (accountId) => `This project contains a CMS theme. You must opt in to theme migration beta to continue updating it on ${uiAccountDescription(accountId)}. Try again with a different account or ${uiLink('join the beta now', getProductUpdatesUrl('253920', accountId))}`,
|
|
1138
1141
|
},
|
|
1139
1142
|
examples: {
|
|
1140
1143
|
default: 'Migrate an existing project to the new version of the projects framework.',
|
|
@@ -2598,7 +2601,7 @@ export const lib = {
|
|
|
2598
2601
|
checking: 'Checking if your deployed build is up to date...',
|
|
2599
2602
|
upToDate: 'Deployed build is up to date.',
|
|
2600
2603
|
notUpToDate: `Your project contains undeployed local changes.`,
|
|
2601
|
-
notUpToDateExplanation: `Run ${uiCommandReference(
|
|
2604
|
+
notUpToDateExplanation: (profile) => `Run ${uiCommandReference(`hs project upload ${profile ? `--profile ${profile}` : ''}`)} to upload these changes to HubSpot, then re-run ${uiCommandReference(`hs project dev ${profile ? `--profile ${profile}` : ''}`)} to continue local development.`,
|
|
2602
2605
|
},
|
|
2603
2606
|
createNewProjectForLocalDev: {
|
|
2604
2607
|
projectMustExistExplanation: (projectName, accountId) => `The project ${projectName} does not exist in the target account ${uiAccountDescription(accountId)}. This command requires the project to exist in the target account.`,
|
|
@@ -3382,6 +3385,7 @@ export const lib = {
|
|
|
3382
3385
|
sourceContentsMoved: (newLocation) => `The contents of your old source directory have been moved to ${newLocation}, move any required files to the new source directory.`,
|
|
3383
3386
|
projectMigrationWarningTitle: 'Important: Migrating to platformVersion 2025.2 is irreversible',
|
|
3384
3387
|
projectMigrationWarning: uiBetaTag(`Running the ${uiCommandReference('hs project migrate')} command will permanently upgrade your project to platformVersion 2025.2. This action cannot be undone. To ensure you have access to your original files, they will be copied to a new directory (archive) for safekeeping.\n\nThis command will guide you through the process, prompting you to enter the required fields and will download the new project source code into your project source directory.`, false),
|
|
3388
|
+
exitWithoutMigrating: 'Exiting without migrating',
|
|
3385
3389
|
success: {
|
|
3386
3390
|
downloadedProject: (projectName, projectDest) => `Saved ${projectName} to ${projectDest}`,
|
|
3387
3391
|
themesMigrationSuccess: (platformVersion) => `Successfully migrated project to platformVersion ${chalk.bold(platformVersion)}. Upload your project using ${uiCommandReference('hs project upload')}`,
|
|
@@ -1,35 +1,173 @@
|
|
|
1
1
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
2
|
-
import {
|
|
2
|
+
import { http } from '@hubspot/local-dev-lib/http';
|
|
3
|
+
import { hasFeature, hasUnfiedAppsAccess } from '../hasFeature.js';
|
|
4
|
+
import { FEATURES } from '../constants.js';
|
|
3
5
|
vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
|
|
6
|
+
vi.mock('@hubspot/local-dev-lib/http');
|
|
4
7
|
const mockedFetchEnabledFeatures = fetchEnabledFeatures;
|
|
8
|
+
const mockedHttp = http;
|
|
5
9
|
describe('lib/hasFeature', () => {
|
|
6
10
|
describe('hasFeature()', () => {
|
|
7
11
|
const accountId = 123;
|
|
8
|
-
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it('should return true if the feature is enabled', async () => {
|
|
9
16
|
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
10
17
|
data: {
|
|
11
18
|
enabledFeatures: {
|
|
12
19
|
'feature-1': true,
|
|
13
|
-
'feature-2': false,
|
|
14
|
-
'feature-3': true,
|
|
15
20
|
},
|
|
16
21
|
},
|
|
17
22
|
});
|
|
18
|
-
});
|
|
19
|
-
it('should return true if the feature is enabled', async () => {
|
|
20
23
|
// @ts-expect-error test data
|
|
21
24
|
const result = await hasFeature(accountId, 'feature-1');
|
|
22
25
|
expect(result).toBe(true);
|
|
23
26
|
});
|
|
24
|
-
it('should return false if the feature is
|
|
27
|
+
it('should return false if the feature is disabled', async () => {
|
|
28
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
29
|
+
data: {
|
|
30
|
+
enabledFeatures: {
|
|
31
|
+
'feature-2': false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
25
35
|
// @ts-expect-error test data
|
|
26
36
|
const result = await hasFeature(accountId, 'feature-2');
|
|
27
37
|
expect(result).toBe(false);
|
|
28
38
|
});
|
|
29
39
|
it('should return false if the feature is not present', async () => {
|
|
40
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
41
|
+
data: {
|
|
42
|
+
enabledFeatures: {},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
30
45
|
// @ts-expect-error test data
|
|
31
46
|
const result = await hasFeature(accountId, 'feature-4');
|
|
32
47
|
expect(result).toBe(false);
|
|
33
48
|
});
|
|
49
|
+
it('should return true for APPS_HOME feature when not present in enabled features (defaults on)', async () => {
|
|
50
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
51
|
+
data: {
|
|
52
|
+
enabledFeatures: {},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const result = await hasFeature(accountId, FEATURES.APPS_HOME);
|
|
56
|
+
expect(result).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('should respect explicit setting for APPS_HOME feature even when it defaults on', async () => {
|
|
59
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
60
|
+
data: {
|
|
61
|
+
enabledFeatures: {
|
|
62
|
+
[FEATURES.APPS_HOME]: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const result = await hasFeature(accountId, FEATURES.APPS_HOME);
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('should handle truthy values correctly', async () => {
|
|
70
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
71
|
+
data: {
|
|
72
|
+
enabledFeatures: {
|
|
73
|
+
'feature-truthy': 'yes',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// @ts-expect-error test data
|
|
78
|
+
const truthyResult = await hasFeature(accountId, 'feature-truthy');
|
|
79
|
+
expect(truthyResult).toBe(true);
|
|
80
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
81
|
+
data: {
|
|
82
|
+
enabledFeatures: {
|
|
83
|
+
'feature-number': 1,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// @ts-expect-error test data
|
|
88
|
+
const numberResult = await hasFeature(accountId, 'feature-number');
|
|
89
|
+
expect(numberResult).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('should handle falsy values correctly', async () => {
|
|
92
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
93
|
+
data: {
|
|
94
|
+
enabledFeatures: {
|
|
95
|
+
'feature-null': null,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
// @ts-expect-error test data
|
|
100
|
+
const nullResult = await hasFeature(accountId, 'feature-null');
|
|
101
|
+
expect(nullResult).toBe(false);
|
|
102
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
103
|
+
data: {
|
|
104
|
+
enabledFeatures: {
|
|
105
|
+
'feature-zero': 0,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
// @ts-expect-error test data
|
|
110
|
+
const zeroResult = await hasFeature(accountId, 'feature-zero');
|
|
111
|
+
expect(zeroResult).toBe(false);
|
|
112
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
113
|
+
data: {
|
|
114
|
+
enabledFeatures: {
|
|
115
|
+
'feature-empty': '',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// @ts-expect-error test data
|
|
120
|
+
const emptyResult = await hasFeature(accountId, 'feature-empty');
|
|
121
|
+
expect(emptyResult).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('should propagate errors from fetchEnabledFeatures', async () => {
|
|
124
|
+
const error = new Error('API error');
|
|
125
|
+
mockedFetchEnabledFeatures.mockRejectedValueOnce(error);
|
|
126
|
+
await expect(hasFeature(accountId, FEATURES.UNIFIED_APPS)).rejects.toThrow('API error');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('hasUnfiedAppsAccess()', () => {
|
|
130
|
+
const accountId = 123;
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
vi.clearAllMocks();
|
|
133
|
+
});
|
|
134
|
+
it('should return true when API returns true', async () => {
|
|
135
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
136
|
+
mockedHttp.get.mockResolvedValueOnce({ data: true });
|
|
137
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
138
|
+
expect(result).toBe(true);
|
|
139
|
+
expect(mockedHttp.get).toHaveBeenCalledWith(accountId, {
|
|
140
|
+
url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('should return false when API returns false', async () => {
|
|
144
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
145
|
+
mockedHttp.get.mockResolvedValueOnce({ data: false });
|
|
146
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
147
|
+
expect(result).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
it('should handle truthy values correctly', async () => {
|
|
150
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
151
|
+
mockedHttp.get.mockResolvedValueOnce({ data: 'yes' });
|
|
152
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
153
|
+
expect(result).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('should handle falsy values correctly', async () => {
|
|
156
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
157
|
+
mockedHttp.get.mockResolvedValueOnce({ data: null });
|
|
158
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
159
|
+
expect(result).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('should handle undefined response data', async () => {
|
|
162
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
163
|
+
mockedHttp.get.mockResolvedValueOnce({ data: undefined });
|
|
164
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
165
|
+
expect(result).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
it('should propagate errors from http.get', async () => {
|
|
168
|
+
const error = new Error('Network error');
|
|
169
|
+
mockedHttp.get.mockRejectedValueOnce(error);
|
|
170
|
+
await expect(hasUnfiedAppsAccess(accountId)).rejects.toThrow('Network error');
|
|
171
|
+
});
|
|
34
172
|
});
|
|
35
173
|
});
|