@hubspot/cli 7.7.35-experimental.0 → 7.8.0-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.
Files changed (61) hide show
  1. package/bin/cli.js +2 -0
  2. package/commands/getStarted.d.ts +1 -1
  3. package/commands/getStarted.js +16 -64
  4. package/commands/getStartedV2.d.ts +9 -0
  5. package/commands/getStartedV2.js +39 -0
  6. package/commands/project/create.js +1 -1
  7. package/commands/project/dev/unifiedFlow.js +1 -1
  8. package/commands/project/migrate.js +14 -4
  9. package/lang/en.d.ts +1 -1
  10. package/lang/en.js +1 -1
  11. package/lib/__tests__/hasFeature.test.js +7 -145
  12. package/lib/constants.d.ts +0 -1
  13. package/lib/constants.js +0 -1
  14. package/lib/dependencyManagement.d.ts +5 -0
  15. package/lib/dependencyManagement.js +9 -0
  16. package/lib/hasFeature.js +0 -6
  17. package/lib/mcp/setup.js +1 -1
  18. package/lib/projects/create/v3.js +2 -3
  19. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  20. package/lib/projects/localDev/helpers/project.js +6 -5
  21. package/lib/ui/SpinniesManager.js +8 -105
  22. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  23. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  24. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  25. package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -2
  26. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  27. package/mcp-server/tools/cms/HsListTool.js +1 -1
  28. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -1
  29. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
  30. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
  31. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
  32. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
  33. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  34. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  35. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  36. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  37. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  38. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  39. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  40. package/mcp-server/tools/project/DocsSearchTool.js +2 -2
  41. package/mcp-server/tools/project/GetConfigValuesTool.js +1 -1
  42. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  43. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  44. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  45. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  46. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  47. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  48. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  49. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
  50. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  51. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  52. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  53. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  54. package/mcp-server/tools/project/constants.d.ts +1 -1
  55. package/mcp-server/tools/project/constants.js +3 -9
  56. package/package.json +7 -2
  57. package/ui/components/Ascii.d.ts +10 -0
  58. package/ui/components/Ascii.js +11 -0
  59. package/ui/components/HorizontalSelectPrompt.js +1 -1
  60. package/ui/views/GetStarted.d.ts +7 -0
  61. package/ui/views/GetStarted.js +157 -0
package/bin/cli.js CHANGED
@@ -42,6 +42,7 @@ 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';
45
46
  function getTerminalWidth() {
46
47
  const width = yargs().terminalWidth();
47
48
  if (width >= 100)
@@ -92,6 +93,7 @@ const argv = yargs(process.argv.slice(2))
92
93
  type: 'boolean',
93
94
  })
94
95
  .check(performChecks)
96
+ .command(getStartedV2Command)
95
97
  .command(authCommand)
96
98
  .command(initCommand)
97
99
  .command(logsCommand)
@@ -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
- type GetStartedArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
4
+ export type GetStartedArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
5
5
  name?: string;
6
6
  dest?: string;
7
7
  };
@@ -4,7 +4,6 @@ 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';
8
7
  import { EXIT_CODES } from '../lib/enums/exitCodes.js';
9
8
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
10
9
  import { promptUser } from '../lib/prompts/promptUtils.js';
@@ -28,8 +27,9 @@ export const describe = undefined;
28
27
  async function handler(args) {
29
28
  const { derivedAccountId } = args;
30
29
  const env = getEnv(derivedAccountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD;
31
- await trackCommandUsage('get-started', {}, derivedAccountId);
32
30
  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,8 +52,6 @@ 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);
57
55
  if (selectedOption === GET_STARTED_OPTIONS.CMS) {
58
56
  uiLogger.log(' ');
59
57
  uiLogger.log(commands.getStarted.designManager);
@@ -66,11 +64,6 @@ async function handler(args) {
66
64
  message: commands.getStarted.openDesignManagerPrompt,
67
65
  },
68
66
  ]);
