@hubspot/cli 7.7.29-experimental.0 → 7.7.31-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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import yargs from 'yargs';
2
+ import { addAccountOptions, addConfigOptions, addUseEnvironmentOptions, addGlobalOptions, addJSONOutputOptions, } from '../../../lib/commonOpts.js';
3
+ import installCommand from '../install.js';
4
+ vi.mock('../../../lib/commonOpts');
5
+ describe('commands/app/install', () => {
6
+ const yargsMock = yargs;
7
+ describe('command', () => {
8
+ it('should have the correct command structure', () => {
9
+ expect(installCommand.command).toEqual('install <test-account-id>');
10
+ });
11
+ });
12
+ describe('describe', () => {
13
+ it('should provide a description', () => {
14
+ expect(installCommand.describe).not.toBeDefined();
15
+ });
16
+ });
17
+ describe('builder', () => {
18
+ it('should support the correct options', () => {
19
+ installCommand.builder(yargsMock);
20
+ expect(addGlobalOptions).toHaveBeenCalledTimes(1);
21
+ expect(addGlobalOptions).toHaveBeenCalledWith(yargsMock);
22
+ expect(addAccountOptions).toHaveBeenCalledTimes(1);
23
+ expect(addAccountOptions).toHaveBeenCalledWith(yargsMock);
24
+ expect(addConfigOptions).toHaveBeenCalledTimes(1);
25
+ expect(addConfigOptions).toHaveBeenCalledWith(yargsMock);
26
+ expect(addUseEnvironmentOptions).toHaveBeenCalledTimes(1);
27
+ expect(addUseEnvironmentOptions).toHaveBeenCalledWith(yargsMock);
28
+ expect(addJSONOutputOptions).toHaveBeenCalledTimes(1);
29
+ expect(addJSONOutputOptions).toHaveBeenCalledWith(yargsMock);
30
+ expect(yargsMock.positional).toHaveBeenCalledTimes(1);
31
+ expect(yargsMock.positional).toHaveBeenCalledWith('test-account-id', expect.objectContaining({
32
+ type: 'number',
33
+ required: true,
34
+ describe: expect.any(String),
35
+ }));
36
+ expect(yargsMock.option).toHaveBeenCalledTimes(2);
37
+ expect(yargsMock.option).toHaveBeenCalledWith('app-uid', expect.objectContaining({
38
+ type: 'string',
39
+ describe: expect.any(String),
40
+ }));
41
+ expect(yargsMock.option).toHaveBeenCalledWith('project-name', expect.objectContaining({
42
+ type: 'string',
43
+ describe: expect.any(String),
44
+ }));
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,8 @@
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, YargsCommandModule, JSONOutputArgs } from '../../types/Yargs.js';
2
+ type InstallAppArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs & {
3
+ appUid?: string;
4
+ projectName?: string;
5
+ testAccountId: number;
6
+ };
7
+ declare const installAppCommand: YargsCommandModule<unknown, InstallAppArgs>;
8
+ export default installAppCommand;
@@ -0,0 +1,122 @@
1
+ import { fetchDeveloperTestAccountOauthAppInstallStatus, installOauthAppIntoDeveloperTestAccount, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
2
+ import { trackCommandUsage } from '../../lib/usageTracking.js';
3
+ import { commands } from '../../lang/en.js';
4
+ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
5
+ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
6
+ import { APP_AUTH_TYPES } from '../../lib/constants.js';
7
+ import { uiLogger } from '../../lib/ui/logger.js';
8
+ import SpinniesManager from '../../lib/ui/SpinniesManager.js';
9
+ import { poll } from '../../lib/polling.js';
10
+ import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
11
+ import { handleTranslate } from '../../lib/projects/upload.js';
12
+ import { isAppIRNode } from '../../lib/projects/structure.js';
13
+ import { logError } from '../../lib/errorHandlers/index.js';
14
+ const command = 'install <test-account-id>';
15
+ const describe = undefined; // commands.app.subcommands.install.describe;
16
+ async function handler(args) {
17
+ const { derivedAccountId, appUid, projectName, testAccountId, formatOutputAsJson, } = args;
18
+ trackCommandUsage('app-install', {}, derivedAccountId);
19
+ const jsonOutput = {};
20
+ let targetProjectName = projectName;
21
+ let targetAppUid = appUid;
22
+ const { projectConfig, projectDir } = await getProjectConfig();
23
+ if (!targetProjectName) {
24
+ validateProjectConfig(projectConfig, projectDir);
25
+ targetProjectName = projectConfig?.name;
26
+ }
27
+ if (!targetProjectName) {
28
+ uiLogger.error(commands.app.subcommands.install.errors.mustSpecifyProjectName);
29
+ process.exit(EXIT_CODES.ERROR);
30
+ }
31
+ let isAppOauth = true;
32
+ if (!targetAppUid) {
33
+ const intermediateRepresentation = await handleTranslate(projectDir, projectConfig, derivedAccountId, true, undefined);
34
+ if (intermediateRepresentation) {
35
+ Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).forEach(node => {
36
+ if (isAppIRNode(node)) {
37
+ targetAppUid = node.uid;
38
+ isAppOauth = node.config.auth.type === APP_AUTH_TYPES.OAUTH;
39
+ }
40
+ });
41
+ }
42
+ }
43
+ if (!targetAppUid) {
44
+ uiLogger.error(commands.app.subcommands.install.errors.noAppUidFound);
45
+ process.exit(EXIT_CODES.ERROR);
46
+ }
47
+ if (!isAppOauth) {
48
+ uiLogger.error(commands.app.subcommands.install.errors.appMustBeOauth);
49
+ process.exit(EXIT_CODES.ERROR);
50
+ }
51
+ try {
52
+ const { data } = await installOauthAppIntoDeveloperTestAccount(derivedAccountId, testAccountId, targetProjectName, targetAppUid);
53
+ if (data?.authCodes.length > 0) {
54
+ jsonOutput.authCode = data.authCodes[0].authCode;
55
+ }
56
+ }
57
+ catch (err) {
58
+ logError(err);
59
+ process.exit(EXIT_CODES.ERROR);
60
+ }
61
+ SpinniesManager.init({
62
+ succeedColor: 'white',
63
+ });
64
+ SpinniesManager.add('installApp', {
65
+ text: commands.app.subcommands.install.polling.start,
66
+ });
67
+ let appInstallSucceeded = false;
68
+ try {
69
+ await poll(() => fetchDeveloperTestAccountOauthAppInstallStatus(derivedAccountId, targetProjectName, targetAppUid), {
70
+ successStates: ['SUCCESS'],
71
+ errorStates: [],
72
+ });
73
+ appInstallSucceeded = true;
74
+ }
75
+ catch (err) {
76
+ SpinniesManager.fail('installApp');
77
+ logError(err);
78
+ process.exit(EXIT_CODES.ERROR);
79
+ }
80
+ if (!appInstallSucceeded) {
81
+ SpinniesManager.fail('installApp');
82
+ process.exit(EXIT_CODES.ERROR);
83
+ }
84
+ SpinniesManager.succeed('installApp', {
85
+ text: commands.app.subcommands.install.polling.success,
86
+ });
87
+ if (formatOutputAsJson) {
88
+ uiLogger.json(jsonOutput);
89
+ }
90
+ process.exit(EXIT_CODES.SUCCESS);
91
+ }
92
+ function installAppBuilder(yargs) {
93
+ yargs.positional('test-account-id', {
94
+ describe: commands.app.subcommands.install.positionals.testAccountId,
95
+ required: true,
96
+ type: 'number',
97
+ });
98
+ yargs.option('app-uid', {
99
+ describe: commands.app.subcommands.install.options.appUid,
100
+ type: 'string',
101
+ });
102
+ yargs.option('project-name', {
103
+ describe: commands.app.subcommands.install.options.projectName,
104
+ type: 'string',
105
+ });
106
+ yargs.example('install 1234567890 --app-uid=my-app-uid --project-name=my-project', commands.app.subcommands.install.example);
107
+ return yargs;
108
+ }
109
+ const builder = makeYargsBuilder(installAppBuilder, command, commands.app.subcommands.install.describe, {
110
+ useGlobalOptions: true,
111
+ useAccountOptions: true,
112
+ useConfigOptions: true,
113
+ useEnvironmentOptions: true,
114
+ useJSONOutputOptions: true,
115
+ });
116
+ const installAppCommand = {
117
+ command,
118
+ describe,
119
+ handler,
120
+ builder,
121
+ };
122
+ export default installAppCommand;
package/commands/app.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import migrateCommand from './app/migrate.js';
2
2
  import appSecretCommand from './app/secret.js';
3
+ import installAppCommand from './app/install.js';
3
4
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
4
5
  const command = ['app', 'apps'];
5
6
  // Keep the command hidden for now
6
7
  const describe = undefined;
7
8
  function appBuilder(yargs) {
8
- yargs.command(migrateCommand).command(appSecretCommand).demandCommand(1, '');
9
+ yargs
10
+ .command(migrateCommand)
11
+ .command(appSecretCommand)
12
+ .command(installAppCommand)
13
+ .demandCommand(1, '');
9
14
  return yargs;
10
15
  }
11
16
  const builder = makeYargsBuilder(appBuilder, command, describe);
@@ -1,7 +1,22 @@
1
1
  import yargs from 'yargs';
2
2
  import projectAddCommand from '../add.js';
3
3
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../../lib/constants.js';
4
+ import { v3AddComponent } from '../../../lib/projects/add/v3AddComponent.js';
5
+ import { legacyAddComponent } from '../../../lib/projects/add/legacyAddComponent.js';
6
+ import { getProjectConfig } from '../../../lib/projects/config.js';
7
+ import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
8
+ import { trackCommandUsage } from '../../../lib/usageTracking.js';
4
9
  vi.mock('../../../lib/commonOpts');
10
+ vi.mock('../../../lib/projects/add/v3AddComponent');
11
+ vi.mock('../../../lib/projects/add/legacyAddComponent');
12
+ vi.mock('../../../lib/projects/config');
13
+ vi.mock('../../../lib/projects/buildAndDeploy');
14
+ vi.mock('../../../lib/usageTracking');
15
+ const mockedV3AddComponent = vi.mocked(v3AddComponent);
16
+ const mockedLegacyAddComponent = vi.mocked(legacyAddComponent);
17
+ const mockedGetProjectConfig = vi.mocked(getProjectConfig);
18
+ const mockedUseV3Api = vi.mocked(useV3Api);
19
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
5
20
  describe('commands/project/add', () => {
6
21
  const yargsMock = yargs;
7
22
  describe('command', () => {
@@ -40,4 +55,55 @@ describe('commands/project/add', () => {
40
55
  });
41
56
  });
42
57
  });
58
+ describe('handler', () => {
59
+ const mockProjectConfig = {
60
+ name: 'test-project',
61
+ srcDir: 'src',
62
+ platformVersion: 'v3',
63
+ };
64
+ const mockProjectDir = '/path/to/project';
65
+ const mockArgs = {
66
+ derivedAccountId: 123,
67
+ name: 'test-component',
68
+ type: 'module',
69
+ };
70
+ beforeEach(() => {
71
+ mockedGetProjectConfig.mockResolvedValue({
72
+ projectConfig: mockProjectConfig,
73
+ projectDir: mockProjectDir,
74
+ });
75
+ mockedTrackCommandUsage.mockResolvedValue();
76
+ mockedV3AddComponent.mockResolvedValue();
77
+ mockedLegacyAddComponent.mockResolvedValue();
78
+ vi.spyOn(process, 'exit').mockImplementation(() => {
79
+ throw new Error('process.exit called');
80
+ });
81
+ });
82
+ it('should call v3AddComponent with accountId for v3 projects', async () => {
83
+ mockedUseV3Api.mockReturnValue(true);
84
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
85
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
86
+ expect(mockedV3AddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
87
+ expect(mockedLegacyAddComponent).not.toHaveBeenCalled();
88
+ });
89
+ it('should call legacyAddComponent for non-v3 projects', async () => {
90
+ mockedUseV3Api.mockReturnValue(false);
91
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
92
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
93
+ expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig);
94
+ expect(mockedV3AddComponent).not.toHaveBeenCalled();
95
+ });
96
+ it('should exit with error when project config is not found', async () => {
97
+ mockedGetProjectConfig.mockResolvedValue({
98
+ projectConfig: null,
99
+ projectDir: null,
100
+ });
101
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
102
+ throw new Error('process.exit called');
103
+ });
104
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
105
+ expect(mockExit).toHaveBeenCalledWith(1);
106
+ mockExit.mockRestore();
107
+ });
108
+ });
43
109
  });
@@ -1,5 +1,5 @@
1
1
  import { YargsCommandModule, CommonArgs } from '../../types/Yargs.js';
2
- type ProjectAddArgs = CommonArgs & {
2
+ export type ProjectAddArgs = CommonArgs & {
3
3
  type?: string;
4
4
  name?: string;
5
5
  features?: string[];
@@ -23,7 +23,7 @@ async function handler(args) {
23
23
  }
24
24
  const isV3ProjectCreate = useV3Api(projectConfig.platformVersion);
25
25
  if (isV3ProjectCreate) {
26
- await v3AddComponent(args, projectDir, projectConfig);
26
+ await v3AddComponent(args, projectDir, projectConfig, derivedAccountId);
27
27
  }
28
28
  else {
29
29
  await legacyAddComponent(args, projectDir, projectConfig);
@@ -41,7 +41,7 @@ async function handler(args) {
41
41
  type: selectProjectTemplatePromptResponse.projectTemplate?.name ||
42
42
  (selectProjectTemplatePromptResponse.componentTemplates || [])
43
43
  // @ts-expect-error
44
- .map((item) => item.label)
44
+ .map((item) => item.type)
45
45
  .join(','),
46
46
  }, derivedAccountId);
47
47
  const projectDest = path.resolve(getCwd(), projectNameAndDestPromptResponse.dest);
@@ -1,5 +1,5 @@
1
- import { CommonArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectUploadArgs = CommonArgs & JSONOutputArgs & {
1
+ import { CommonArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ type ProjectUploadArgs = CommonArgs & JSONOutputArgs & EnvironmentArgs & {
3
3
  forceCreate: boolean;
4
4
  message: string;
5
5
  m: string;
@@ -25,7 +25,7 @@ async function handler(args) {
25
25
  validateProjectConfig(projectConfig, projectDir);
26
26
  let targetAccountId;
27
27
  if (useV3Api(projectConfig.platformVersion)) {
28
- targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
28
+ targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile, args.useEnv);
29
29
  }
30
30
  targetAccountId = targetAccountId || derivedAccountId;
31
31
  const accountConfig = getAccountConfig(targetAccountId);
@@ -75,6 +75,9 @@ async function handler(args) {
75
75
  resultJson.personalAccessKey = createResult.personalAccessKey;
76
76
  }
77
77
  catch (err) {
78
+ SpinniesManager.fail('createTestAccount', {
79
+ text: commands.testAccount.create.polling.createFailure,
80
+ });
78
81
  logError(err);
79
82
  SpinniesManager.fail('createTestAccount', {
80
83
  text: commands.testAccount.create.polling.createFailure,
package/lang/en.d.ts CHANGED
@@ -884,12 +884,10 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
884
884
  readonly configuringCursor: "Configuring Cursor...";
885
885
  readonly failedToConfigureCursor: "Failed to configure Cursor";
886
886
  readonly configuredCursor: "Configured Cursor";
887
- readonly cursorNotFound: "Cursor not found - skipping configuration";
888
887
  readonly alreadyInstalled: "HubSpot CLI mcp server already installed, reinstalling";
889
888
  readonly configuringWindsurf: "Configuring Windsurf...";
890
889
  readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
891
890
  readonly configuredWindsurf: "Configured Windsurf";
892
- readonly windsurfNotFound: "Windsurf not found - skipping configuration";
893
891
  readonly configuringVsCode: "Configuring VSCode...";
894
892
  readonly failedToConfigureVsCode: "Failed to configure VSCode";
895
893
  readonly configuredVsCode: "Configured VSCode";
@@ -1552,6 +1550,28 @@ ${string}`;
1552
1550
  readonly app: {
1553
1551
  readonly describe: "Commands for managing apps.";
1554
1552
  readonly subcommands: {
1553
+ readonly install: {
1554
+ readonly describe: "Install an OAuth app into a test account.";
1555
+ readonly options: {
1556
+ readonly appUid: "The uid of the app to install";
1557
+ readonly projectName: "The name of the project that contains the app";
1558
+ };
1559
+ readonly positionals: {
1560
+ readonly testAccountId: "The id of the test account to install the app into";
1561
+ };
1562
+ readonly errors: {
1563
+ readonly mustSpecifyProjectName: `You must specify a project name. Use the ${string} flag to specify the project name or run this command from within a project directory.`;
1564
+ readonly noAppUidFound: `No app uid found. Please specify the app uid with the ${string} flag or run this command from within a project that contains an app.`;
1565
+ readonly appMustBeOauth: "This command only supports installing oauth apps. Please specify an app with oauth auth type.";
1566
+ };
1567
+ readonly polling: {
1568
+ readonly start: "Installing app...";
1569
+ readonly success: "App installed successfully";
1570
+ readonly failure: "App installation failed";
1571
+ readonly error: "Error installing app";
1572
+ };
1573
+ readonly example: "Install the app with uid my-app-uid from the project named \"my-project\" into the target account with id 1234567890";
1574
+ };
1555
1575
  readonly secret: {
1556
1576
  readonly describe: "Commands for managing secrets.";
1557
1577
  readonly subcommands: {
@@ -2571,6 +2591,8 @@ export declare const lib: {
2571
2591
  readonly activeInstallations: (appName: string, installCount: number) => string;
2572
2592
  readonly error: "An error occurred while checking installations for your app";
2573
2593
  };
2594
+ readonly distributionChanged: `Your app's distribution type has been changed from private to marketplace. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${string} change your app's distribution type. This will uninstall your app from all accounts.`;
2595
+ readonly authTypeChanged: `Your app's auth type has been changed from static to oauth. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${string} change your app's auth type. This will uninstall your app from all accounts.`;
2574
2596
  };
2575
2597
  readonly LocalDevWebsocketServer: {
2576
2598
  readonly errors: {
package/lang/en.js CHANGED
@@ -5,7 +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, } from '../lib/projects/urls.js';
8
- import { PROJECT_CONFIG_FILE, PROJECT_WITH_APP } from '../lib/constants.js';
8
+ import { APP_DISTRIBUTION_TYPES, APP_AUTH_TYPES, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
9
9
  import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
10
10
  export const commands = {
11
11
  generalErrors: {
@@ -889,13 +889,11 @@ export const commands = {
889
889
  configuringCursor: 'Configuring Cursor...',
890
890
  failedToConfigureCursor: 'Failed to configure Cursor',
891
891
  configuredCursor: 'Configured Cursor',
892
- cursorNotFound: 'Cursor not found - skipping configuration',
893
892
  alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
894
893
  // Windsurf
895
894
  configuringWindsurf: 'Configuring Windsurf...',
896
895
  failedToConfigureWindsurf: 'Failed to configure Windsurf',
897
896
  configuredWindsurf: 'Configured Windsurf',
898
- windsurfNotFound: 'Windsurf not found - skipping configuration',
899
897
  // VS Code
900
898
  configuringVsCode: 'Configuring VSCode...',
901
899
  failedToConfigureVsCode: 'Failed to configure VSCode',
@@ -1549,6 +1547,28 @@ export const commands = {
1549
1547
  app: {
1550
1548
  describe: 'Commands for managing apps.',
1551
1549
  subcommands: {
1550
+ install: {
1551
+ describe: 'Install an OAuth app into a test account.',
1552
+ options: {
1553
+ appUid: 'The uid of the app to install',
1554
+ projectName: 'The name of the project that contains the app',
1555
+ },
1556
+ positionals: {
1557
+ testAccountId: 'The id of the test account to install the app into',
1558
+ },
1559
+ errors: {
1560
+ mustSpecifyProjectName: `You must specify a project name. Use the ${uiCommandReference('--project-name')} flag to specify the project name or run this command from within a project directory.`,
1561
+ noAppUidFound: `No app uid found. Please specify the app uid with the ${uiCommandReference('--app-uid')} flag or run this command from within a project that contains an app.`,
1562
+ appMustBeOauth: 'This command only supports installing oauth apps. Please specify an app with oauth auth type.',
1563
+ },
1564
+ polling: {
1565
+ start: 'Installing app...',
1566
+ success: 'App installed successfully',
1567
+ failure: 'App installation failed',
1568
+ error: 'Error installing app',
1569
+ },
1570
+ example: 'Install the app with uid my-app-uid from the project named "my-project" into the target account with id 1234567890',
1571
+ },
1552
1572
  secret: {
1553
1573
  describe: 'Commands for managing secrets.',
1554
1574
  subcommands: {
@@ -2568,6 +2588,8 @@ export const lib = {
2568
2588
  activeInstallations: (appName, installCount) => `[WARNING] Your app ${chalk.bold(appName)} is installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}`,
2569
2589
  error: 'An error occurred while checking installations for your app',
2570
2590
  },
2591
+ distributionChanged: `Your app's distribution type has been changed from ${APP_DISTRIBUTION_TYPES.PRIVATE} to ${APP_DISTRIBUTION_TYPES.MARKETPLACE}. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${chalk.bold('permanantly')} change your app's distribution type. This will uninstall your app from all accounts.`,
2592
+ authTypeChanged: `Your app's auth type has been changed from ${APP_AUTH_TYPES.STATIC} to ${APP_AUTH_TYPES.OAUTH}. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${chalk.bold('permanantly')} change your app's auth type. This will uninstall your app from all accounts.`,
2571
2593
  },
2572
2594
  LocalDevWebsocketServer: {
2573
2595
  errors: {
@@ -16,8 +16,8 @@ interface McpCommand {
16
16
  args: string[];
17
17
  }
18
18
  export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
19
- export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
20
19
  export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
21
- export declare function setupCursor(mcpCommand?: McpCommand): Promise<boolean>;
22
- export declare function setupWindsurf(mcpCommand?: McpCommand): Promise<boolean>;
20
+ export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
21
+ export declare function setupCursor(mcpCommand?: McpCommand): boolean;
22
+ export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
23
23
  export {};
package/lib/mcp/setup.js CHANGED
@@ -4,6 +4,10 @@ import { promptUser } from '../prompts/promptUtils.js';
4
4
  import SpinniesManager from '../ui/SpinniesManager.js';
5
5
  import { logError } from '../errorHandlers/index.js';
6
6
  import { execAsync } from '../../mcp-server/utils/command.js';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import fs from 'fs-extra';
10
+ import { existsSync } from 'fs';
7
11
  const mcpServerName = 'hubspot-cli-mcp';
8
12
  const claudeCode = 'claude';
9
13
  const windsurf = 'windsurf';
@@ -68,6 +72,96 @@ async function runSetupFunction(func) {
68
72
  throw new Error();
69
73
  }
70
74
  }
75
+ function setupMcpConfigFile(config) {
76
+ try {
77
+ SpinniesManager.add('spinner', {
78
+ text: config.configuringMessage,
79
+ });
80
+ if (!existsSync(config.configPath)) {
81
+ fs.writeFileSync(config.configPath, JSON.stringify({}, null, 2));
82
+ }
83
+ let mcpConfig = {};
84
+ let configContent;
85
+ try {
86
+ configContent = fs.readFileSync(config.configPath, 'utf8');
87
+ }
88
+ catch (error) {
89
+ SpinniesManager.fail('spinner', {
90
+ text: config.failedMessage,
91
+ });
92
+ logError(error);
93
+ return false;
94
+ }
95
+ try {
96
+ // In the event the file exists, but is empty, initialize it to and empty object
97
+ if (configContent.trim() === '') {
98
+ mcpConfig = {};
99
+ }
100
+ else {
101
+ mcpConfig = JSON.parse(configContent);
102
+ }
103
+ }
104
+ catch (error) {
105
+ SpinniesManager.fail('spinner', {
106
+ text: config.failedMessage,
107
+ });
108
+ uiLogger.error(commands.mcp.setup.errors.errorParsingJsonFIle(config.configPath, error instanceof Error ? error.message : `${error}`));
109
+ return false;
110
+ }
111
+ // Initialize mcpServers if it doesn't exist
112
+ if (!mcpConfig.mcpServers) {
113
+ mcpConfig.mcpServers = {};
114
+ }
115
+ // Add or update HubSpot CLI MCP server
116
+ mcpConfig.mcpServers[mcpServerName] = {
117
+ ...config.mcpCommand,
118
+ };
119
+ // Write the updated config
120
+ fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
121
+ SpinniesManager.succeed('spinner', {
122
+ text: config.configuredMessage,
123
+ });
124
+ return true;
125
+ }
126
+ catch (error) {
127
+ SpinniesManager.fail('spinner', {
128
+ text: config.failedMessage,
129
+ });
130
+ logError(error);
131
+ return false;
132
+ }
133
+ }
134
+ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
135
+ try {
136
+ SpinniesManager.add('vsCode', {
137
+ text: commands.mcp.setup.spinners.configuringVsCode,
138
+ });
139
+ const mcpConfig = JSON.stringify({
140
+ name: mcpServerName,
141
+ ...buildCommandWithAgentString(mcpCommand, vscode),
142
+ });
143
+ await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
144
+ SpinniesManager.succeed('vsCode', {
145
+ text: commands.mcp.setup.spinners.configuredVsCode,
146
+ });
147
+ return true;
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error &&
151
+ error.message.includes('code: command not found')) {
152
+ SpinniesManager.fail('vsCode', {
153
+ text: commands.mcp.setup.spinners.vsCodeNotFound,
154
+ });
155
+ }
156
+ else {
157
+ SpinniesManager.fail('vsCode', {
158
+ text: commands.mcp.setup.spinners.failedToConfigureVsCode,
159
+ });
160
+ logError(error);
161
+ }
162
+ return false;
163
+ }
164
+ }
71
165
  export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
72
166
  try {
73
167
  SpinniesManager.add('claudeCode', {
@@ -88,7 +182,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
88
182
  });
89
183
  await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
90
184
  }
91
- await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
185
+ await execAsync(`claude mcp add-json "${mcpServerName}" '${mcpConfig}' --scope user`);
92
186
  SpinniesManager.succeed('claudeCode', {
93
187
  text: commands.mcp.setup.spinners.configuredClaudeCode,
94
188
  });
@@ -117,44 +211,25 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
117
211
  return false;
118
212
  }
119
213
  }
120
- async function setupVsCodeBasedIntegration(commandName, configuringText, configuredText, notFoundText, failedText, mcpCommand = defaultMcpCommand) {
121
- try {
122
- SpinniesManager.add(commandName, {
123
- text: configuringText,
124
- });
125
- const mcpConfig = JSON.stringify({
126
- name: mcpServerName,
127
- ...buildCommandWithAgentString(mcpCommand, vscode),
128
- });
129
- await execAsync(`${commandName} --add-mcp ${JSON.stringify(mcpConfig)}`);
130
- SpinniesManager.succeed(commandName, {
131
- text: configuredText,
132
- });
133
- return true;
134
- }
135
- catch (error) {
136
- if (error instanceof Error && error.message.includes(commandName)) {
137
- SpinniesManager.fail(commandName, {
138
- text: notFoundText,
139
- });
140
- }
141
- else {
142
- SpinniesManager.fail(commandName, {
143
- text: failedText,
144
- });
145
- logError(error);
146
- }
147
- return false;
148
- }
149
- }
150
- export async function setupVsCode(mcpCommand = defaultMcpCommand) {
151
- return setupVsCodeBasedIntegration('code', commands.mcp.setup.spinners.configuringVsCode, commands.mcp.setup.spinners.configuredVsCode, commands.mcp.setup.spinners.vsCodeNotFound, commands.mcp.setup.spinners.failedToConfigureVsCode, mcpCommand);
152
- }
153
- export async function setupCursor(mcpCommand = defaultMcpCommand) {
154
- return setupVsCodeBasedIntegration('cursor', commands.mcp.setup.spinners.configuringCursor, commands.mcp.setup.spinners.configuredCursor, commands.mcp.setup.spinners.cursorNotFound, commands.mcp.setup.spinners.failedToConfigureCursor, mcpCommand);
214
+ export function setupCursor(mcpCommand = defaultMcpCommand) {
215
+ const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
216
+ return setupMcpConfigFile({
217
+ configPath: cursorConfigPath,
218
+ configuringMessage: commands.mcp.setup.spinners.configuringCursor,
219
+ configuredMessage: commands.mcp.setup.spinners.configuredCursor,
220
+ failedMessage: commands.mcp.setup.spinners.failedToConfigureCursor,
221
+ mcpCommand: buildCommandWithAgentString(mcpCommand, cursor),
222
+ });
155
223
  }
156
- export async function setupWindsurf(mcpCommand = defaultMcpCommand) {
157
- return setupVsCodeBasedIntegration('windsurf', commands.mcp.setup.spinners.configuringWindsurf, commands.mcp.setup.spinners.configuredWindsurf, commands.mcp.setup.spinners.windsurfNotFound, commands.mcp.setup.spinners.failedToConfigureWindsurf, mcpCommand);
224
+ export function setupWindsurf(mcpCommand = defaultMcpCommand) {
225
+ const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
226
+ return setupMcpConfigFile({
227
+ configPath: windsurfConfigPath,
228
+ configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
229
+ configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
230
+ failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
231
+ mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
232
+ });
158
233
  }
159
234
  function buildCommandWithAgentString(mcpCommand, agent) {
160
235
  const mcpCommandCopy = structuredClone(mcpCommand);
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
5
  export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
6
6
  export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
7
- export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined, useEnv?: boolean): Promise<number | undefined>;
@@ -38,7 +38,12 @@ export function loadProfile(projectConfig, projectDir, profileName) {
38
38
  uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
39
39
  return;
40
40
  }
41
- return profile;
41
+ return {
42
+ ...profile,
43
+ accountId: process.env.HUBSPOT_ACCOUNT_ID
44
+ ? Number(process.env.HUBSPOT_ACCOUNT_ID)
45
+ : profile.accountId,
46
+ };
42
47
  }
43
48
  catch (e) {
44
49
  uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
@@ -54,7 +59,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
54
59
  }
55
60
  }
56
61
  }
57
- export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
62
+ export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile, useEnv = false) {
58
63
  if (argsProfile) {
59
64
  logProfileHeader(argsProfile);
60
65
  const profile = loadProfile(projectConfig, projectDir, argsProfile);
@@ -63,6 +68,9 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
63
68
  process.exit(EXIT_CODES.ERROR);
64
69
  }
65
70
  logProfileFooter(profile, true);
71
+ if (useEnv) {
72
+ return Number(process.env.HUBSPOT_ACCOUNT_ID);
73
+ }
66
74
  return profile.accountId;
67
75
  }
68
76
  else {
@@ -100,6 +100,7 @@ describe('AppDevModeInterface', () => {
100
100
  setAppDataForUid: vi.fn(),
101
101
  addListener: vi.fn(),
102
102
  addUploadWarning: vi.fn(),
103
+ removeListener: vi.fn(),
103
104
  };
104
105
  mockLocalDevLogger = {};
105
106
  // Mock constructors
@@ -387,6 +388,15 @@ describe('AppDevModeInterface', () => {
387
388
  await appDevModeInterface.cleanup();
388
389
  expect(UIEDevModeInterface.cleanup).toHaveBeenCalled();
389
390
  });
391
+ it('should remove state listeners', async () => {
392
+ await appDevModeInterface.cleanup();
393
+ expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('devServerMessage',
394
+ // @ts-expect-error access private method for testing
395
+ appDevModeInterface.onDevServerMessage);
396
+ expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('projectNodes',
397
+ // @ts-expect-error
398
+ appDevModeInterface.onChangeProjectNodes);
399
+ });
390
400
  });
391
401
  describe('isAutomaticallyInstallable()', () => {
392
402
  it('should return true for static auth app on test account with correct parent', () => {
@@ -7,6 +7,7 @@ import { projectAddPromptV3 } from '../../../prompts/projectAddPrompt.js';
7
7
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
8
8
  import { logger } from '@hubspot/local-dev-lib/logger';
9
9
  import { getProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
10
+ import { trackCommandUsage } from '../../../usageTracking.js';
10
11
  import { commands } from '../../../../lang/en.js';
11
12
  vi.mock('fs');
12
13
  vi.mock('../../../prompts/promptUtils');
@@ -16,6 +17,7 @@ vi.mock('../../../prompts/projectAddPrompt');
16
17
  vi.mock('@hubspot/local-dev-lib/github');
17
18
  vi.mock('@hubspot/local-dev-lib/logger');
18
19
  vi.mock('@hubspot/project-parsing-lib/src/lib/project');
20
+ vi.mock('../../../usageTracking');
19
21
  const mockedFs = vi.mocked(fs);
20
22
  const mockedGetConfigForPlatformVersion = vi.mocked(getConfigForPlatformVersion);
21
23
  const mockedConfirmPrompt = vi.mocked(confirmPrompt);
@@ -24,6 +26,7 @@ const mockedProjectAddPromptV3 = vi.mocked(projectAddPromptV3);
24
26
  const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
25
27
  const mockedLogger = vi.mocked(logger);
26
28
  const mockedGetProjectMetadata = vi.mocked(getProjectMetadata);
29
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
27
30
  describe('lib/projects/add/v3AddComponent', () => {
28
31
  const mockProjectConfig = {
29
32
  name: 'test-project',
@@ -32,6 +35,7 @@ describe('lib/projects/add/v3AddComponent', () => {
32
35
  };
33
36
  const mockArgs = { name: 'test-component', type: 'module' };
34
37
  const projectDir = '/path/to/project';
38
+ const mockAccountId = 123;
35
39
  const mockComponentTemplate = {
36
40
  label: 'Test Component',
37
41
  path: 'test-component',
@@ -62,6 +66,7 @@ describe('lib/projects/add/v3AddComponent', () => {
62
66
  authType: 'oauth',
63
67
  distribution: 'private',
64
68
  });
69
+ mockedTrackCommandUsage.mockResolvedValue();
65
70
  });
66
71
  describe('v3AddComponent()', () => {
67
72
  it('successfully adds a component when app already exists', async () => {
@@ -79,10 +84,13 @@ describe('lib/projects/add/v3AddComponent', () => {
79
84
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
80
85
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
81
86
  mockedCloneGithubRepo.mockResolvedValue(true);
82
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
87
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
83
88
  expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('v3');
84
89
  expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
85
90
  expect(mockedProjectAddPromptV3).toHaveBeenCalled();
91
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
92
+ type: 'module',
93
+ }, mockAccountId);
86
94
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
87
95
  sourceDir: ['v3/test-component'],
88
96
  hideLogs: true,
@@ -106,8 +114,11 @@ describe('lib/projects/add/v3AddComponent', () => {
106
114
  mockedConfirmPrompt.mockResolvedValue(true);
107
115
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
108
116
  mockedCloneGithubRepo.mockResolvedValue(true);
109
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
117
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
110
118
  expect(mockedCreateV3App).toHaveBeenCalled();
119
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
120
+ type: 'module',
121
+ }, mockAccountId);
111
122
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
112
123
  sourceDir: ['v3/test-component', 'v3/app-template'],
113
124
  }));
@@ -132,8 +143,11 @@ describe('lib/projects/add/v3AddComponent', () => {
132
143
  mockedConfirmPrompt.mockResolvedValue(true);
133
144
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
134
145
  mockedCloneGithubRepo.mockResolvedValue(true);
135
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
146
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
136
147
  expect(mockedCreateV3App).not.toHaveBeenCalled();
148
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
149
+ type: '',
150
+ }, mockAccountId);
137
151
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
138
152
  });
139
153
  it('throws an error when app count exceeds maximum', async () => {
@@ -146,7 +160,7 @@ describe('lib/projects/add/v3AddComponent', () => {
146
160
  };
147
161
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
148
162
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataMaxApps);
149
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('This project currently has the maximum number of apps: 1');
163
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
150
164
  });
151
165
  it('throws an error when components list is empty', async () => {
152
166
  const mockEmptyConfig = {
@@ -154,7 +168,7 @@ describe('lib/projects/add/v3AddComponent', () => {
154
168
  parentComponents: [],
155
169
  };
156
170
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockEmptyConfig);
157
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
171
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
158
172
  });
159
173
  it('throws an error when app meta file cannot be parsed', async () => {
160
174
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
@@ -162,7 +176,7 @@ describe('lib/projects/add/v3AddComponent', () => {
162
176
  mockedFs.readFileSync.mockImplementation(() => {
163
177
  throw new Error('File read error');
164
178
  });
165
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('Unable to parse app file');
179
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
166
180
  });
167
181
  it('throws an error when cloning fails', async () => {
168
182
  const mockAppMeta = {
@@ -179,7 +193,57 @@ describe('lib/projects/add/v3AddComponent', () => {
179
193
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
180
194
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
181
195
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
182
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
196
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
197
+ });
198
+ it('should track usage with multiple component types', async () => {
199
+ const mockAppMeta = {
200
+ config: {
201
+ distribution: 'private',
202
+ auth: { type: 'oauth' },
203
+ },
204
+ };
205
+ const mockSecondComponentTemplate = {
206
+ label: 'Test Card',
207
+ path: 'test-card',
208
+ type: 'card',
209
+ supportedAuthTypes: ['oauth'],
210
+ supportedDistributions: ['private'],
211
+ };
212
+ const mockPromptResponse = {
213
+ componentTemplate: [mockComponentTemplate, mockSecondComponentTemplate],
214
+ };
215
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
216
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
217
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
218
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
219
+ mockedCloneGithubRepo.mockResolvedValue(true);
220
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
221
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
222
+ type: 'module,card',
223
+ }, mockAccountId);
224
+ });
225
+ it('should track usage with empty type when no components are selected', async () => {
226
+ const mockProjectMetadataNoApps = {
227
+ hsMetaFiles: [],
228
+ components: {
229
+ app: {
230
+ count: 1,
231
+ maxCount: 1,
232
+ hsMetaFiles: ['/path/to/app.meta.json'],
233
+ },
234
+ module: { count: 0, maxCount: 5, hsMetaFiles: [] },
235
+ },
236
+ };
237
+ const mockPromptResponse = {
238
+ componentTemplate: [],
239
+ };
240
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
241
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
242
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
243
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
244
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
245
+ type: '',
246
+ }, mockAccountId);
183
247
  });
184
248
  });
185
249
  });
@@ -5,4 +5,4 @@ export declare function v3AddComponent(args: {
5
5
  features?: string[];
6
6
  auth?: string;
7
7
  distribution?: string;
8
- }, projectDir: string, projectConfig: ProjectConfig): Promise<void>;
8
+ }, projectDir: string, projectConfig: ProjectConfig, accountId: number): Promise<void>;
@@ -12,7 +12,8 @@ import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
12
12
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
13
13
  import { debugError } from '../../errorHandlers/index.js';
14
14
  import { uiLogger } from '../../ui/logger.js';
15
- export async function v3AddComponent(args, projectDir, projectConfig) {
15
+ import { trackCommandUsage } from '../../usageTracking.js';
16
+ export async function v3AddComponent(args, projectDir, projectConfig, accountId) {
16
17
  uiLogger.log(commands.project.add.creatingComponent(projectConfig.name));
17
18
  const config = await getConfigForPlatformVersion(projectConfig.platformVersion);
18
19
  const { components, parentComponents } = config;
@@ -47,6 +48,10 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
47
48
  }
48
49
  const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
49
50
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
51
+ const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.type);
52
+ await trackCommandUsage('project-add', {
53
+ type: componentTypes?.join(','),
54
+ }, accountId);
50
55
  try {
51
56
  const components = projectAddPromptResponse.componentTemplate?.map((componentTemplate) => {
52
57
  return path.join(projectConfig.platformVersion, componentTemplate.path);
@@ -11,6 +11,7 @@ declare class AppDevModeInterface {
11
11
  _appNode?: AppIRNode | null;
12
12
  marketplaceAppInstalls?: number;
13
13
  constructor(options: AppDevModeInterfaceConstructorOptions);
14
+ private getAppNodeFromProjectNodes;
14
15
  private get appNode();
15
16
  private get appData();
16
17
  private set appData(value);
@@ -21,7 +22,10 @@ declare class AppDevModeInterface {
21
22
  private autoInstallStaticAuthApp;
22
23
  private installAppOrOpenInstallUrl;
23
24
  private checkTestAccountAppInstallation;
24
- private setUpLocalDevServerMessageListeners;
25
+ private onDevServerMessage;
26
+ private onChangeProjectNodes;
27
+ private setUpStateListeners;
28
+ private removeStateListeners;
25
29
  setup(args: any): Promise<void>;
26
30
  start(): Promise<void>;
27
31
  fileChange(filePath: string, event: string): Promise<void>;
@@ -30,12 +30,13 @@ class AppDevModeInterface {
30
30
  process.exit(EXIT_CODES.ERROR);
31
31
  }
32
32
  }
33
+ getAppNodeFromProjectNodes(projectNodes) {
34
+ return Object.values(projectNodes).find(isAppIRNode) || null;
35
+ }
33
36
  // Assumes only one app per project
34
37
  get appNode() {
35
38
  if (this._appNode === undefined) {
36
- this._appNode =
37
- Object.values(this.localDevState.projectNodes).find(isAppIRNode) ||
38
- null;
39
+ this._appNode = this.getAppNodeFromProjectNodes(this.localDevState.projectNodes);
39
40
  }
40
41
  return this._appNode;
41
42
  }
@@ -175,12 +176,33 @@ class AppDevModeInterface {
175
176
  }
176
177
  return { needsInstall: !isInstalledWithScopeGroups, isReinstall };
177
178
  }
178
- setUpLocalDevServerMessageListeners() {
179
- this.localDevState.addListener('devServerMessage', message => {
180
- if (message === LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED) {
181
- this.checkTestAccountAppInstallation();
182
- }
183
- });
179
+ onDevServerMessage = (message) => {
180
+ if (message === LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED) {
181
+ this.checkTestAccountAppInstallation();
182
+ }
183
+ };
184
+ onChangeProjectNodes = (nodes) => {
185
+ const newAppNode = this.getAppNodeFromProjectNodes(nodes);
186
+ const oldDistribution = this.appNode?.config.distribution;
187
+ const newDistribution = newAppNode?.config.distribution;
188
+ const oldAuthType = this.appNode?.config.auth.type;
189
+ const newAuthType = newAppNode?.config.auth.type;
190
+ if (newDistribution === APP_DISTRIBUTION_TYPES.MARKETPLACE &&
191
+ oldDistribution !== APP_DISTRIBUTION_TYPES.MARKETPLACE) {
192
+ this.localDevState.addUploadWarning(lib.AppDevModeInterface.distributionChanged);
193
+ }
194
+ else if (newAuthType === APP_AUTH_TYPES.OAUTH &&
195
+ oldAuthType !== APP_AUTH_TYPES.OAUTH) {
196
+ this.localDevState.addUploadWarning(lib.AppDevModeInterface.authTypeChanged);
197
+ }
198
+ };
199
+ setUpStateListeners() {
200
+ this.localDevState.addListener('devServerMessage', this.onDevServerMessage);
201
+ this.localDevState.addListener('projectNodes', this.onChangeProjectNodes);
202
+ }
203
+ removeStateListeners() {
204
+ this.localDevState.removeListener('devServerMessage', this.onDevServerMessage);
205
+ this.localDevState.removeListener('projectNodes', this.onChangeProjectNodes);
184
206
  }
185
207
  // @ts-expect-error TODO: reconcile types between CLI and UIE Dev Server
186
208
  // In the future, update UIE Dev Server to use LocalDevState
@@ -215,7 +237,7 @@ class AppDevModeInterface {
215
237
  catch (e) {
216
238
  logError(e);
217
239
  }
218
- this.setUpLocalDevServerMessageListeners();
240
+ this.setUpStateListeners();
219
241
  return UIEDevModeInterface.setup(args);
220
242
  }
221
243
  async start() {
@@ -239,6 +261,7 @@ class AppDevModeInterface {
239
261
  if (!this.appNode) {
240
262
  return;
241
263
  }
264
+ this.removeStateListeners();
242
265
  return UIEDevModeInterface.cleanup();
243
266
  }
244
267
  }
@@ -1,5 +1,5 @@
1
1
  import { ComponentTypes, Component, GenericComponentConfig, PublicAppComponentConfig, PrivateAppComponentConfig, AppCardComponentConfig } from '../../types/Projects.js';
2
- import { IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/src/lib/types.js';
2
+ import { IntermediateRepresentationNode, IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/src/lib/types.js';
3
3
  import { AppIRNode } from '../../types/ProjectComponents.js';
4
4
  export declare const CONFIG_FILES: {
5
5
  [k in ComponentTypes]: string;
@@ -15,4 +15,4 @@ export declare function getProjectComponentTypes(components: Array<Component>):
15
15
  export declare function getComponentUid(component?: Component | null): string | null;
16
16
  export declare function componentIsApp(component?: Component | null): component is Component<PublicAppComponentConfig | PrivateAppComponentConfig>;
17
17
  export declare function componentIsPublicApp(component?: Component | null): component is Component<PublicAppComponentConfig>;
18
- export declare function isAppIRNode(component: IntermediateRepresentationNodeLocalDev): component is AppIRNode;
18
+ export declare function isAppIRNode(component: IntermediateRepresentationNodeLocalDev | IntermediateRepresentationNode): component is AppIRNode;
@@ -1,4 +1,5 @@
1
1
  import { FileResult } from 'tmp';
2
+ import { IntermediateRepresentation } from '@hubspot/project-parsing-lib';
2
3
  import { ProjectConfig } from '../../types/Projects.js';
3
4
  type ProjectUploadCallbackFunction<T> = (accountId: number, projectConfig: ProjectConfig, tempFile: FileResult, buildId: number) => Promise<T>;
4
5
  type ProjectUploadResult<T> = {
@@ -20,5 +21,5 @@ type HandleProjectUploadArg<T> = {
20
21
  export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
21
22
  export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig): void;
22
23
  export declare function validateNoHSMetaMismatch(srcDir: string, projectConfig: ProjectConfig): Promise<void>;
23
- export declare function handleTranslate(projectDir: string, projectConfig: ProjectConfig, accountId: number, skipValidation: boolean, profile: string | undefined): Promise<unknown>;
24
+ export declare function handleTranslate(projectDir: string, projectConfig: ProjectConfig, accountId: number, skipValidation: boolean, profile: string | undefined): Promise<IntermediateRepresentation | undefined>;
24
25
  export {};
@@ -133,4 +133,5 @@ export async function handleTranslate(projectDir, projectConfig, accountId, skip
133
133
  }
134
134
  throw e;
135
135
  }
136
+ return undefined;
136
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.7.29-experimental.0",
3
+ "version": "7.7.31-experimental.0",
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",
package/types/Yargs.d.ts CHANGED
@@ -15,7 +15,7 @@ export type AccountArgs = {
15
15
  account?: string;
16
16
  };
17
17
  export type EnvironmentArgs = {
18
- 'use-env'?: string;
18
+ 'use-env'?: boolean;
19
19
  };
20
20
  export type OverwriteArgs = Options & {
21
21
  o?: boolean;