@hubspot/cli 8.0.11-experimental.1 → 8.0.12-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 (115) hide show
  1. package/api/migrate.js +3 -3
  2. package/bin/cli.js +3 -0
  3. package/commands/account/link.d.ts +4 -0
  4. package/commands/account/link.js +88 -0
  5. package/commands/account/unlink.d.ts +4 -0
  6. package/commands/account/unlink.js +69 -0
  7. package/commands/account.js +4 -0
  8. package/commands/app/migrate.js +4 -4
  9. package/commands/getStarted.js +2 -2
  10. package/commands/project/add.js +3 -3
  11. package/commands/project/appInstallStatus.d.ts +4 -0
  12. package/commands/project/appInstallStatus.js +132 -0
  13. package/commands/project/create.js +18 -7
  14. package/commands/project/delete.js +2 -2
  15. package/commands/project/deploy.js +4 -3
  16. package/commands/project/dev/index.js +5 -4
  17. package/commands/project/info.js +2 -2
  18. package/commands/project/lint.js +2 -1
  19. package/commands/project/migrate.js +5 -5
  20. package/commands/project/profile/add.js +2 -2
  21. package/commands/project/profile/delete.js +2 -2
  22. package/commands/project/upload.d.ts +2 -0
  23. package/commands/project/upload.js +38 -6
  24. package/commands/project/validate.js +2 -2
  25. package/commands/project/watch.js +2 -2
  26. package/commands/project.js +8 -3
  27. package/commands/testAccount/create.js +4 -4
  28. package/lang/en.d.ts +95 -0
  29. package/lang/en.js +108 -8
  30. package/lib/app/migrate.js +8 -0
  31. package/lib/doctor/Doctor.js +2 -2
  32. package/lib/getStartedV2Actions.js +2 -2
  33. package/lib/link/accountTableUtils.d.ts +10 -0
  34. package/lib/link/accountTableUtils.js +39 -0
  35. package/lib/link/index.d.ts +14 -0
  36. package/lib/link/index.js +154 -0
  37. package/lib/link/linkUtils.d.ts +4 -0
  38. package/lib/link/linkUtils.js +24 -0
  39. package/lib/link/prompts.d.ts +7 -0
  40. package/lib/link/prompts.js +126 -0
  41. package/lib/link/renderLinkedAccountsTable.d.ts +2 -0
  42. package/lib/link/renderLinkedAccountsTable.js +14 -0
  43. package/lib/projects/ProjectLogsManager.js +6 -3
  44. package/lib/projects/create/index.js +2 -2
  45. package/lib/projects/create/legacy.js +2 -2
  46. package/lib/projects/create/v2.js +2 -2
  47. package/lib/projects/delete.js +2 -2
  48. package/lib/projects/deploy.d.ts +1 -1
  49. package/lib/projects/deploy.js +2 -2
  50. package/lib/projects/preview.d.ts +7 -0
  51. package/lib/projects/preview.js +71 -0
  52. package/lib/projects/uieLinting.d.ts +8 -3
  53. package/lib/projects/uieLinting.js +48 -27
  54. package/lib/projects/upload.d.ts +1 -0
  55. package/lib/projects/upload.js +8 -7
  56. package/lib/prompts/projectsLogsPrompt.js +3 -0
  57. package/lib/prompts/promptUtils.js +1 -0
  58. package/lib/theme/cmsDevServerProcess.js +1 -1
  59. package/mcp-server/Tool.d.ts +15 -0
  60. package/mcp-server/Tool.js +53 -0
  61. package/mcp-server/server.js +39 -3
  62. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +4 -2
  63. package/mcp-server/tools/cms/HsCreateFunctionTool.js +8 -6
  64. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -2
  65. package/mcp-server/tools/cms/HsCreateModuleTool.js +8 -6
  66. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +4 -2
  67. package/mcp-server/tools/cms/HsCreateTemplateTool.js +8 -6
  68. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -2
  69. package/mcp-server/tools/cms/HsFunctionLogsTool.js +8 -6
  70. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +4 -2
  71. package/mcp-server/tools/cms/HsListFunctionsTool.js +8 -6
  72. package/mcp-server/tools/cms/HsListTool.d.ts +4 -2
  73. package/mcp-server/tools/cms/HsListTool.js +8 -6
  74. package/mcp-server/tools/index.d.ts +3 -2
  75. package/mcp-server/tools/index.js +22 -22
  76. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +6 -4
  77. package/mcp-server/tools/project/AddFeatureToProjectTool.js +8 -6
  78. package/mcp-server/tools/project/CreateProjectTool.d.ts +6 -4
  79. package/mcp-server/tools/project/CreateProjectTool.js +9 -7
  80. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +4 -2
  81. package/mcp-server/tools/project/CreateTestAccountTool.js +20 -8
  82. package/mcp-server/tools/project/DeployProjectTool.d.ts +4 -2
  83. package/mcp-server/tools/project/DeployProjectTool.js +4 -6
  84. package/mcp-server/tools/project/DocFetchTool.d.ts +4 -2
  85. package/mcp-server/tools/project/DocFetchTool.js +8 -6
  86. package/mcp-server/tools/project/DocsSearchTool.d.ts +9 -3
  87. package/mcp-server/tools/project/DocsSearchTool.js +32 -9
  88. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +4 -2
  89. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +8 -6
  90. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +4 -2
  91. package/mcp-server/tools/project/GetApplicationInfoTool.js +8 -6
  92. package/mcp-server/tools/project/GetBuildLogsTool.d.ts +4 -2
  93. package/mcp-server/tools/project/GetBuildLogsTool.js +8 -6
  94. package/mcp-server/tools/project/GetBuildStatusTool.d.ts +4 -2
  95. package/mcp-server/tools/project/GetBuildStatusTool.js +8 -6
  96. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -2
  97. package/mcp-server/tools/project/GetConfigValuesTool.js +12 -7
  98. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +4 -2
  99. package/mcp-server/tools/project/GuidedWalkthroughTool.js +4 -6
  100. package/mcp-server/tools/project/UploadProjectTools.d.ts +4 -2
  101. package/mcp-server/tools/project/UploadProjectTools.js +9 -7
  102. package/mcp-server/tools/project/ValidateProjectTool.d.ts +4 -2
  103. package/mcp-server/tools/project/ValidateProjectTool.js +8 -6
  104. package/mcp-server/tools/project/constants.d.ts +2 -2
  105. package/mcp-server/types.d.ts +0 -7
  106. package/mcp-server/types.js +1 -13
  107. package/mcp-server/utils/logger.d.ts +10 -0
  108. package/mcp-server/utils/logger.js +29 -0
  109. package/mcp-server/utils/toolUsageTracking.js +0 -2
  110. package/package.json +10 -10
  111. package/types/Link.d.ts +27 -0
  112. package/types/Link.js +1 -0
  113. package/types/Prompts.d.ts +1 -0
  114. package/lib/projects/platformVersion.d.ts +0 -9
  115. package/lib/projects/platformVersion.js +0 -39