69
- // Track Design Manager browser action
70
- await trackCommandMetadataUsage('get-started', {
71
- step: 'open-design-manager',
72
- type: shouldOpen ? 'opened' : 'declined',
73
- }, derivedAccountId);
74
67
  if (shouldOpen) {
75
68
  uiLogger.log('');
76
69
  openLink(derivedAccountId, 'design-manager');
@@ -81,37 +74,36 @@ async function handler(args) {
81
74
  else {
82
75
  uiLogger.log(' ');
83
76
  uiLogger.log(commands.getStarted.logs.appSelected);
77
+ // 1. Fetch project templates
78
+ let latestRepoReleaseTag;
84
79
  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
85
87
  const projectDest = path.resolve(getCwd(), dest);
86
88
  const { projectConfig: existingProjectConfig, projectDir: existingProjectDir, } = await getProjectConfig(projectDest);
87
89
  if (existingProjectConfig &&
88
90
  existingProjectDir &&
89
91
  projectDest.startsWith(existingProjectDir)) {
90
- // Track nested project error
91
- await trackCommandMetadataUsage('get-started', {
92
- successful: false,
93
- step: 'project-creation',
94
- }, derivedAccountId);
95
92
  uiLogger.log(' ');
96
93
  uiLogger.error(commands.project.create.errors.cannotNestProjects(existingProjectDir));
97
94
  process.exit(EXIT_CODES.ERROR);
98
95
  }
96
+ const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
99
97
  // 4. Clone the project template from GitHub
98
+ // This is temporary until we have the UA template in the main repo
100
99
  try {
101
- await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDest, {
102
- sourceDir: '2025.2/private-app-get-started-template',
100
+ await cloneGithubRepo(repo, projectDest, {
101
+ sourceDir: projectTemplate.path,
102
+ tag: latestRepoReleaseTag,
103
103
  hideLogs: true,
104
104
  });
105
- await trackCommandMetadataUsage('get-started', {
106
- successful: true,
107
- step: 'github-clone',
108
- }, derivedAccountId);
109
105
  }
110
106
  catch (err) {
111
- await trackCommandMetadataUsage('get-started', {
112
- successful: false,
113
- step: 'github-clone',
114
- }, derivedAccountId);
115
107
  debugError(err);
116
108
  uiLogger.log(' ');
117
109
  uiLogger.error(commands.project.create.errors.failedToDownloadProject);
@@ -130,11 +122,6 @@ async function handler(args) {
130
122
  uiLogger.log(' ');
131
123
  uiLogger.log(commands.getStarted.prompts.projectCreated.description);
132
124
  uiLogger.log(' ');
133
- // Track successful project creation
134
- await trackCommandMetadataUsage('get-started', {
135
- successful: true,
136
- step: 'project-creation',
137
- }, derivedAccountId);
138
125
  // 5. Install dependencies
139
126
  const installLocations = await getProjectPackageJsonLocations(projectDest);
140
127
  try {
@@ -160,21 +147,11 @@ async function handler(args) {
160
147
  default: true,
161
148
  },
162
149
  ]);
163
- // Track upload decision
164
- await trackCommandMetadataUsage('get-started', {
165
- step: 'upload-decision',
166
- type: shouldUpload ? 'upload' : 'skip',
167
- }, derivedAccountId);
168
150
  if (shouldUpload) {
169
151
  try {
170
152
  // Get the project config for the newly created project
171
153
  const { projectConfig: newProjectConfig, projectDir: newProjectDir } = await getProjectConfig(projectDest);
172
154
  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);
178
155
  uiLogger.log(' ');
179
156
  uiLogger.error(commands.getStarted.errors.configFileNotFound);
180
157
  process.exit(EXIT_CODES.ERROR);
@@ -195,21 +172,11 @@ async function handler(args) {
195
172
  skipValidation: false,
196
173
  });
197
174
  if (uploadError) {
198
- // Track upload failure
199
- await trackCommandMetadataUsage('get-started', {
200
- successful: false,
201
- step: 'upload',
202
- }, derivedAccountId);
203
175
  uiLogger.log(' ');
204
176
  uiLogger.error(commands.getStarted.errors.uploadFailed);
205
177
  debugError(uploadError);
206
178
  }
207
179
  else if (result) {
208
- // Track successful upload completion
209
- await trackCommandMetadataUsage('get-started', {
210
- successful: true,
211
- step: 'upload',
212
- }, derivedAccountId);
213
180
  uiLogger.log(' ');
214
181
  uiLogger.success(commands.getStarted.logs.uploadSuccess);
215
182
  const { data: { results }, } = await fetchPublicAppsForPortal(derivedAccountId);
@@ -225,11 +192,6 @@ async function handler(args) {
225
192
  message: commands.getStarted.openInstallUrl,
226
193
  },
227
194
  ]);
228
- // Track Developer Overview browser action
229
- await trackCommandMetadataUsage('get-started', {
230
- step: 'open-distribution-page',
231
- type: shouldOpenOverview ? 'opened' : 'declined',
232
- }, derivedAccountId);
233
195
  if (shouldOpenOverview) {
234
196
  open(getStaticAuthAppInstallUrl({
235
197
  targetAccountId: derivedAccountId,
@@ -245,11 +207,6 @@ async function handler(args) {
245
207
  }
246
208
  }
247
209
  catch (err) {
248
- // Track upload exception
249
- await trackCommandMetadataUsage('get-started', {
250
- successful: false,
251
- step: 'upload',
252
- }, derivedAccountId);
253
210
  uiLogger.log(' ');
254
211
  uiLogger.error(commands.getStarted.errors.uploadFailed);
255
212
  debugError(err);
@@ -257,11 +214,6 @@ async function handler(args) {
257
214
  }
258
215
  }
259
216
  }
260
- // Track successful completion of get-started command
261
- await trackCommandMetadataUsage('get-started', {
262
- successful: true,
263
- step: 'command-completed',
264
- }, derivedAccountId);
265
217
  process.exit(EXIT_CODES.SUCCESS);
266
218
  }
267
219
  function getStartedBuilder(yargs) {
@@ -0,0 +1,9 @@
1
+ import { AccountArgs, YargsCommandModule, CommonArgs, ConfigArgs, EnvironmentArgs } from '../types/Yargs.js';
2
+ export declare const command = "get-started-v2";
3
+ export declare const describe: undefined;
4
+ export type GetStartedArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
5
+ name?: string;
6
+ dest?: string;
7
+ };
8
+ declare const getStartedV2Command: YargsCommandModule<unknown, GetStartedArgs>;
9
+ export default getStartedV2Command;
@@ -0,0 +1,39 @@
1
+ import { commands } from '../lang/en.js';
2
+ import { makeYargsBuilder } from '../lib/yargsUtils.js';
3
+ import { getGetStarted } from '../ui/views/GetStarted.js';
4
+ import { render } from 'ink';
5
+ export const command = 'get-started-v2';
6
+ export const describe = undefined;
7
+ async function handler(args) {
8
+ render(getGetStarted({ args }));
9
+ }
10
+ function getStartedBuilder(yargs) {
11
+ yargs.options({
12
+ name: {
13
+ describe: commands.getStarted.options.name.describe,
14
+ type: 'string',
15
+ },
16
+ dest: {
17
+ describe: commands.getStarted.options.dest.describe,
18
+ type: 'string',
19
+ },
20
+ 'template-source': {
21
+ describe: commands.getStarted.options.templateSource.describe,
22
+ type: 'string',
23
+ },
24
+ });
25
+ return yargs;
26
+ }
27
+ const builder = makeYargsBuilder(getStartedBuilder, command, commands.getStarted.verboseDescribe, {
28
+ useGlobalOptions: true,
29
+ useAccountOptions: true,
30
+ useConfigOptions: true,
31
+ useEnvironmentOptions: true,
32
+ });
33
+ const getStartedV2Command = {
34
+ command,
35
+ describe,
36
+ handler,
37
+ builder,
38
+ };
39
+ export default getStartedV2Command;
@@ -125,7 +125,7 @@ function projectCreateBuilder(yargs) {
125
125
  hidden: true,
126
126
  type: 'string',
127
127
  choices: [v2023_2, v2025_1, v2025_2],
128
- default: v2025_2,
128
+ default: v2023_2,
129
129
  },
130
130
  'project-base': {
131
131
  describe: commands.project.create.options.projectBase.describe,
@@ -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, args.profile);
112
+ await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes);
113
113
  }
114
114
  else {
115
115
  project = await createNewProjectForLocalDev(projectConfig, targetProjectAccountId, false, false);
@@ -8,6 +8,8 @@ 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 { renderInline } from '../../ui/index.js';
12
+ import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
11
13
  const { v2025_2 } = PLATFORM_VERSIONS;
12
14
  const command = 'migrate';
13
15
  const describe = undefined; // commands.project.migrate.describe
@@ -19,10 +21,18 @@ async function handler(args) {
19
21
  return process.exit(EXIT_CODES.ERROR);
20
22
  }
21
23
  if (projectConfig?.projectConfig) {
22
- await logInBox({
23
- contents: lib.migrate.projectMigrationWarning,
24
- options: { title: lib.migrate.projectMigrationWarningTitle },
25
- });
24
+ if (!process.env.ENABLE_INK) {
25
+ await logInBox({
26
+ contents: lib.migrate.projectMigrationWarning,
27
+ options: { title: lib.migrate.projectMigrationWarningTitle },
28
+ });
29
+ }
30
+ else {
31
+ await renderInline(getWarningBox({
32
+ title: lib.migrate.projectMigrationWarningTitle,
33
+ message: lib.migrate.projectMigrationWarning,
34
+ }));
35
+ }
26
36
  }
27
37
  const { derivedAccountId } = args;
28
38
  try {
package/lang/en.d.ts CHANGED
@@ -2601,7 +2601,7 @@ export declare const lib: {
2601
2601
  readonly checking: "Checking if your deployed build is up to date...";
2602
2602
  readonly upToDate: "Deployed build is up to date.";
2603
2603
  readonly notUpToDate: "Your project contains undeployed local changes.";
2604
- readonly notUpToDateExplanation: (profile?: string) => string;
2604
+ readonly notUpToDateExplanation: `Run ${string} to upload these changes to HubSpot, then re-run ${string} to continue local development.`;
2605
2605
  };
2606
2606
  readonly createNewProjectForLocalDev: {
2607
2607
  readonly projectMustExistExplanation: (projectName: string, accountId: number) => string;
package/lang/en.js CHANGED
@@ -2598,7 +2598,7 @@ export const lib = {
2598
2598
  checking: 'Checking if your deployed build is up to date...',
2599
2599
  upToDate: 'Deployed build is up to date.',
2600
2600
  notUpToDate: `Your project contains undeployed local changes.`,
2601
- 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.`,
2601
+ notUpToDateExplanation: `Run ${uiCommandReference('hs project upload')} to upload these changes to HubSpot, then re-run ${uiCommandReference('hs project dev')} to continue local development.`,
2602
2602
  },
2603
2603
  createNewProjectForLocalDev: {
2604
2604
  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.`,
@@ -1,173 +1,35 @@
1
1
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
2
- import { http } from '@hubspot/local-dev-lib/http';
3
- import { hasFeature, hasUnfiedAppsAccess } from '../hasFeature.js';
4
- import { FEATURES } from '../constants.js';
2
+ import { hasFeature } from '../hasFeature.js';
5
3
  vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
6
- vi.mock('@hubspot/local-dev-lib/http');
7
4
  const mockedFetchEnabledFeatures = fetchEnabledFeatures;
8
- const mockedHttp = http;
9
5
  describe('lib/hasFeature', () => {
10
6
  describe('hasFeature()', () => {
11
7
  const accountId = 123;
12
- afterEach(() => {
13
- vi.clearAllMocks();
14
- });
15
- it('should return true if the feature is enabled', async () => {
8
+ beforeEach(() => {
16
9
  mockedFetchEnabledFeatures.mockResolvedValueOnce({
17
10
  data: {
18
11
  enabledFeatures: {
19
12
  'feature-1': true,
13
+ 'feature-2': false,
14
+ 'feature-3': true,
20
15
  },
21
16
  },
22
17
  });
18
+ });
19
+ it('should return true if the feature is enabled', async () => {
23
20
  // @ts-expect-error test data
24
21
  const result = await hasFeature(accountId, 'feature-1');
25
22
  expect(result).toBe(true);
26
23
  });
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
- });
24
+ it('should return false if the feature is not enabled', async () => {
35
25
  // @ts-expect-error test data
36
26
  const result = await hasFeature(accountId, 'feature-2');
37
27
  expect(result).toBe(false);
38
28
  });
39
29
  it('should return false if the feature is not present', async () => {
40
- mockedFetchEnabledFeatures.mockResolvedValueOnce({
41
- data: {
42
- enabledFeatures: {},
43
- },
44
- });
45
30
  // @ts-expect-error test data
46
31
  const result = await hasFeature(accountId, 'feature-4');
47
32
  expect(result).toBe(false);
48
33
  });
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
- });
172
34
  });
173
35
  });
@@ -81,7 +81,6 @@ export declare const FEATURES: {
81
81
  readonly SANDBOXES_V2: "sandboxes:v2:enabled";
82
82
  readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
83
83
  readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
84
- readonly APPS_HOME: "UIE:AppHome";
85
84
  };
86
85
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
87
86
  UPLOAD_SUCCESS: string;
package/lib/constants.js CHANGED
@@ -73,7 +73,6 @@ export const FEATURES = {
73
73
  SANDBOXES_V2: 'sandboxes:v2:enabled',
74
74
  SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
75
75
  APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
76
- APPS_HOME: 'UIE:AppHome',
77
76
  };
78
77
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
79
78
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -2,5 +2,10 @@ export declare function installPackages({ packages, installLocations, }: {
2
2
  packages?: string[];
3
3
  installLocations?: string[];
4
4
  }): Promise<void>;
5
+ export declare function installPackagesV2({ packages, installLocations, }: {
6
+ packages?: string[];
7
+ installLocations?: string[];
8
+ }): Promise<void>;
9
+ export declare function installPackagesInDirectoryV2(directory: string, packages?: string[]): Promise<void>;
5
10
  export declare function getProjectPackageJsonLocations(dir?: string): Promise<string[]>;
6
11
  export declare function hasMissingPackages(directory: string): Promise<boolean>;
@@ -22,6 +22,15 @@ export async function installPackages({ packages, installLocations, }) {
22
22
  await installPackagesInDirectory(dir, packages);
23
23
  }));
24
24
  }
25
+ export async function installPackagesV2({ packages, installLocations, }) {
26
+ const installDirs = installLocations || (await getProjectPackageJsonLocations());
27
+ await Promise.all(installDirs.map(async (dir) => {
28
+ await installPackagesInDirectoryV2(dir, packages);
29
+ }));
30
+ }
31
+ export async function installPackagesInDirectoryV2(directory, packages) {
32
+ await executeInstall(packages, null, { cwd: directory });
33
+ }
25
34
  async function installPackagesInDirectory(directory, packages) {
26
35
  const spinner = `installingDependencies-${directory}`;
27
36
  const relativeDir = path.relative(process.cwd(), directory);
package/lib/hasFeature.js CHANGED
@@ -1,13 +1,7 @@
1
1
  import { http } from '@hubspot/local-dev-lib/http';
2
2
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
3
- import { FEATURES } from './constants.js';
4
- const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
5
3
  export async function hasFeature(accountId, feature) {
6
4
  const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
7
- if (enabledFeatures[feature] === undefined &&
8
- FEATURES_THAT_DEFAULT_ON.includes(feature)) {
9
- return true;
10
- }
11
5
  return Boolean(enabledFeatures[feature]);
12
6
  }
13
7
  export async function hasUnfiedAppsAccess(accountId) {
package/lib/mcp/setup.js CHANGED
@@ -8,7 +8,7 @@ import path from 'path';
8
8
  import os from 'os';
9
9
  import fs from 'fs-extra';
10
10
  import { existsSync } from 'fs';
11
- const mcpServerName = 'HubSpot';
11
+ const mcpServerName = 'hubspot-cli-mcp';
12
12
  const claudeCode = 'claude';
13
13
  const windsurf = 'windsurf';
14
14
  const cursor = 'cursor';
@@ -9,7 +9,7 @@ import { getConfigForPlatformVersion } from './legacy.js';
9
9
  import { logError } from '../../errorHandlers/index.js';
10
10
  import { EXIT_CODES } from '../../enums/exitCodes.js';
11
11
  import { hasFeature } from '../../hasFeature.js';
12
- import { AppEventsKey, PagesKey, } from '@hubspot/project-parsing-lib/src/lib/constants.js';
12
+ import { AppEventsKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
13
13
  export async function createV3App(providedAuth, providedDistribution) {
14
14
  let authType;
15
15
  if (providedAuth &&
@@ -51,7 +51,6 @@ export async function createV3App(providedAuth, providedDistribution) {
51
51
  }
52
52
  const componentTypeToGateMap = {
53
53
  [AppEventsKey]: FEATURES.APP_EVENTS,
54
- [PagesKey]: FEATURES.APPS_HOME,
55
54
  };
56
55
  export async function calculateComponentTemplateChoices(components, authType, distribution, accountId, projectMetadata) {
57
56
  const enabledComponents = [];
@@ -89,7 +88,7 @@ export async function calculateComponentTemplateChoices(components, authType, di
89
88
  }
90
89
  if (disabledReasons.length > 0) {
91
90
  disabledComponents.push({
92
- name: `[${chalk.yellow('DISABLED')}] ${template.label} -`,
91
+ name: `[${chalk.yellow('DISABLED')}] ${template.label}`,
93
92
  value: template,
94
93
  disabled: disabledReasons.join(' '),
95
94
  });
@@ -6,7 +6,7 @@ export declare function createNewProjectForLocalDev(projectConfig: ProjectConfig
6
6
  export declare function createInitialBuildForNewProject(projectConfig: ProjectConfig, projectDir: string, targetAccountId: number, sendIR?: boolean, profile?: string): Promise<Build>;
7
7
  export declare function compareLocalProjectToDeployed(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number | undefined, localProjectNodes: {
8
8
  [key: string]: IntermediateRepresentationNodeLocalDev;
9
- }, profile?: string): Promise<void>;
9
+ }): Promise<void>;
10
10
  export declare function isDeployedProjectUpToDateWithLocal(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, localProjectNodes: {
11
11
  [key: string]: IntermediateRepresentationNodeLocalDev;
12
- }, profile?: string): Promise<boolean>;
12
+ }): Promise<boolean>;