@hubspot/cli 8.2.0-experimental.1 → 8.2.0-experimental.2

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 (45) hide show
  1. package/commands/account/auth.js +2 -0
  2. package/commands/app/logs.js +4 -0
  3. package/commands/getStarted.js +2 -0
  4. package/commands/init.js +2 -0
  5. package/commands/mcp/setup.js +2 -2
  6. package/commands/project/create.js +2 -0
  7. package/commands/project/installApp.d.ts +7 -0
  8. package/commands/project/installApp.js +181 -0
  9. package/commands/project/installDeps.js +2 -0
  10. package/commands/project/upload.js +4 -0
  11. package/commands/project.js +2 -0
  12. package/commands/upgrade.js +2 -0
  13. package/lang/en.d.ts +58 -0
  14. package/lang/en.js +71 -2
  15. package/lib/accountTargetDiscovery.d.ts +15 -0
  16. package/lib/accountTargetDiscovery.js +175 -0
  17. package/lib/app/install.d.ts +22 -0
  18. package/lib/app/install.js +42 -0
  19. package/lib/app/logs.js +1 -1
  20. package/lib/auth/awaitPersonalAccessKeyOverWebsocket.js +3 -2
  21. package/lib/doctor/Doctor.d.ts +1 -0
  22. package/lib/doctor/Doctor.js +26 -0
  23. package/lib/mcp/clients.d.ts +13 -0
  24. package/lib/mcp/clients.js +55 -0
  25. package/lib/mcp/promotion.d.ts +3 -0
  26. package/lib/mcp/promotion.js +109 -0
  27. package/lib/mcp/setup.d.ts +3 -2
  28. package/lib/mcp/setup.js +17 -12
  29. package/lib/projects/installApp.d.ts +58 -0
  30. package/lib/projects/installApp.js +278 -0
  31. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -3
  32. package/lib/projects/localDev/AppDevModeInterface.js +22 -44
  33. package/lib/projects/localDev/helpers/project.d.ts +4 -3
  34. package/lib/projects/localDev/helpers/project.js +46 -12
  35. package/lib/prompts/projectDevTargetAccountPrompt.js +3 -2
  36. package/lib/theme/cmsDevServerProcess.js +1 -1
  37. package/lib/usageTracking.d.ts +1 -0
  38. package/lib/usageTracking.js +9 -0
  39. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  40. package/package.json +4 -4
  41. package/types/AccountTargets.d.ts +37 -0
  42. package/types/AccountTargets.js +12 -0
  43. package/types/LocalDev.d.ts +3 -2
  44. package/types/ProjectComponents.d.ts +2 -1
  45. package/types/Projects.d.ts +5 -0