@@ -0,0 +1,126 @@
1
+ import { promptUser } from '../prompts/promptUtils.js';
2
+ import { uiAccountDescription } from '../ui/index.js';
3
+ import { uiLogger } from '../ui/logger.js';
4
+ import { commands } from '../../lang/en.js';
5
+ import { buildAccountRow, getNameColumnWidth, buildAccountHeader, sortDefaultFirst, } from './accountTableUtils.js';
6
+ import { Separator } from '@inquirer/prompts';
7
+ function buildColumnarChoices(accounts, localDefaultAccount) {
8
+ const rows = accounts.map(a => ({
9
+ ...buildAccountRow(a.accountId, a.accountId === localDefaultAccount),
10
+ disabled: a.disabled,
11
+ checked: a.checked,
12
+ hint: a.hint,
13
+ }));
14
+ const nameWidth = getNameColumnWidth(rows);
15
+ const header = buildAccountHeader(nameWidth);
16
+ return [
17
+ new Separator(header),
18
+ ...rows.map(row => {
19
+ const label = `${row.name.padEnd(nameWidth)} ${row.accountId}`;
20
+ return {
21
+ name: row.hint ? `${label} ${row.hint}` : label,
22
+ short: uiAccountDescription(Number(row.accountId), false),
23
+ value: Number(row.accountId),
24
+ disabled: row.disabled,
25
+ checked: row.checked,
26
+ };
27
+ }),
28
+ ];
29
+ }
30
+ function mapLinkAccountChoices(eligibleAccounts, inEligibleAccounts, accountOverrideId, localDefaultAccount, preselectedAccountId) {
31
+ const sortedIneligible = sortDefaultFirst(inEligibleAccounts, localDefaultAccount);
32
+ const accounts = [
33
+ ...eligibleAccounts.map(a => ({
34
+ accountId: a.accountId,
35
+ disabled: false,
36
+ checked: a.accountId === accountOverrideId ||
37
+ a.accountId === preselectedAccountId,
38
+ hint: a.accountId === accountOverrideId
39
+ ? commands.account.subcommands.link.prompts.fromHsAccount
40
+ : a.accountId === preselectedAccountId
41
+ ? commands.account.subcommands.link.prompts.newlyAuthenticated
42
+ : undefined,
43
+ })),
44
+ ...sortedIneligible.map(a => ({
45
+ accountId: a.accountId,
46
+ disabled: commands.account.subcommands.link.prompts.alreadyLinked,
47
+ checked: false,
48
+ })),
49
+ ];
50
+ return buildColumnarChoices(accounts, localDefaultAccount);
51
+ }
52
+ export async function promptForAction(state) {
53
+ const isSettingsEmpty = state.accounts.length === 0 && state.localDefaultAccount === undefined;
54
+ const { accountEditOption } = await promptUser({
55
+ type: 'list',
56
+ name: 'accountEditOption',
57
+ message: isSettingsEmpty
58
+ ? commands.account.subcommands.link.prompts.howToProceed
59
+ : commands.account.subcommands.link.prompts.whatToDo,
60
+ choices: [
61
+ {
62
+ name: commands.account.subcommands.link.prompts.linkExisting,
63
+ value: 'link',
64
+ },
65
+ {
66
+ name: commands.account.subcommands.link.prompts.authenticateNew,
67
+ value: 'authenticate',
68
+ },
69
+ {
70
+ name: commands.account.subcommands.link.prompts.cancel,
71
+ value: 'cancel',
72
+ },
73
+ ],
74
+ });
75
+ uiLogger.log('');
76
+ return accountEditOption;
77
+ }
78
+ export async function promptForDefaultAccount(accounts, currentDefaultAccount, prompt = '') {
79
+ const choiceAccounts = accounts.map(accountId => ({
80
+ accountId,
81
+ }));
82
+ const choices = buildColumnarChoices(choiceAccounts, currentDefaultAccount);
83
+ const { defaultAccount } = await promptUser({
84
+ type: 'list',
85
+ name: 'defaultAccount',
86
+ pageSize: 20,
87
+ message: prompt || commands.account.subcommands.link.prompts.selectDefault,
88
+ choices,
89
+ default: currentDefaultAccount ?? undefined,
90
+ });
91
+ uiLogger.log('');
92
+ return defaultAccount;
93
+ }
94
+ export async function promptForAccountsToLink(context, eligibleAccounts, inEligibleAccounts, localDefaultAccount) {
95
+ const { accountsToAdd } = await promptUser({
96
+ type: 'checkbox',
97
+ name: 'accountsToAdd',
98
+ pageSize: 20,
99
+ message: commands.account.subcommands.link.prompts.selectToLink,
100
+ choices: mapLinkAccountChoices(eligibleAccounts, inEligibleAccounts, context.accountOverrideId, localDefaultAccount, context.preselectedAccountId),
101
+ validate: (answer) => {
102
+ if (answer.length === 0) {
103
+ return commands.account.subcommands.link.prompts.mustSelectOne;
104
+ }
105
+ return true;
106
+ },
107
+ });
108
+ uiLogger.log('');
109
+ return accountsToAdd;
110
+ }
111
+ export async function promptForAccountsToUnlink(accounts, localDefaultAccount) {
112
+ const sortedAccounts = sortDefaultFirst(accounts, localDefaultAccount);
113
+ const choiceAccounts = sortedAccounts.map(accountId => ({
114
+ accountId,
115
+ }));
116
+ const choices = buildColumnarChoices(choiceAccounts, localDefaultAccount);
117
+ const { accountsToRemove } = await promptUser({
118
+ type: 'checkbox',
119
+ name: 'accountsToRemove',
120
+ pageSize: 20,
121
+ message: commands.account.subcommands.link.prompts.selectToUnlink,
122
+ choices,
123
+ });
124
+ uiLogger.log('');
125
+ return accountsToRemove;
126
+ }
@@ -0,0 +1,2 @@
1
+ import { HsSettingsFile } from '@hubspot/local-dev-lib/types/HsSettings';
2
+ export declare function renderLinkedAccountsTable(settings: HsSettingsFile): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { commands } from '../../lang/en.js';
2
+ import { renderTable } from '../../ui/render.js';
3
+ import { buildAccountRow, sortDefaultFirst } from './accountTableUtils.js';
4
+ export async function renderLinkedAccountsTable(settings) {
5
+ const labels = commands.account.subcommands.list.labels;
6
+ const tableHeader = [labels.name, labels.accountId];
7
+ const sortedAccounts = sortDefaultFirst(settings.accounts, settings.localDefaultAccount);
8
+ const tableData = sortedAccounts.map(accountId => {
9
+ const isDefault = accountId === settings.localDefaultAccount;
10
+ const row = buildAccountRow(accountId, isDefault);
11
+ return [row.name, row.accountId];
12
+ });
13
+ await renderTable(tableHeader, tableData, true);
14
+ }
@@ -4,7 +4,7 @@ import { fetchProjectComponentsMetadata } from '@hubspot/local-dev-lib/api/proje
4
4
  import { fetchAppMetadataBySourceId } from '@hubspot/local-dev-lib/api/appsDev';
5
5
  import { uiLogger } from '../ui/logger.js';
6
6
  import { commands } from '../../lang/en.js';
7
- import { isV2Project } from './platformVersion.js';
7
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
8
8
  import { getDeployedProjectNodes } from './localDev/helpers/project.js';
9
9
  import { debugError } from '../errorHandlers/index.js';
10
10
  class _ProjectLogsManager {
@@ -51,7 +51,7 @@ class _ProjectLogsManager {
51
51
  throw new Error(commands.project.logs.errors.failedToFetchProjectDetails);
52
52
  }
53
53
  this.projectId = project.id;
54
- if (isV2Project(projectConfig.platformVersion)) {
54
+ if (!isLegacyProject(projectConfig.platformVersion)) {
55
55
  const deployedBuildId = project.deployedBuild.buildId;
56
56
  if (!deployedBuildId) {
57
57
  throw new Error(commands.project.logs.errors.noDeployedBuild);
@@ -134,9 +134,12 @@ class _ProjectLogsManager {
134
134
  return this.functions.map(serverlessFunction => serverlessFunction.componentName);
135
135
  }
136
136
  setFunction(functionName) {
137
- if (!functionName || this.functions.length === 0) {
137
+ if (this.functions.length === 0) {
138
138
  throw new Error(commands.project.logs.errors.noFunctionsInProject);
139
139
  }
140
+ if (!functionName) {
141
+ throw new Error(commands.project.logs.errors.functionNameRequired);
142
+ }
140
143
  this.selectedFunction = this.functions.find(serverlessFunction => serverlessFunction.componentName === functionName);
141
144
  if (!this.selectedFunction) {
142
145
  throw new Error(commands.project.logs.errors.noFunctionWithName(functionName));
@@ -1,7 +1,7 @@
1
1
  import { selectProjectTemplatePrompt, } from '../../prompts/selectProjectTemplatePrompt.js';
2
2
  import { projectNameAndDestPrompt } from '../../prompts/projectNameAndDestPrompt.js';
3
3
  import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, EMPTY_PROJECT, } from '../../constants.js';
4
- import { isV2Project } from '../platformVersion.js';
4
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
5
5
  import { v2ComponentFlow } from './v2.js';
6
6
  import { getProjectTemplateListFromRepo } from './legacy.js';
7
7
  import { commands } from '../../../lang/en.js';
@@ -9,7 +9,7 @@ export async function handleProjectCreationFlow(args) {
9
9
  const { platformVersion, templateSource, projectBase, auth: providedAuth, distribution: providedDistribution, } = args;
10
10
  const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
11
11
  const projectNameAndDestPromptResponse = await projectNameAndDestPrompt(args);
12
- if (isV2Project(platformVersion)) {
12
+ if (!isLegacyProject(platformVersion)) {
13
13
  const { componentTemplateChoices, authType, distribution, repoConfig, projectContents, } = await v2ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, args.derivedAccountId);
14
14
  const selectProjectTemplatePromptResponse = await selectProjectTemplatePrompt(args, undefined, projectContents !== EMPTY_PROJECT ? componentTemplateChoices : undefined);
15
15
  return {
@@ -1,13 +1,13 @@
1
1
  import { fetchRepoFile } from '@hubspot/local-dev-lib/api/github';
2
2
  import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_COMPONENT_TYPES, } from '../../constants.js';
3
3
  import { debugError } from '../../errorHandlers/index.js';
4
- import { isV2Project } from '../platformVersion.js';
4
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
5
5
  import { lib } from '../../../lang/en.js';
6
6
  const PROJECT_TEMPLATE_PROPERTIES = ['name', 'label', 'path'];
7
7
  export const EMPTY_PROJECT_TEMPLATE_NAME = 'no-template';
8
8
  export async function getConfigForPlatformVersion(platformVersion) {
9
9
  let path = '';
10
- if (isV2Project(platformVersion)) {
10
+ if (!isLegacyProject(platformVersion)) {
11
11
  path = `${platformVersion}/`;
12
12
  }
13
13
  const { data } = await fetchRepoFile(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, `${path}config.json`, DEFAULT_PROJECT_TEMPLATE_BRANCH);
@@ -2,7 +2,7 @@ import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_
2
2
  import { commands, lib } from '../../../lang/en.js';
3
3
  import { listPrompt } from '../../prompts/promptUtils.js';
4
4
  import { APP_EVENTS_KEY as AppEventsKey } from '@hubspot/project-parsing-lib/constants';
5
- import { isV2Project } from '../platformVersion.js';
5
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
6
6
  import path from 'path';
7
7
  import { getConfigForPlatformVersion } from './legacy.js';
8
8
  import { hasFeature } from '../../hasFeature.js';
@@ -134,7 +134,7 @@ export async function v2ComponentFlow(platformVersion, projectBase, providedAuth
134
134
  };
135
135
  }
136
136
  export function generateComponentPaths({ selectProjectTemplatePromptResponse, platformVersion, repoConfig, projectContents, authType, distribution, }) {
137
- if (!isV2Project(platformVersion)) {
137
+ if (isLegacyProject(platformVersion)) {
138
138
  return [];
139
139
  }
140
140
  const components = selectProjectTemplatePromptResponse.componentTemplates?.map((componentTemplate) => {
@@ -10,7 +10,7 @@ import { PromptExitError } from '../errors/PromptExitError.js';
10
10
  import { EXIT_CODES } from '../enums/exitCodes.js';
11
11
  import { AUTO_GENERATED_COMPONENT_TYPES } from '@hubspot/project-parsing-lib/constants';
12
12
  import { mapToUserFacingType } from '@hubspot/project-parsing-lib/transform';
13
- import { isV2Project } from './platformVersion.js';
13
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
14
14
  import { COMPONENT_TYPES, SUBCOMPONENT_TYPES, } from '@hubspot/local-dev-lib/enums/build';
15
15
  export const DELETION_POLL_TIMEOUT_MS = 5 * 60 * 1000;
16
16
  export const DELETION_DEPLOY_SUCCESS_STATES = [
@@ -81,7 +81,7 @@ export async function checkDeployedComponents(accountId, projectName) {
81
81
  if (!platformVersion) {
82
82
  throw new Error(commands.project.delete.errors.noPlatformVersion);
83
83
  }
84
- if (!isV2Project(platformVersion)) {
84
+ if (isLegacyProject(platformVersion)) {
85
85
  const userVisibleComponents = [];
86
86
  projectData.deployedBuild?.subbuildStatuses?.forEach(item => {
87
87
  if (LEGACY_COMPONENTS_TO_FILTER.includes(item.buildType)) {
@@ -10,4 +10,4 @@ export declare function logDeployErrors(errorData: {
10
10
  };
11
11
  }>;
12
12
  }): void;
13
- export declare function handleProjectDeploy(targetAccountId: number, projectName: string, buildId: number, isV2Project: boolean, force: boolean): Promise<Deploy | undefined>;
13
+ export declare function handleProjectDeploy(targetAccountId: number, projectName: string, buildId: number, isLegacyProject: boolean, force: boolean): Promise<Deploy | undefined>;
@@ -47,9 +47,9 @@ function handleBlockedDeploy(deployResp) {
47
47
  uiLogger.log('');
48
48
  });
49
49
  }
50
- export async function handleProjectDeploy(targetAccountId, projectName, buildId, isV2Project, force) {
50
+ export async function handleProjectDeploy(targetAccountId, projectName, buildId, isLegacyProject, force) {
51
51
  let deployId;
52
- if (isV2Project) {
52
+ if (!isLegacyProject) {
53
53
  const { data: deployResp } = await deployProjectV2(targetAccountId, projectName, buildId, force);
54
54
  if (!deployResp || deployResp.buildResultType !== 'DEPLOY_QUEUED') {
55
55
  if (deployResp?.buildResultType === 'DEPLOY_BLOCKED') {
@@ -0,0 +1,7 @@
1
+ type PreviewResult = {
2
+ succeeded: boolean;
3
+ releaseTag?: string;
4
+ appId?: number;
5
+ };
6
+ export declare function triggerAndPollPreview(accountId: number, projectId: number, buildId: number, targetPortalId: number): Promise<PreviewResult>;
7
+ export {};
@@ -0,0 +1,71 @@
1
+ import { triggerAutoRelease, getAutoReleaseStatus, } from '@hubspot/local-dev-lib/api/projects';
2
+ import { DEFAULT_POLLING_DELAY } from '../constants.js';
3
+ import SpinniesManager from '../ui/SpinniesManager.js';
4
+ import { logError, ApiErrorContext } from '../errorHandlers/index.js';
5
+ import { lib } from '../../lang/en.js';
6
+ const PREVIEW_POLL_TIMEOUT = 5 * 60 * 1000;
7
+ export async function triggerAndPollPreview(accountId, projectId, buildId, targetPortalId) {
8
+ let triggerResponse;
9
+ SpinniesManager.add('preview', {
10
+ text: lib.projectPreview.triggeringPreview(buildId, targetPortalId),
11
+ succeedColor: 'white',
12
+ });
13
+ try {
14
+ const { data } = await triggerAutoRelease(accountId, projectId, buildId, targetPortalId);
15
+ triggerResponse = data;
16
+ }
17
+ catch (e) {
18
+ SpinniesManager.fail('preview', {
19
+ text: lib.projectPreview.triggerFailed,
20
+ });
21
+ logError(e, new ApiErrorContext({
22
+ accountId,
23
+ request: 'preview trigger',
24
+ }));
25
+ return { succeeded: false };
26
+ }
27
+ const { releaseTag, appId } = triggerResponse;
28
+ SpinniesManager.update('preview', {
29
+ text: lib.projectPreview.pollingStatus(releaseTag, targetPortalId),
30
+ });
31
+ try {
32
+ await pollPreviewStatus(accountId, projectId, targetPortalId, releaseTag, appId);
33
+ }
34
+ catch (e) {
35
+ SpinniesManager.fail('preview', {
36
+ text: lib.projectPreview.pollFailed,
37
+ });
38
+ logError(e, new ApiErrorContext({
39
+ accountId,
40
+ request: 'preview status',
41
+ }));
42
+ return { succeeded: false, releaseTag, appId };
43
+ }
44
+ SpinniesManager.succeed('preview', {
45
+ text: lib.projectPreview.succeeded(releaseTag, targetPortalId),
46
+ });
47
+ return { succeeded: true, releaseTag, appId };
48
+ }
49
+ function pollPreviewStatus(accountId, projectId, targetPortalId, expectedReleaseTag, appId) {
50
+ return new Promise((resolve, reject) => {
51
+ const startTime = Date.now();
52
+ const pollInterval = setInterval(async () => {
53
+ try {
54
+ const { data } = await getAutoReleaseStatus(accountId, projectId, targetPortalId, expectedReleaseTag, appId);
55
+ if (data.status === 'COMPLETE') {
56
+ clearInterval(pollInterval);
57
+ resolve();
58
+ return;
59
+ }
60
+ if (Date.now() - startTime >= PREVIEW_POLL_TIMEOUT) {
61
+ clearInterval(pollInterval);
62
+ reject(new Error(lib.projectPreview.timeout));
63
+ }
64
+ }
65
+ catch (e) {
66
+ clearInterval(pollInterval);
67
+ reject(e);
68
+ }
69
+ }, DEFAULT_POLLING_DELAY);
70
+ });
71
+ }
@@ -1,8 +1,13 @@
1
1
  export declare const REQUIRED_PACKAGES_AND_MIN_VERSIONS: {
2
2
  readonly eslint: "9.0.0";
3
- readonly '@typescript-eslint/eslint-plugin': "8.46.4";
4
- readonly '@typescript-eslint/parser': "8.46.4";
3
+ readonly '@eslint/js': "9.0.0";
5
4
  readonly 'typescript-eslint': "8.46.4";
5
+ readonly '@hubspot/eslint-config-ui-extensions': "1.0.0";
6
+ readonly 'eslint-config-prettier': "10.0.0";
7
+ readonly 'eslint-plugin-react': "7.0.0";
8
+ readonly 'eslint-plugin-react-hooks': "7.0.0";
9
+ readonly 'eslint-plugin-unused-imports': "4.0.0";
10
+ readonly prettier: "3.0.0";
6
11
  readonly jiti: "2.6.1";
7
12
  };
8
13
  export declare function isEslintInstalled(directory: string): boolean;
@@ -13,7 +18,7 @@ export declare function getMissingLintPackages(directory: string): {
13
18
  export declare function hasEslintConfig(directory: string): boolean;
14
19
  export declare function hasDeprecatedEslintConfig(directory: string): boolean;
15
20
  export declare function getDeprecatedEslintConfigFiles(directory: string): string[];
16
- export declare function createEslintConfig(directory: string): string;
21
+ export declare function createEslintConfig(directory: string, platformVersion?: string | null): Promise<string>;
17
22
  export declare function lintPackagesInDirectory(directory: string, projectDir?: string): Promise<{
18
23
  success: boolean;
19
24
  output: string;
@@ -3,17 +3,27 @@ import path from 'path';
3
3
  import util from 'util';
4
4
  import semver from 'semver';
5
5
  import { exec as execAsync } from 'node:child_process';
6
+ import { fetchRepoFile } from '@hubspot/local-dev-lib/api/github';
6
7
  import { getProjectPackageJsonLocations, isPackageInstalled, } from '../dependencyManagement.js';
7
8
  import { commands } from '../../lang/en.js';
8
9
  import { uiLogger } from '../ui/logger.js';
9
10
  import { safeGetPackageJsonCached } from '../npm/packageJson.js';
11
+ import { debugError } from '../errorHandlers/index.js';
12
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
+ import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../constants.js';
10
14
  export const REQUIRED_PACKAGES_AND_MIN_VERSIONS = {
11
15
  eslint: '9.0.0',
12
- '@typescript-eslint/eslint-plugin': '8.46.4',
13
- '@typescript-eslint/parser': '8.46.4',
16
+ '@eslint/js': '9.0.0',
14
17
  'typescript-eslint': '8.46.4',
18
+ '@hubspot/eslint-config-ui-extensions': '1.0.0',
19
+ 'eslint-config-prettier': '10.0.0',
20
+ 'eslint-plugin-react': '7.0.0',
21
+ 'eslint-plugin-react-hooks': '7.0.0',
22
+ 'eslint-plugin-unused-imports': '4.0.0',
23
+ prettier: '3.0.0',
15
24
  jiti: '2.6.1',
16
25
  };
26
+ const UIE_ESLINT_CONFIG_PATH_IN_REPO = 'components/cards/src/app/cards/eslint.config.js';
17
27
  const ESLINT_CONFIG_FILES = [
18
28
  'eslint.config.mts',
19
29
  'eslint.config.ts',
@@ -30,28 +40,6 @@ const DEPRECATED_ESLINT_CONFIG_FILES = [
30
40
  '.eslintrc.json',
31
41
  '.eslintrc',
32
42
  ];
33
- const ESLINT_CONFIG_TEMPLATE = `import { defineConfig } from "eslint/config";
34
- import tsParser from "@typescript-eslint/parser";
35
-
36
- export default defineConfig([
37
- {
38
- files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
39
- languageOptions: {
40
- parser: tsParser,
41
- parserOptions: {
42
- ecmaVersion: "latest",
43
- sourceType: "module",
44
- ecmaFeatures: {
45
- jsx: true
46
- }
47
- }
48
- },
49
- rules: {
50
- "no-console": ["warn", { allow: ["warn", "error"] }]
51
- }
52
- },
53
- ]);
54
- `;
55
43
  function getPackageVersionFromPackageJson(directory, packageName) {
56
44
  const packageJsonPath = path.join(directory, 'package.json');
57
45
  const packageJson = safeGetPackageJsonCached(packageJsonPath);
@@ -127,10 +115,43 @@ export function getDeprecatedEslintConfigFiles(directory) {
127
115
  return fs.existsSync(configPath);
128
116
  });
129
117
  }
130
- export function createEslintConfig(directory) {
131
- const configPath = path.join(directory, 'eslint.config.mts');
118
+ function repoFileDataToString(data) {
119
+ if (typeof data === 'string') {
120
+ return data;
121
+ }
122
+ if (Buffer.isBuffer(data)) {
123
+ return data.toString('utf-8');
124
+ }
125
+ return String(data);
126
+ }
127
+ export async function createEslintConfig(directory, platformVersion) {
128
+ const versionForRemote = platformVersion && !isLegacyProject(platformVersion)
129
+ ? platformVersion
130
+ : null;
131
+ if (versionForRemote === null) {
132
+ const message = commands.project.lint.createEslintConfigRequiresV2Platform(platformVersion);
133
+ uiLogger.error(message);
134
+ throw new Error(message);
135
+ }
136
+ let fetchedContent = null;
137
+ try {
138
+ const { data } = await fetchRepoFile(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, `${versionForRemote}/${UIE_ESLINT_CONFIG_PATH_IN_REPO}`, DEFAULT_PROJECT_TEMPLATE_BRANCH);
139
+ const content = repoFileDataToString(data);
140
+ if (content.trim().length > 0) {
141
+ fetchedContent = content;
142
+ }
143
+ }
144
+ catch (error) {
145
+ debugError(error);
146
+ }
147
+ if (fetchedContent === null) {
148
+ const message = commands.project.lint.failedToFetchRemoteEslintConfig(versionForRemote);
149
+ uiLogger.error(message);
150
+ throw new Error(message);
151
+ }
152
+ const configPath = path.join(directory, 'eslint.config.js');
132
153
  try {
133
- fs.writeFileSync(configPath, ESLINT_CONFIG_TEMPLATE, 'utf-8');
154
+ fs.writeFileSync(configPath, fetchedContent, 'utf-8');
134
155
  return path.relative(process.cwd(), configPath);
135
156
  }
136
157
  catch (error) {
@@ -5,6 +5,7 @@ type ProjectUploadResult<T> = {
5
5
  result?: T;
6
6
  uploadError?: unknown;
7
7
  projectNotFound?: boolean;
8
+ projectId?: number;
8
9
  };
9
10
  type HandleProjectUploadArg<T> = {
10
11
  accountId: number;
@@ -13,11 +13,11 @@ import util from 'node:util';
13
13
  import { lib } from '../../lang/en.js';
14
14
  import { ensureProjectExists } from './ensureProjectExists.js';
15
15
  import { uiLogger } from '../ui/logger.js';
16
- import { isV2Project } from './platformVersion.js';
17
16
  import ProjectValidationError from '../errors/ProjectValidationError.js';
18
17
  import { walk } from '@hubspot/local-dev-lib/fs';
19
18
  import { LEGACY_CONFIG_FILES } from '../constants.js';
20
19
  import { archiveWorkspacesAndDependencies, getPackageJsonPathsToUpdate, getLockfilePathsToUpdate, } from './workspaces.js';
20
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
21
21
  async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
22
22
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
23
23
  SpinniesManager.add('upload', {
@@ -54,7 +54,7 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
54
54
  // Versions <= 2025.1 do not support the new npm workspaces bundling behavior.
55
55
  let workspaceMappings = [];
56
56
  let fileDependencyMappings = [];
57
- if (isV2Project(projectConfig.platformVersion)) {
57
+ if (!isLegacyProject(projectConfig.platformVersion)) {
58
58
  const parsedPackageJsons = await findAndParsePackageJsonFiles(srcDir);
59
59
  workspaceMappings = await collectWorkspaceDirectories(parsedPackageJsons);
60
60
  fileDependencyMappings = await collectFileDependencies(parsedPackageJsons);
@@ -79,7 +79,7 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
79
79
  return resolve({ uploadError: e });
80
80
  }
81
81
  }
82
- const { projectExists } = await ensureProjectExists(accountId, projectConfig.name, {
82
+ const { projectExists, project } = await ensureProjectExists(accountId, projectConfig.name, {
83
83
  forceCreate,
84
84
  uploadCommand: isUploadCommand,
85
85
  noLogs: true,
@@ -88,13 +88,14 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
88
88
  uiLogger.log(lib.projectUpload.handleProjectUpload.projectDoesNotExist(accountId));
89
89
  return resolve({ projectNotFound: true });
90
90
  }
91
+ const projectId = project?.id;
91
92
  const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation);
92
93
  if (error) {
93
- resolve({ uploadError: error });
94
+ resolve({ uploadError: error, projectId });
94
95
  }
95
96
  else if (callbackFunc) {
96
97
  const uploadResult = await callbackFunc(accountId, projectConfig, tempFile, buildId);
97
- resolve({ result: uploadResult });
98
+ resolve({ result: uploadResult, projectId });
98
99
  }
99
100
  }
100
101
  catch (e) {
@@ -132,7 +133,7 @@ export async function validateSourceDirectory(srcDir, projectConfig, projectDir)
132
133
  if (!projectFilePaths || projectFilePaths.length === 0) {
133
134
  throw new ProjectValidationError(lib.projectUpload.handleProjectUpload.emptySource(projectConfig.srcDir));
134
135
  }
135
- if (isV2Project(projectConfig.platformVersion)) {
136
+ if (!isLegacyProject(projectConfig.platformVersion)) {
136
137
  projectFilePaths.forEach(filePath => {
137
138
  const filename = path.basename(filePath);
138
139
  if (LEGACY_CONFIG_FILES.includes(filename)) {
@@ -143,7 +144,7 @@ export async function validateSourceDirectory(srcDir, projectConfig, projectDir)
143
144
  }
144
145
  export async function validateNoHSMetaMismatch(srcDir, projectConfig) {
145
146
  const hasHsMetaFiles = await projectContainsHsMetaFiles(srcDir);
146
- if (!isV2Project(projectConfig.platformVersion) && hasHsMetaFiles) {
147
+ if (isLegacyProject(projectConfig.platformVersion) && hasHsMetaFiles) {
147
148
  throw new ProjectValidationError(lib.projectUpload.wrongPlatformVersionMetaFiles);
148
149
  }
149
150
  }
@@ -4,6 +4,9 @@ export async function projectLogsPrompt({ functionChoices, promptOptions, projec
4
4
  if (!functionChoices) {
5
5
  return {};
6
6
  }
7
+ if (promptOptions?.function) {
8
+ return { functionName: promptOptions.function };
9
+ }
7
10
  if (functionChoices.length === 1) {
8
11
  return { functionName: functionChoices[0] };
9
12
  }
@@ -42,6 +42,7 @@ function mapPromptChoicesToChoices(choices) {
42
42
  return {
43
43
  value: choice.value,
44
44
  name: choice.name,
45
+ short: choice.short,
45
46
  disabled: choice.disabled,
46
47
  checked: choice.checked,
47
48
  };
@@ -10,7 +10,7 @@ import { EXIT_CODES } from '../enums/exitCodes.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
  // cms-dev-server version to install to isolated cache
13
- const TARGET_CMS_DEV_SERVER_VERSION = '1.2.16';
13
+ const TARGET_CMS_DEV_SERVER_VERSION = '1.2.26';
14
14
  /**
15
15
  * Ensures cms-dev-server is installed in an isolated cache directory.
16
16
  * This prevents React version conflicts with the CLI.
@@ -0,0 +1,15 @@
1
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { McpLogger } from './utils/logger.js';
3
+ import { TextContentResponse } from './types.js';
4
+ export declare class Tool<InputSchema> {
5
+ protected mcpServer: McpServer;
6
+ protected logger: McpLogger;
7
+ protected toolName: string;
8
+ constructor(mcpServer: McpServer, logger: McpLogger, toolName: string);
9
+ register(): RegisteredTool;
10
+ handler(input: InputSchema): TextContentResponse | Promise<TextContentResponse>;
11
+ protected getTrackingMeta(input: InputSchema): {
12
+ [key: string]: string;
13
+ } | undefined;
14
+ protected wrappedHandler(input: InputSchema): Promise<TextContentResponse>;
15
+ }
@@ -0,0 +1,53 @@
1
+ import { formatTextContents } from './utils/content.js';
2
+ import { getErrorMessage } from '../lib/errorHandlers/index.js';
3
+ import { trackToolUsage } from './utils/toolUsageTracking.js';
4
+ export class Tool {
5
+ mcpServer;
6
+ logger;
7
+ toolName;
8
+ constructor(mcpServer, logger, toolName) {
9
+ this.mcpServer = mcpServer;
10
+ this.logger = logger;
11
+ this.toolName = toolName;
12
+ }
13
+ register() {
14
+ throw new Error('Must implement register');
15
+ }
16
+ handler(
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ input) {
19
+ throw new Error('Must implement handler');
20
+ }
21
+ getTrackingMeta(
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
+ input) {
24
+ return undefined;
25
+ }
26
+ async wrappedHandler(input) {
27
+ const startTime = Date.now();
28
+ try {
29
+ // `input` is logged unredacted. Tool input schemas MUST NOT include
30
+ // credentials or other sensitive values, since MCP clients (Claude
31
+ // Desktop, Inspector, etc.) will display these logs.
32
+ this.logger.debug(this.toolName, {
33
+ message: 'Tool invoked',
34
+ args: input,
35
+ });
36
+ await trackToolUsage(this.toolName, this.getTrackingMeta(input));
37
+ const result = await this.handler(input);
38
+ this.logger.debug(this.toolName, {
39
+ message: 'Tool completed',
40
+ durationMs: Date.now() - startTime,
41
+ });
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ this.logger.error(this.toolName, {
46
+ message: 'Tool failed',
47
+ error: error instanceof Error ? error.message : String(error),
48
+ durationMs: Date.now() - startTime,
49
+ });
50
+ return formatTextContents(getErrorMessage(error));
51
+ }
52
+ }
53
+ }