@@ -0,0 +1,278 @@
1
+ import path from 'path';
2
+ import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth';
3
+ import { fetchAppMetadataBySourceId, fetchPublicAppMetadata, installStaticAuthAppOnCurrentAccount, installStaticAuthAppOnTestAccount, } from '@hubspot/local-dev-lib/api/appsDev';
4
+ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
5
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
6
+ import { getAllConfigAccounts, getConfigAccountIfExists, } from '@hubspot/local-dev-lib/config';
7
+ import { translateForLocalDev } from '@hubspot/project-parsing-lib/translate';
8
+ import { commands } from '../../lang/en.js';
9
+ import { isAppDeveloperAccount } from '../accountTypes.js';
10
+ import { ApiErrorContext, debugError, logError, } from '../errorHandlers/index.js';
11
+ import { confirmPrompt, listPrompt } from '../prompts/promptUtils.js';
12
+ import { projectProfilePrompt } from '../prompts/projectProfilePrompt.js';
13
+ import { loadProfile } from './projectProfiles.js';
14
+ import { handleProjectUpload } from './upload.js';
15
+ import { pollProjectBuildAndDeploy } from './pollProjectBuildAndDeploy.js';
16
+ import { getAppNodeFromProjectNodes, canUseStaticAuthTestAccountInstall, isPrivateApp, isStaticAuthApp, } from '../app/install.js';
17
+ import { uiLogger } from '../ui/logger.js';
18
+ import { uiAccountDescription } from '../ui/index.js';
19
+ import { getStaticAuthAppInstallUrl } from '../app/urls.js';
20
+ export async function resolveProjectAccountId(derivedAccountId, projectConfig, projectDir, profileOption, useEnvOption) {
21
+ try {
22
+ const profileName = await projectProfilePrompt(projectDir, projectConfig, profileOption, useEnvOption);
23
+ if (profileName) {
24
+ const profile = loadProfile(projectConfig, projectDir, profileName);
25
+ uiLogger.log(commands.project.installApp.profileMessage(profileName, profile.accountId));
26
+ uiLogger.log('');
27
+ return { accountId: profile.accountId, profileName };
28
+ }
29
+ return { accountId: derivedAccountId };
30
+ }
31
+ catch (error) {
32
+ logError(error);
33
+ return null;
34
+ }
35
+ }
36
+ export async function loadInstallableAppNode(projectConfig, projectDir, accountId) {
37
+ let appNode;
38
+ try {
39
+ const { intermediateNodesIndexedByUid } = await translateForLocalDev({
40
+ projectSourceDir: path.join(projectDir, projectConfig.srcDir),
41
+ platformVersion: projectConfig.platformVersion,
42
+ accountId,
43
+ }, { skipValidation: true });
44
+ appNode = getAppNodeFromProjectNodes(intermediateNodesIndexedByUid);
45
+ }
46
+ catch (error) {
47
+ debugError(error);
48
+ uiLogger.error(commands.project.installApp.errors.failedToParseProject);
49
+ return null;
50
+ }
51
+ if (!appNode) {
52
+ uiLogger.error(commands.project.installApp.errors.noAppInProject);
53
+ return null;
54
+ }
55
+ if (!isStaticAuthApp(appNode)) {
56
+ uiLogger.error(commands.project.installApp.errors.unsupportedAuthType(appNode.config.auth.type));
57
+ return null;
58
+ }
59
+ if (!isPrivateApp(appNode)) {
60
+ uiLogger.error(commands.project.installApp.errors.unsupportedDistribution(appNode.config.distribution || 'unknown'));
61
+ return null;
62
+ }
63
+ return appNode;
64
+ }
65
+ // App developer accounts can't host app installs.
66
+ export async function resolveValidInstallAccount(targetAccountId, force, formatOutputAsJson) {
67
+ const accountConfig = getConfigAccountIfExists(targetAccountId);
68
+ if (!accountConfig || !isAppDeveloperAccount(accountConfig)) {
69
+ return targetAccountId;
70
+ }
71
+ const validAccounts = getAllConfigAccounts().filter(account => !isAppDeveloperAccount(account));
72
+ if (force || formatOutputAsJson || validAccounts.length === 0) {
73
+ uiLogger.error(commands.project.installApp.errors.invalidAppDeveloperAccount(targetAccountId));
74
+ return null;
75
+ }
76
+ uiLogger.log(commands.project.installApp.appDeveloperAccountNotice(targetAccountId));
77
+ return listPrompt(commands.project.installApp.selectInstallAccountPrompt, {
78
+ choices: validAccounts.map(account => ({
79
+ name: uiAccountDescription(account.accountId, false),
80
+ value: account.accountId,
81
+ })),
82
+ });
83
+ }
84
+ async function offerUploadAndDeploy(options) {
85
+ const { accountId, projectConfig, projectDir, profile, force } = options;
86
+ if (!force) {
87
+ if (options.formatOutputAsJson) {
88
+ return false;
89
+ }
90
+ const shouldUpload = await confirmPrompt(commands.project.installApp.uploadAndDeployPrompt(accountId));
91
+ if (!shouldUpload) {
92
+ return false;
93
+ }
94
+ }
95
+ try {
96
+ const { result, uploadError } = await handleProjectUpload({
97
+ accountId,
98
+ projectConfig,
99
+ projectDir,
100
+ callbackFunc: pollProjectBuildAndDeploy,
101
+ isUploadCommand: true,
102
+ sendIR: true,
103
+ forceCreate: true,
104
+ profile,
105
+ });
106
+ if (uploadError) {
107
+ logError(uploadError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
108
+ return false;
109
+ }
110
+ return Boolean(result && result.succeeded);
111
+ }
112
+ catch (error) {
113
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
114
+ return false;
115
+ }
116
+ }
117
+ export async function resolveProjectId(options) {
118
+ const { accountId, projectConfig } = options;
119
+ try {
120
+ const response = await fetchProject(accountId, projectConfig.name);
121
+ return response.data.id;
122
+ }
123
+ catch (error) {
124
+ if (!isHubSpotHttpError(error) || error.status !== 404) {
125
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
126
+ return null;
127
+ }
128
+ const uploaded = await offerUploadAndDeploy(options);
129
+ if (!uploaded) {
130
+ uiLogger.error(commands.project.installApp.errors.projectNotFound(accountId, projectConfig.name));
131
+ return null;
132
+ }
133
+ try {
134
+ const response = await fetchProject(accountId, projectConfig.name);
135
+ return response.data.id;
136
+ }
137
+ catch (retryError) {
138
+ logError(retryError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
139
+ return null;
140
+ }
141
+ }
142
+ }
143
+ export async function resolveAppMetadata(options) {
144
+ const { appId, projectId, appUid, appName, accountId, projectConfig } = options;
145
+ const fetchMetadata = (forceSourceIdLookup = false) => appId && !forceSourceIdLookup
146
+ ? fetchPublicAppMetadata(appId, accountId)
147
+ : fetchAppMetadataBySourceId(projectId, appUid, accountId);
148
+ try {
149
+ const { data } = await fetchMetadata();
150
+ return {
151
+ appId: appId ?? data.id,
152
+ scopeGroupIds: data.scopeGroupIds,
153
+ ownerPortalId: data.portalId,
154
+ };
155
+ }
156
+ catch (error) {
157
+ if (!isHubSpotHttpError(error) || error.status !== 404) {
158
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
159
+ return null;
160
+ }
161
+ const uploaded = await offerUploadAndDeploy(options);
162
+ if (!uploaded) {
163
+ uiLogger.error(commands.project.installApp.errors.appNotDeployed(appName, accountId));
164
+ return null;
165
+ }
166
+ try {
167
+ const { data } = await fetchMetadata(true);
168
+ return {
169
+ appId: data.id,
170
+ scopeGroupIds: data.scopeGroupIds,
171
+ ownerPortalId: data.portalId,
172
+ };
173
+ }
174
+ catch (retryError) {
175
+ logError(retryError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
176
+ return null;
177
+ }
178
+ }
179
+ }
180
+ export async function fetchProjectAppInstallationData(targetAccountId, projectId, appNode, projectConfig) {
181
+ try {
182
+ const response = await fetchAppInstallationData(targetAccountId, projectId, appNode.uid, appNode.config.auth.requiredScopes, appNode.config.auth.optionalScopes);
183
+ return {
184
+ appId: response.data.appId,
185
+ isInstalledWithScopeGroups: response.data.isInstalledWithScopeGroups,
186
+ previouslyAuthorizedScopeGroups: response.data.previouslyAuthorizedScopeGroups,
187
+ };
188
+ }
189
+ catch (error) {
190
+ if (isHubSpotHttpError(error) && error.status === 404) {
191
+ return {
192
+ isInstalledWithScopeGroups: false,
193
+ previouslyAuthorizedScopeGroups: [],
194
+ };
195
+ }
196
+ logError(error, new ApiErrorContext({
197
+ accountId: targetAccountId,
198
+ projectName: projectConfig.name,
199
+ }));
200
+ return null;
201
+ }
202
+ }
203
+ export async function confirmInstallAppAction({ appName, targetAccountId, force, needsReinstall, }) {
204
+ if (force) {
205
+ return true;
206
+ }
207
+ if (needsReinstall) {
208
+ uiLogger.log(commands.project.installApp.outdatedScopes(appName, targetAccountId));
209
+ return confirmPrompt(commands.project.installApp.reinstallPrompt);
210
+ }
211
+ return confirmPrompt(commands.project.installApp.installPrompt(appName, targetAccountId));
212
+ }
213
+ export async function installStaticAuthAppForAccount({ appId, appNode, appName, targetAccountId, targetAccountConfig, projectId, projectName, scopeGroupIds, ownerPortalId, installationState, formatOutputAsJson, }) {
214
+ const installingIntoOwnerAccount = ownerPortalId === targetAccountId;
215
+ const useTestAccountInstall = !installingIntoOwnerAccount &&
216
+ canUseStaticAuthTestAccountInstall({
217
+ accountConfig: targetAccountConfig,
218
+ appNode,
219
+ });
220
+ if (!installingIntoOwnerAccount && !useTestAccountInstall) {
221
+ const installUrl = getStaticAuthAppInstallUrl({
222
+ targetAccountId,
223
+ env: (targetAccountConfig?.env || 'prod'),
224
+ appId,
225
+ });
226
+ if (formatOutputAsJson) {
227
+ uiLogger.json({
228
+ appId,
229
+ appUid: appNode.uid,
230
+ accountId: targetAccountId,
231
+ projectId,
232
+ installationState,
233
+ installed: false,
234
+ reinstalled: false,
235
+ installUrl,
236
+ error: commands.project.installApp.jsonErrors.automaticInstallUnavailable,
237
+ });
238
+ }
239
+ else {
240
+ uiLogger.error(commands.project.installApp.errors.automaticInstallUnavailable(appName, targetAccountId));
241
+ uiLogger.log(commands.project.installApp.installFromBrowser(installUrl));
242
+ }
243
+ return false;
244
+ }
245
+ try {
246
+ if (useTestAccountInstall) {
247
+ await installStaticAuthAppOnTestAccount(appId, targetAccountId, scopeGroupIds);
248
+ }
249
+ else {
250
+ await installStaticAuthAppOnCurrentAccount(appId, targetAccountId, scopeGroupIds);
251
+ }
252
+ return true;
253
+ }
254
+ catch (error) {
255
+ if (formatOutputAsJson) {
256
+ uiLogger.json({
257
+ appId,
258
+ appUid: appNode.uid,
259
+ accountId: targetAccountId,
260
+ projectId,
261
+ installationState,
262
+ installed: false,
263
+ reinstalled: false,
264
+ error: isHubSpotHttpError(error)
265
+ ? error.message
266
+ : commands.project.installApp.jsonErrors.installFailed(appName, targetAccountId),
267
+ });
268
+ }
269
+ else {
270
+ uiLogger.error(commands.project.installApp.errors.installFailed(appName, targetAccountId));
271
+ logError(error, new ApiErrorContext({
272
+ accountId: targetAccountId,
273
+ projectName,
274
+ }));
275
+ }
276
+ return false;
277
+ }
278
+ }
@@ -12,12 +12,9 @@ declare class AppDevModeInterface {
12
12
  marketplaceAppInstalls?: number;
13
13
  private appInstallResolve?;
14
14
  constructor(options: AppDevModeInterfaceConstructorOptions);
15
- private getAppNodeFromProjectNodes;
16
15
  private get appNode();
17
16
  private get appData();
18
17
  private set appData(value);
19
- private isStaticAuthApp;
20
- private isOAuthApp;
21
18
  private isAutomaticallyInstallable;
22
19
  private getAppInstallUrl;
23
20
  private fetchAppData;
@@ -3,7 +3,6 @@ import { fetchAppMetadataBySourceId, fetchPublicAppProductionInstallCounts, inst
3
3
  import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
4
4
  import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, APP_INSTALLATION_STATES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
5
5
  import { EXIT_CODES } from '../../enums/exitCodes.js';
6
- import { isAppIRNode } from '../../projects/structure.js';
7
6
  import { uiLine } from '../../ui/index.js';
8
7
  import { logError } from '../../errorHandlers/index.js';
9
8
  import { isPromptExitError } from '../../errors/PromptExitError.js';
@@ -12,9 +11,9 @@ import { confirmPrompt } from '../../prompts/promptUtils.js';
12
11
  import { lib } from '../../../lang/en.js';
13
12
  import { uiLogger } from '../../ui/logger.js';
14
13
  import { getOauthAppInstallUrl, getStaticAuthAppInstallUrl, } from '../../app/urls.js';
15
- import { isDeveloperTestAccount, isSandbox } from '../../accountTypes.js';
16
14
  import SpinniesManager from '../../ui/SpinniesManager.js';
17
15
  import { isServerRunningAtUrl } from '../../http.js';
16
+ import { canUseStaticAuthTestAccountInstall, getAppInstallationState, getAppNodeFromProjectNodes, isMarketplaceApp, isOAuthApp, } from '../../app/install.js';
18
17
  class AppDevModeInterface {
19
18
  localDevState;
20
19
  localDevLogger;
@@ -31,13 +30,10 @@ class AppDevModeInterface {
31
30
  throw new Error(lib.LocalDevManager.failedToInitialize);
32
31
  }
33
32
  }
34
- getAppNodeFromProjectNodes(projectNodes) {
35
- return Object.values(projectNodes).find(isAppIRNode) || null;
36
- }
37
33
  // Assumes only one app per project
38
34
  get appNode() {
39
35
  if (this._appNode === undefined) {
40
- this._appNode = this.getAppNodeFromProjectNodes(this.localDevState.projectNodes);
36
+ this._appNode = getAppNodeFromProjectNodes(this.localDevState.projectNodes);
41
37
  }
42
38
  return this._appNode;
43
39
  }
@@ -61,25 +57,16 @@ class AppDevModeInterface {
61
57
  }
62
58
  this.localDevState.setAppDataForUid(this.appNode.uid, appData);
63
59
  }
64
- isStaticAuthApp() {
65
- return (this.appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.STATIC);
66
- }
67
- isOAuthApp() {
68
- return (this.appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.OAUTH);
69
- }
70
60
  isAutomaticallyInstallable() {
71
61
  const targetTestingAccount = getConfigAccountById(this.localDevState.targetTestingAccountId);
72
- if (!targetTestingAccount) {
73
- return false;
74
- }
75
- const isTestAccount = isDeveloperTestAccount(targetTestingAccount) ||
76
- isSandbox(targetTestingAccount);
77
- const hasCorrectParent = targetTestingAccount.parentAccountId ===
78
- this.localDevState.targetProjectAccountId;
79
- return isTestAccount && hasCorrectParent && this.isStaticAuthApp();
62
+ return canUseStaticAuthTestAccountInstall({
63
+ accountConfig: targetTestingAccount,
64
+ appNode: this.appNode,
65
+ parentAccountId: this.localDevState.targetProjectAccountId,
66
+ });
80
67
  }
81
68
  async getAppInstallUrl() {
82
- if (this.appNode && this.isOAuthApp()) {
69
+ if (this.appNode && isOAuthApp(this.appNode)) {
83
70
  return getOauthAppInstallUrl({
84
71
  targetAccountId: this.localDevState.targetTestingAccountId,
85
72
  env: this.localDevState.env,
@@ -195,26 +182,17 @@ class AppDevModeInterface {
195
182
  return {};
196
183
  }
197
184
  const { data: { isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups }, } = await fetchAppInstallationData(this.localDevState.targetTestingAccountId, this.localDevState.projectId, this.appNode.uid, this.appNode.config.auth.requiredScopes, this.appNode.config.auth.optionalScopes);
198
- const isReinstall = previouslyAuthorizedScopeGroups.length > 0;
199
- if (isInstalledWithScopeGroups) {
200
- this.appData = {
201
- ...this.appData,
202
- installationState: APP_INSTALLATION_STATES.INSTALLED,
203
- };
204
- }
205
- else if (isReinstall) {
206
- this.appData = {
207
- ...this.appData,
208
- installationState: APP_INSTALLATION_STATES.INSTALLED_WITH_OUTDATED_SCOPES,
209
- };
210
- }
211
- else {
212
- this.appData = {
213
- ...this.appData,
214
- installationState: APP_INSTALLATION_STATES.NOT_INSTALLED,
215
- };
216
- }
217
- return { needsInstall: !isInstalledWithScopeGroups, isReinstall };
185
+ const installationState = getAppInstallationState(isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups);
186
+ const isReinstall = installationState ===
187
+ APP_INSTALLATION_STATES.INSTALLED_WITH_OUTDATED_SCOPES;
188
+ this.appData = {
189
+ ...this.appData,
190
+ installationState,
191
+ };
192
+ return {
193
+ needsInstall: installationState !== APP_INSTALLATION_STATES.INSTALLED,
194
+ isReinstall,
195
+ };
218
196
  }
219
197
  async validateOauthAppRedirectUrl() {
220
198
  const redirectUrl = this.appNode?.config.auth.redirectUrls[0];
@@ -263,7 +241,7 @@ class AppDevModeInterface {
263
241
  }
264
242
  };
265
243
  onChangeProjectNodes = (nodes) => {
266
- const newAppNode = this.getAppNodeFromProjectNodes(nodes);
244
+ const newAppNode = getAppNodeFromProjectNodes(nodes);
267
245
  const oldDistribution = this.appNode?.config.distribution;
268
246
  const newDistribution = newAppNode?.config.distribution;
269
247
  const oldAuthType = this.appNode?.config.auth.type;
@@ -287,7 +265,7 @@ class AppDevModeInterface {
287
265
  }
288
266
  try {
289
267
  await this.fetchAppData();
290
- if (this.appNode.config.distribution === APP_DISTRIBUTION_TYPES.MARKETPLACE) {
268
+ if (isMarketplaceApp(this.appNode)) {
291
269
  await this.checkMarketplaceAppInstalls();
292
270
  }
293
271
  const { needsInstall, isReinstall } = await this.checkTestAccountAppInstallation();
@@ -298,7 +276,7 @@ class AppDevModeInterface {
298
276
  failColor: 'white',
299
277
  });
300
278
  }
301
- if (this.isOAuthApp()) {
279
+ if (isOAuthApp(this.appNode)) {
302
280
  await this.validateOauthAppRedirectUrl();
303
281
  }
304
282
  this.localDevState.addListener('devServerMessage', this.onDevServerMessage);
@@ -1,7 +1,7 @@
1
1
  import { type IntermediateRepresentationNode, type IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/translate';
2
2
  import { Build } from '@hubspot/local-dev-lib/types/Build';
3
3
  import { Project } from '@hubspot/local-dev-lib/types/Project';
4
- import { ProjectConfig } from '../../../../types/Projects.js';
4
+ import { LocallyChangedComponents, ProjectConfig } from '../../../../types/Projects.js';
5
5
  import { ExitFunction } from '../../../../types/Yargs.js';
6
6
  export declare function createNewProjectForLocalDev(projectConfig: ProjectConfig, targetAccountId: number, shouldCreateWithoutConfirmation: boolean, hasPublicApps: boolean, exit: ExitFunction): Promise<Project>;
7
7
  export declare function createInitialBuildForNewProject(projectConfig: ProjectConfig, projectDir: string, targetAccountId: number, exit: ExitFunction, sendIR?: boolean, profile?: string): Promise<Build>;
@@ -11,7 +11,8 @@ export declare function compareLocalProjectToDeployed(projectConfig: ProjectConf
11
11
  export declare function getDeployedProjectNodes(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, profile?: string): Promise<{
12
12
  [key: string]: IntermediateRepresentationNode;
13
13
  }>;
14
- export declare function isDeployedProjectUpToDateWithLocal(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, localProjectNodes: {
14
+ export declare function hasLocalComponentChanges(changed: LocallyChangedComponents): boolean;
15
+ export declare function getLocallyChangedComponents(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, localProjectNodes: {
15
16
  [key: string]: IntermediateRepresentationNodeLocalDev;
16
- }, profile?: string): Promise<boolean>;
17
+ }, profile?: string): Promise<LocallyChangedComponents>;
17
18
  export declare function checkAndInstallDependencies(): Promise<void>;
@@ -1,25 +1,24 @@
1
1
  import fs from 'fs-extra';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { createProject } from '@hubspot/local-dev-lib/api/projects';
5
- import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
4
+ import { createProject, downloadProject, } from '@hubspot/local-dev-lib/api/projects';
6
5
  import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
7
6
  import { sanitizeFileName } from '@hubspot/local-dev-lib/path';
8
7
  import { isDeepEqual } from '@hubspot/local-dev-lib/isDeepEqual';
9
8
  import { translate, } from '@hubspot/project-parsing-lib/translate';
9
+ import { AUTO_GENERATED_COMPONENT_TYPES } from '@hubspot/project-parsing-lib/constants';
10
+ import { mapToUserFacingType } from '@hubspot/project-parsing-lib/transform';
10
11
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
11
- import { PROJECT_ERROR_TYPES, PROJECT_BUILD_TEXT, PROJECT_DEPLOY_TEXT, PROJECT_CONFIG_FILE, } from '../../../constants.js';
12
+ import { PROJECT_BUILD_TEXT, PROJECT_CONFIG_FILE, PROJECT_DEPLOY_TEXT, PROJECT_ERROR_TYPES, } from '../../../constants.js';
12
13
  import { lib } from '../../../../lang/en.js';
13
14
  import { uiLogger } from '../../../ui/logger.js';
14
- import { uiLine } from '../../../ui/index.js';
15
+ import { uiAccountDescription, uiLine } from '../../../ui/index.js';
15
16
  import { confirmPrompt } from '../../../prompts/promptUtils.js';
16
- import { uiAccountDescription } from '../../../ui/index.js';
17
17
  import SpinniesManager from '../../../ui/SpinniesManager.js';
18
18
  import { EXIT_CODES } from '../../../enums/exitCodes.js';
19
19
  import { handleProjectUpload } from '../../upload.js';
20
20
  import { pollProjectBuildAndDeploy } from '../../pollProjectBuildAndDeploy.js';
21
- import { debugError, logError } from '../../../errorHandlers/index.js';
22
- import { ApiErrorContext } from '../../../errorHandlers/index.js';
21
+ import { ApiErrorContext, debugError, logError, } from '../../../errorHandlers/index.js';
23
22
  import { getProjectPackageJsonLocations, hasMissingPackages, installPackages, } from '../../../dependencyManagement.js';
24
23
  // Prompt the user to create a new project if one doesn't exist on their target account
25
24
  export async function createNewProjectForLocalDev(projectConfig, targetAccountId, shouldCreateWithoutConfirmation, hasPublicApps, exit) {
@@ -133,8 +132,8 @@ export async function compareLocalProjectToDeployed(projectConfig, accountId, de
133
132
  SpinniesManager.add('compareLocalProjectToDeployed', {
134
133
  text: lib.localDevHelpers.project.compareLocalProjectToDeployed.checking,
135
134
  });
136
- const isUpToDate = await isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes, profile);
137
- if (isUpToDate) {
135
+ const changedComponents = await getLocallyChangedComponents(projectConfig, accountId, deployedBuildId, localProjectNodes, profile);
136
+ if (!hasLocalComponentChanges(changedComponents)) {
138
137
  SpinniesManager.succeed('compareLocalProjectToDeployed', {
139
138
  text: lib.localDevHelpers.project.compareLocalProjectToDeployed.upToDate,
140
139
  });
@@ -145,6 +144,8 @@ export async function compareLocalProjectToDeployed(projectConfig, accountId, de
145
144
  .notUpToDate,
146
145
  });
147
146
  uiLogger.log('');
147
+ uiLogger.log(lib.localDevHelpers.project.compareLocalProjectToDeployed.changedFiles(changedComponents));
148
+ uiLogger.log('');
148
149
  uiLogger.log(lib.localDevHelpers.project.compareLocalProjectToDeployed.notUpToDateExplanation(profile));
149
150
  return exit(EXIT_CODES.SUCCESS);
150
151
  }
@@ -185,14 +186,47 @@ export async function getDeployedProjectNodes(projectConfig, accountId, deployed
185
186
  }
186
187
  }
187
188
  }
188
- export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes, profile) {
189
+ function filterAutoGeneratedNodes(nodes) {
190
+ return Object.fromEntries(Object.entries(nodes).filter(([, node]) => !AUTO_GENERATED_COMPONENT_TYPES.includes(mapToUserFacingType(node.componentType))));
191
+ }
192
+ export function hasLocalComponentChanges(changed) {
193
+ return (changed.added.length > 0 ||
194
+ changed.updated.length > 0 ||
195
+ changed.removed.length > 0);
196
+ }
197
+ export async function getLocallyChangedComponents(projectConfig, accountId, deployedBuildId, localProjectNodes, profile) {
198
+ const result = {
199
+ added: [],
200
+ updated: [],
201
+ removed: [],
202
+ };
189
203
  try {
190
204
  const deployedProjectNodes = await getDeployedProjectNodes(projectConfig, accountId, deployedBuildId, profile);
191
- return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
205
+ const filteredLocal = filterAutoGeneratedNodes(localProjectNodes);
206
+ const filteredDeployed = filterAutoGeneratedNodes(deployedProjectNodes);
207
+ const allUids = new Set([
208
+ ...Object.keys(filteredLocal),
209
+ ...Object.keys(filteredDeployed),
210
+ ]);
211
+ for (const uid of allUids) {
212
+ const local = filteredLocal[uid];
213
+ const deployed = filteredDeployed[uid];
214
+ if (!local) {
215
+ result.removed.push(path.join(projectConfig.srcDir, deployed.metaFilePath));
216
+ }
217
+ else if (!deployed) {
218
+ result.added.push(path.join(projectConfig.srcDir, local.metaFilePath));
219
+ }
220
+ else if (!isDeepEqual(local, deployed, ['localDev', 'componentDeps'])) {
221
+ result.updated.push(path.join(projectConfig.srcDir, local.metaFilePath));
222
+ }
223
+ }
224
+ return result;
192
225
  }
193
226
  catch (err) {
194
227
  debugError(err);
195
- return false;
228
+ uiLogger.warn(lib.localDevHelpers.project.compareLocalProjectToDeployed.unableToCompare);
229
+ return result;
196
230
  }
197
231
  }
198
232
  export async function checkAndInstallDependencies() {
@@ -1,5 +1,5 @@
1
1
  import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
2
- import { HUBSPOT_ACCOUNT_TYPES, HUBSPOT_ACCOUNT_TYPE_STRINGS, } from '@hubspot/local-dev-lib/constants/config';
2
+ import { HUBSPOT_ACCOUNT_TYPE_STRINGS, HUBSPOT_ACCOUNT_TYPES, } from '@hubspot/local-dev-lib/constants/config';
3
3
  import { fetchDeveloperTestAccounts } from '@hubspot/local-dev-lib/api/developerTestAccounts';
4
4
  import { promptUser } from './promptUtils.js';
5
5
  import { lib } from '../../lang/en.js';
@@ -92,7 +92,8 @@ export async function selectDeveloperTestTargetAccountPrompt(accounts, defaultAc
92
92
  devTestAccountsResponse = data;
93
93
  }
94
94
  catch (err) {
95
- uiLogger.debug('Unable to fetch developer test account usage limits: ', err);
95
+ uiLogger.error(lib.prompts.projectDevTargetAccountPrompt.fetchDeveloperTestAccountsError);
96
+ throw new PromptExitError(lib.prompts.projectDevTargetAccountPrompt.fetchDeveloperTestAccountsError, EXIT_CODES.ERROR);
96
97
  }
97
98
  let disabledMessage = false;
98
99
  if (devTestAccountsResponse &&
@@ -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.26';
13
+ const TARGET_CMS_DEV_SERVER_VERSION = '1.2.44';
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.
@@ -19,3 +19,4 @@ export declare function trackHelpUsage(command: string): Promise<void>;
19
19
  export declare function trackConvertFieldsUsage(command: string): Promise<void>;
20
20
  export declare function trackAuthAction(command: string, authType: string, step: string, accountId?: number): Promise<void>;
21
21
  export declare function trackCommandMetadataUsage(command: string, meta?: UsageTrackingMeta, accountId?: number): Promise<void>;
22
+ export declare function trackMcpPromotionShown(command?: string): Promise<void>;
@@ -171,6 +171,15 @@ export async function trackCommandMetadataUsage(command, meta = {}, accountId) {
171
171
  meta,
172
172
  });
173
173
  }
174
+ export async function trackMcpPromotionShown(command) {
175
+ return trackCliInteraction({
176
+ action: 'cli-mcp-promotion',
177
+ command: command ?? 'mcp-promotion',
178
+ meta: {
179
+ step: 'shown',
180
+ },
181
+ });
182
+ }
174
183
  async function trackCliInteraction({ action, accountId, command, authType, meta = {}, }) {
175
184
  try {
176
185
  const config = getConfig();
@@ -61,7 +61,7 @@ const inputSchema = {
61
61
  commerceLevel: z
62
62
  .enum(ACCOUNT_LEVEL_CHOICES_WITHOUT_STARTER)
63
63
  .optional()
64
- .describe(`Commerce Hub tier level. Options: ${ACCOUNT_LEVEL_CHOICES_WITHOUT_STARTER.join(', ')}. Defaults to ENTERPRISE if not specified.`),
64
+ .describe(`Revenue Hub tier level. Options: ${ACCOUNT_LEVEL_CHOICES_WITHOUT_STARTER.join(', ')}. Defaults to ENTERPRISE if not specified.`),
65
65
  };
66
66
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
67
  const createTestAccountInputSchema = z.object({ ...inputSchema });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.2.0-experimental.1",
3
+ "version": "8.2.0-experimental.2",
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",
@@ -10,10 +10,10 @@
10
10
  "!**/__tests__/**"
11
11
  ],
12
12
  "dependencies": {
13
- "@hubspot/local-dev-lib": "5.8.1",
14
- "@hubspot/project-parsing-lib": "0.2.2-experimental.1",
13
+ "@hubspot/local-dev-lib": "5.9.0",
14
+ "@hubspot/project-parsing-lib": "0.18.0",
15
15
  "@hubspot/serverless-dev-runtime": "7.0.7",
16
- "@hubspot/ui-extensions-dev-server": "2.0.9",
16
+ "@hubspot/ui-extensions-dev-server": "2.0.10",
17
17
  "@inquirer/prompts": "7.1.0",
18
18
  "@modelcontextprotocol/sdk": "1.29.0",
19
19
  "archiver": "7.0.1",