@hubspot/cli 8.9.1 → 8.10.0-beta.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 (53) hide show
  1. package/commands/account/auth.js +2 -0
  2. package/commands/app/logs.js +4 -0
  3. package/commands/getStarted.js +3 -0
  4. package/commands/init.js +2 -0
  5. package/commands/mcp/setup.js +2 -2
  6. package/commands/project/appInstallStatus.js +2 -2
  7. package/commands/project/create.js +2 -0
  8. package/commands/project/dev/unifiedFlow.js +3 -1
  9. package/commands/project/installApp.d.ts +7 -0
  10. package/commands/project/installApp.js +181 -0
  11. package/commands/project/installDeps.js +2 -0
  12. package/commands/project/upload.d.ts +2 -0
  13. package/commands/project/upload.js +26 -5
  14. package/commands/project.js +2 -0
  15. package/commands/upgrade.js +2 -0
  16. package/lang/en.d.ts +66 -0
  17. package/lang/en.js +73 -7
  18. package/lib/accountTargetDiscovery.d.ts +15 -0
  19. package/lib/accountTargetDiscovery.js +175 -0
  20. package/lib/app/install.d.ts +22 -0
  21. package/lib/app/install.js +42 -0
  22. package/lib/app/logs.js +1 -1
  23. package/lib/auth/awaitPersonalAccessKeyOverWebsocket.js +3 -2
  24. package/lib/doctor/Doctor.d.ts +1 -0
  25. package/lib/doctor/Doctor.js +26 -0
  26. package/lib/getStartedV2Actions.js +2 -1
  27. package/lib/mcp/clients.d.ts +13 -0
  28. package/lib/mcp/clients.js +55 -0
  29. package/lib/mcp/promotion.d.ts +3 -0
  30. package/lib/mcp/promotion.js +109 -0
  31. package/lib/mcp/setup.d.ts +3 -2
  32. package/lib/mcp/setup.js +17 -12
  33. package/lib/projects/installApp.d.ts +58 -0
  34. package/lib/projects/installApp.js +283 -0
  35. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -3
  36. package/lib/projects/localDev/AppDevModeInterface.js +22 -44
  37. package/lib/projects/localDev/LocalDevProcess.js +4 -2
  38. package/lib/projects/localDev/helpers/project.js +3 -2
  39. package/lib/projects/ui.d.ts +1 -0
  40. package/lib/projects/ui.js +14 -0
  41. package/lib/projects/upload.d.ts +10 -2
  42. package/lib/projects/upload.js +15 -4
  43. package/lib/prompts/projectDevTargetAccountPrompt.js +3 -2
  44. package/lib/theme/cmsDevServerProcess.js +1 -1
  45. package/lib/usageTracking.d.ts +1 -0
  46. package/lib/usageTracking.js +9 -0
  47. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  48. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  49. package/package.json +5 -5
  50. package/types/AccountTargets.d.ts +37 -0
  51. package/types/AccountTargets.js +12 -0
  52. package/types/LocalDev.d.ts +3 -2
  53. package/types/ProjectComponents.d.ts +2 -1
@@ -0,0 +1,109 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { getStateValue, setStateValue, } from '@hubspot/local-dev-lib/config/state';
5
+ import { STATE_FLAGS } from '@hubspot/local-dev-lib/constants/config';
6
+ import { commands } from '../../lang/en.js';
7
+ import { debugError } from '../errorHandlers/index.js';
8
+ import { uiLogger } from '../ui/logger.js';
9
+ import { trackMcpPromotionShown } from '../usageTracking.js';
10
+ import { MCP_CLIENTS, MCP_SERVER_NAME } from './clients.js';
11
+ const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ function configContainsMcpServer(value) {
16
+ if (typeof value === 'string') {
17
+ return value === MCP_SERVER_NAME;
18
+ }
19
+ if (Array.isArray(value)) {
20
+ return value.some(configContainsMcpServer);
21
+ }
22
+ if (!isRecord(value)) {
23
+ return false;
24
+ }
25
+ return Object.entries(value).some(([key, childValue]) => {
26
+ return key === MCP_SERVER_NAME || configContainsMcpServer(childValue);
27
+ });
28
+ }
29
+ function textConfigContainsMcpServer(configContent) {
30
+ return configContent.includes(MCP_SERVER_NAME);
31
+ }
32
+ function jsonConfigContainsMcpServer(configContent) {
33
+ const parsedConfig = JSON.parse(configContent);
34
+ return configContainsMcpServer(parsedConfig);
35
+ }
36
+ export function detectConfiguredMcpClients() {
37
+ const homeDirectory = os.homedir();
38
+ return MCP_CLIENTS.reduce((clients, client) => {
39
+ try {
40
+ const configPath = path.join(homeDirectory, ...client.detection.pathSegments);
41
+ if (!fs.existsSync(configPath)) {
42
+ return clients;
43
+ }
44
+ const configContent = fs.readFileSync(configPath, 'utf8');
45
+ const hasMcpServer = client.detection.type === 'json'
46
+ ? jsonConfigContainsMcpServer(configContent)
47
+ : textConfigContainsMcpServer(configContent);
48
+ if (hasMcpServer) {
49
+ clients.push(client.id);
50
+ }
51
+ }
52
+ catch (e) {
53
+ debugError(e);
54
+ return clients;
55
+ }
56
+ return clients;
57
+ }, []);
58
+ }
59
+ function isScriptSafeSuppressed() {
60
+ return Boolean(process.env.CI || process.env.HUBSPOT_MCP_AI_AGENT || !process.stdout.isTTY);
61
+ }
62
+ function hasRecentPromotion(lastShownAt, now, cooldownMs) {
63
+ if (!lastShownAt) {
64
+ return false;
65
+ }
66
+ const lastShownTime = new Date(lastShownAt).getTime();
67
+ if (Number.isNaN(lastShownTime)) {
68
+ return false;
69
+ }
70
+ return now.getTime() - lastShownTime < cooldownMs;
71
+ }
72
+ function getMcpPromotionLastShownAt() {
73
+ return getStateValue(STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT);
74
+ }
75
+ export async function shouldShowMcpPromotion() {
76
+ if (isScriptSafeSuppressed()) {
77
+ return false;
78
+ }
79
+ try {
80
+ const mcpTotalToolCalls = getStateValue(STATE_FLAGS.MCP_TOTAL_TOOL_CALLS);
81
+ if (typeof mcpTotalToolCalls === 'number' && mcpTotalToolCalls > 0) {
82
+ return false;
83
+ }
84
+ if (detectConfiguredMcpClients().length > 0) {
85
+ return false;
86
+ }
87
+ return !hasRecentPromotion(getMcpPromotionLastShownAt(), new Date(), ONE_WEEK_IN_MS);
88
+ }
89
+ catch (e) {
90
+ debugError(e);
91
+ return false;
92
+ }
93
+ }
94
+ function setMcpPromotionShownAt() {
95
+ setStateValue(STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT, new Date().toISOString());
96
+ }
97
+ export async function showMcpPromotionNudge(commandName) {
98
+ try {
99
+ if (!(await shouldShowMcpPromotion())) {
100
+ return;
101
+ }
102
+ setMcpPromotionShownAt();
103
+ uiLogger.info(commands.mcp.promotion.activeNudge);
104
+ await trackMcpPromotionShown(commandName || undefined);
105
+ }
106
+ catch (e) {
107
+ debugError(e);
108
+ }
109
+ }
@@ -1,13 +1,14 @@
1
+ import { McpClientId } from './clients.js';
1
2
  export declare const supportedTools: {
2
3
  name: string;
3
- value: string;
4
+ value: McpClientId;
4
5
  }[];
5
6
  interface McpCommand {
6
7
  command: string;
7
8
  args: string[];
8
9
  env?: Record<string, string>;
9
10
  }
10
- export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
11
+ export declare function configureMcpServer(targets: string[] | undefined): Promise<string[]>;
11
12
  export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
12
13
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
13
14
  export declare function setupCursor(mcpCommand?: McpCommand): boolean;
package/lib/mcp/setup.js CHANGED
@@ -4,30 +4,35 @@ import { promptUser } from '../prompts/promptUtils.js';
4
4
  import SpinniesManager from '../ui/SpinniesManager.js';
5
5
  import { logError, getErrorMessage } from '../errorHandlers/index.js';
6
6
  import { execAsync } from '../../mcp-server/utils/command.js';
7
+ import { MCP_CLIENTS, MCP_SERVER_NAME, getMcpClientPathSegments, } from './clients.js';
7
8
  import path from 'path';
8
9
  import os from 'os';
9
10
  import fs from 'fs-extra';
10
11
  import { existsSync } from 'fs';
11
- const mcpServerName = 'HubSpotDev';
12
+ const mcpServerName = MCP_SERVER_NAME;
12
13
  const claudeCode = 'claude';
13
14
  const windsurf = 'windsurf';
14
15
  const cursor = 'cursor';
15
16
  const vscode = 'vscode';
16
17
  const codex = 'codex';
17
18
  const gemini = 'gemini';
18
- export const supportedTools = [
19
- { name: commands.mcp.setup.codex, value: codex },
20
- { name: commands.mcp.setup.claudeCode, value: claudeCode },
21
- { name: commands.mcp.setup.cursor, value: cursor },
22
- { name: commands.mcp.setup.gemini, value: gemini },
23
- { name: commands.mcp.setup.vsCode, value: vscode },
24
- { name: commands.mcp.setup.windsurf, value: windsurf },
25
- ];
19
+ const clientLabels = {
20
+ codex: commands.mcp.setup.codex,
21
+ claude: commands.mcp.setup.claudeCode,
22
+ cursor: commands.mcp.setup.cursor,
23
+ gemini: commands.mcp.setup.gemini,
24
+ vscode: commands.mcp.setup.vsCode,
25
+ windsurf: commands.mcp.setup.windsurf,
26
+ };
27
+ export const supportedTools = MCP_CLIENTS.map(client => ({
28
+ name: clientLabels[client.id],
29
+ value: client.id,
30
+ }));
26
31
  const defaultMcpCommand = {
27
32
  command: 'hs',
28
33
  args: ['mcp', 'start'],
29
34
  };
30
- export async function addMcpServerToConfig(targets) {
35
+ export async function configureMcpServer(targets) {
31
36
  try {
32
37
  let derivedTargets = [];
33
38
  if (!targets || targets.length === 0) {
@@ -247,7 +252,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
247
252
  }
248
253
  }
249
254
  export function setupCursor(mcpCommand = defaultMcpCommand) {
250
- const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
255
+ const cursorConfigPath = path.join(os.homedir(), ...getMcpClientPathSegments(cursor));
251
256
  return setupMcpConfigFile({
252
257
  configPath: cursorConfigPath,
253
258
  configuringMessage: commands.mcp.setup.spinners.configuringCursor,
@@ -257,7 +262,7 @@ export function setupCursor(mcpCommand = defaultMcpCommand) {
257
262
  });
258
263
  }
259
264
  export function setupWindsurf(mcpCommand = defaultMcpCommand) {
260
- const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
265
+ const windsurfConfigPath = path.join(os.homedir(), ...getMcpClientPathSegments(windsurf));
261
266
  return setupMcpConfigFile({
262
267
  configPath: windsurfConfigPath,
263
268
  configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
@@ -0,0 +1,58 @@
1
+ import type { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ import type { AppInstallationState, AppIRNode } from '../../types/ProjectComponents.js';
3
+ import type { ProjectConfig } from '../../types/Projects.js';
4
+ import { type ScopeGroup } from '../app/install.js';
5
+ export type AppInstallationData = {
6
+ appId?: number;
7
+ isInstalledWithScopeGroups: boolean;
8
+ previouslyAuthorizedScopeGroups: ScopeGroup[];
9
+ };
10
+ export type AppMetadata = {
11
+ appId: number;
12
+ scopeGroupIds: number[];
13
+ ownerPortalId: number;
14
+ };
15
+ type ConfirmInstallAppOptions = {
16
+ appName: string;
17
+ targetAccountId: number;
18
+ force: boolean;
19
+ needsReinstall: boolean;
20
+ };
21
+ type InstallStaticAuthAppOptions = {
22
+ appId: number;
23
+ appNode: AppIRNode;
24
+ appName: string;
25
+ targetAccountId: number;
26
+ targetAccountConfig?: HubSpotConfigAccount;
27
+ projectId: number;
28
+ projectName: string;
29
+ scopeGroupIds: number[];
30
+ ownerPortalId: number;
31
+ installationState: AppInstallationState;
32
+ formatOutputAsJson: boolean;
33
+ };
34
+ type UploadAndDeployOptions = {
35
+ accountId: number;
36
+ projectConfig: ProjectConfig;
37
+ projectDir: string;
38
+ profile: string | undefined;
39
+ force: boolean;
40
+ formatOutputAsJson: boolean;
41
+ };
42
+ export declare function resolveProjectAccountId(derivedAccountId: number, projectConfig: ProjectConfig, projectDir: string, profileOption: string | undefined, useEnvOption: boolean): Promise<{
43
+ accountId: number;
44
+ profileName?: string;
45
+ } | null>;
46
+ export declare function loadInstallableAppNode(projectConfig: ProjectConfig, projectDir: string, accountId: number, force?: boolean): Promise<AppIRNode | null>;
47
+ export declare function resolveValidInstallAccount(targetAccountId: number, force: boolean, formatOutputAsJson: boolean): Promise<number | null>;
48
+ export declare function resolveProjectId(options: UploadAndDeployOptions): Promise<number | null>;
49
+ export declare function resolveAppMetadata(options: UploadAndDeployOptions & {
50
+ appId: number | undefined;
51
+ projectId: number;
52
+ appUid: string;
53
+ appName: string;
54
+ }): Promise<AppMetadata | null>;
55
+ export declare function fetchProjectAppInstallationData(targetAccountId: number, projectId: number, appNode: AppIRNode, projectConfig: ProjectConfig): Promise<AppInstallationData | null>;
56
+ export declare function confirmInstallAppAction({ appName, targetAccountId, force, needsReinstall, }: ConfirmInstallAppOptions): Promise<boolean>;
57
+ export declare function installStaticAuthAppForAccount({ appId, appNode, appName, targetAccountId, targetAccountConfig, projectId, projectName, scopeGroupIds, ownerPortalId, installationState, formatOutputAsJson, }: InstallStaticAuthAppOptions): Promise<boolean>;
58
+ export {};
@@ -0,0 +1,283 @@
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 { warnAboutSkippedHsMetaFiles } from './ui.js';
10
+ import { isAppDeveloperAccount } from '../accountTypes.js';
11
+ import { ApiErrorContext, debugError, logError, } from '../errorHandlers/index.js';
12
+ import { confirmPrompt, listPrompt } from '../prompts/promptUtils.js';
13
+ import { projectProfilePrompt } from '../prompts/projectProfilePrompt.js';
14
+ import { loadProfile } from './projectProfiles.js';
15
+ import { handleProjectUpload } from './upload.js';
16
+ import { pollProjectBuildAndDeploy } from './pollProjectBuildAndDeploy.js';
17
+ import { getAppNodeFromProjectNodes, canUseStaticAuthTestAccountInstall, isPrivateApp, isStaticAuthApp, } from '../app/install.js';
18
+ import { uiLogger } from '../ui/logger.js';
19
+ import { uiAccountDescription } from '../ui/index.js';
20
+ import { getStaticAuthAppInstallUrl } from '../app/urls.js';
21
+ export async function resolveProjectAccountId(derivedAccountId, projectConfig, projectDir, profileOption, useEnvOption) {
22
+ try {
23
+ const profileName = await projectProfilePrompt(projectDir, projectConfig, profileOption, useEnvOption);
24
+ if (profileName) {
25
+ const profile = loadProfile(projectConfig, projectDir, profileName);
26
+ uiLogger.log(commands.project.installApp.profileMessage(profileName, profile.accountId));
27
+ uiLogger.log('');
28
+ return { accountId: profile.accountId, profileName };
29
+ }
30
+ return { accountId: derivedAccountId };
31
+ }
32
+ catch (error) {
33
+ logError(error);
34
+ return null;
35
+ }
36
+ }
37
+ export async function loadInstallableAppNode(projectConfig, projectDir, accountId, force = false) {
38
+ let appNode;
39
+ try {
40
+ const { intermediateRepresentation, skippedHsMetaFiles } = await translateForLocalDev({
41
+ projectSourceDir: path.join(projectDir, projectConfig.srcDir),
42
+ platformVersion: projectConfig.platformVersion,
43
+ accountId,
44
+ }, { skipValidation: true });
45
+ const shouldContinue = await warnAboutSkippedHsMetaFiles(skippedHsMetaFiles, force, false);
46
+ if (!shouldContinue) {
47
+ return null;
48
+ }
49
+ appNode = getAppNodeFromProjectNodes(intermediateRepresentation.intermediateNodesIndexedByUid);
50
+ }
51
+ catch (error) {
52
+ debugError(error);
53
+ uiLogger.error(commands.project.installApp.errors.failedToParseProject);
54
+ return null;
55
+ }
56
+ if (!appNode) {
57
+ uiLogger.error(commands.project.installApp.errors.noAppInProject);
58
+ return null;
59
+ }
60
+ if (!isStaticAuthApp(appNode)) {
61
+ uiLogger.error(commands.project.installApp.errors.unsupportedAuthType(appNode.config.auth.type));
62
+ return null;
63
+ }
64
+ if (!isPrivateApp(appNode)) {
65
+ uiLogger.error(commands.project.installApp.errors.unsupportedDistribution(appNode.config.distribution || 'unknown'));
66
+ return null;
67
+ }
68
+ return appNode;
69
+ }
70
+ // App developer accounts can't host app installs.
71
+ export async function resolveValidInstallAccount(targetAccountId, force, formatOutputAsJson) {
72
+ const accountConfig = getConfigAccountIfExists(targetAccountId);
73
+ if (!accountConfig || !isAppDeveloperAccount(accountConfig)) {
74
+ return targetAccountId;
75
+ }
76
+ const validAccounts = getAllConfigAccounts().filter(account => !isAppDeveloperAccount(account));
77
+ if (force || formatOutputAsJson || validAccounts.length === 0) {
78
+ uiLogger.error(commands.project.installApp.errors.invalidAppDeveloperAccount(targetAccountId));
79
+ return null;
80
+ }
81
+ uiLogger.log(commands.project.installApp.appDeveloperAccountNotice(targetAccountId));
82
+ return listPrompt(commands.project.installApp.selectInstallAccountPrompt, {
83
+ choices: validAccounts.map(account => ({
84
+ name: uiAccountDescription(account.accountId, false),
85
+ value: account.accountId,
86
+ })),
87
+ });
88
+ }
89
+ async function offerUploadAndDeploy(options) {
90
+ const { accountId, projectConfig, projectDir, profile, force } = options;
91
+ if (!force) {
92
+ if (options.formatOutputAsJson) {
93
+ return false;
94
+ }
95
+ const shouldUpload = await confirmPrompt(commands.project.installApp.uploadAndDeployPrompt(accountId));
96
+ if (!shouldUpload) {
97
+ return false;
98
+ }
99
+ }
100
+ try {
101
+ const { result, uploadError } = await handleProjectUpload({
102
+ accountId,
103
+ projectConfig,
104
+ projectDir,
105
+ callbackFunc: pollProjectBuildAndDeploy,
106
+ isUploadCommand: true,
107
+ sendIR: true,
108
+ forceCreate: true,
109
+ profile,
110
+ });
111
+ if (uploadError) {
112
+ logError(uploadError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
113
+ return false;
114
+ }
115
+ return Boolean(result && result.succeeded);
116
+ }
117
+ catch (error) {
118
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
119
+ return false;
120
+ }
121
+ }
122
+ export async function resolveProjectId(options) {
123
+ const { accountId, projectConfig } = options;
124
+ try {
125
+ const response = await fetchProject(accountId, projectConfig.name);
126
+ return response.data.id;
127
+ }
128
+ catch (error) {
129
+ if (!isHubSpotHttpError(error) || error.status !== 404) {
130
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
131
+ return null;
132
+ }
133
+ const uploaded = await offerUploadAndDeploy(options);
134
+ if (!uploaded) {
135
+ uiLogger.error(commands.project.installApp.errors.projectNotFound(accountId, projectConfig.name));
136
+ return null;
137
+ }
138
+ try {
139
+ const response = await fetchProject(accountId, projectConfig.name);
140
+ return response.data.id;
141
+ }
142
+ catch (retryError) {
143
+ logError(retryError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
144
+ return null;
145
+ }
146
+ }
147
+ }
148
+ export async function resolveAppMetadata(options) {
149
+ const { appId, projectId, appUid, appName, accountId, projectConfig } = options;
150
+ const fetchMetadata = (forceSourceIdLookup = false) => appId && !forceSourceIdLookup
151
+ ? fetchPublicAppMetadata(appId, accountId)
152
+ : fetchAppMetadataBySourceId(projectId, appUid, accountId);
153
+ try {
154
+ const { data } = await fetchMetadata();
155
+ return {
156
+ appId: appId ?? data.id,
157
+ scopeGroupIds: data.scopeGroupIds,
158
+ ownerPortalId: data.portalId,
159
+ };
160
+ }
161
+ catch (error) {
162
+ if (!isHubSpotHttpError(error) || error.status !== 404) {
163
+ logError(error, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
164
+ return null;
165
+ }
166
+ const uploaded = await offerUploadAndDeploy(options);
167
+ if (!uploaded) {
168
+ uiLogger.error(commands.project.installApp.errors.appNotDeployed(appName, accountId));
169
+ return null;
170
+ }
171
+ try {
172
+ const { data } = await fetchMetadata(true);
173
+ return {
174
+ appId: data.id,
175
+ scopeGroupIds: data.scopeGroupIds,
176
+ ownerPortalId: data.portalId,
177
+ };
178
+ }
179
+ catch (retryError) {
180
+ logError(retryError, new ApiErrorContext({ accountId, projectName: projectConfig.name }));
181
+ return null;
182
+ }
183
+ }
184
+ }
185
+ export async function fetchProjectAppInstallationData(targetAccountId, projectId, appNode, projectConfig) {
186
+ try {
187
+ const response = await fetchAppInstallationData(targetAccountId, projectId, appNode.uid, appNode.config.auth.requiredScopes, appNode.config.auth.optionalScopes);
188
+ return {
189
+ appId: response.data.appId,
190
+ isInstalledWithScopeGroups: response.data.isInstalledWithScopeGroups,
191
+ previouslyAuthorizedScopeGroups: response.data.previouslyAuthorizedScopeGroups,
192
+ };
193
+ }
194
+ catch (error) {
195
+ if (isHubSpotHttpError(error) && error.status === 404) {
196
+ return {
197
+ isInstalledWithScopeGroups: false,
198
+ previouslyAuthorizedScopeGroups: [],
199
+ };
200
+ }
201
+ logError(error, new ApiErrorContext({
202
+ accountId: targetAccountId,
203
+ projectName: projectConfig.name,
204
+ }));
205
+ return null;
206
+ }
207
+ }
208
+ export async function confirmInstallAppAction({ appName, targetAccountId, force, needsReinstall, }) {
209
+ if (force) {
210
+ return true;
211
+ }
212
+ if (needsReinstall) {
213
+ uiLogger.log(commands.project.installApp.outdatedScopes(appName, targetAccountId));
214
+ return confirmPrompt(commands.project.installApp.reinstallPrompt);
215
+ }
216
+ return confirmPrompt(commands.project.installApp.installPrompt(appName, targetAccountId));
217
+ }
218
+ export async function installStaticAuthAppForAccount({ appId, appNode, appName, targetAccountId, targetAccountConfig, projectId, projectName, scopeGroupIds, ownerPortalId, installationState, formatOutputAsJson, }) {
219
+ const installingIntoOwnerAccount = ownerPortalId === targetAccountId;
220
+ const useTestAccountInstall = !installingIntoOwnerAccount &&
221
+ canUseStaticAuthTestAccountInstall({
222
+ accountConfig: targetAccountConfig,
223
+ appNode,
224
+ });
225
+ if (!installingIntoOwnerAccount && !useTestAccountInstall) {
226
+ const installUrl = getStaticAuthAppInstallUrl({
227
+ targetAccountId,
228
+ env: (targetAccountConfig?.env || 'prod'),
229
+ appId,
230
+ });
231
+ if (formatOutputAsJson) {
232
+ uiLogger.json({
233
+ appId,
234
+ appUid: appNode.uid,
235
+ accountId: targetAccountId,
236
+ projectId,
237
+ installationState,
238
+ installed: false,
239
+ reinstalled: false,
240
+ installUrl,
241
+ error: commands.project.installApp.jsonErrors.automaticInstallUnavailable,
242
+ });
243
+ }
244
+ else {
245
+ uiLogger.error(commands.project.installApp.errors.automaticInstallUnavailable(appName, targetAccountId));
246
+ uiLogger.log(commands.project.installApp.installFromBrowser(installUrl));
247
+ }
248
+ return false;
249
+ }
250
+ try {
251
+ if (useTestAccountInstall) {
252
+ await installStaticAuthAppOnTestAccount(appId, targetAccountId, scopeGroupIds);
253
+ }
254
+ else {
255
+ await installStaticAuthAppOnCurrentAccount(appId, targetAccountId, scopeGroupIds);
256
+ }
257
+ return true;
258
+ }
259
+ catch (error) {
260
+ if (formatOutputAsJson) {
261
+ uiLogger.json({
262
+ appId,
263
+ appUid: appNode.uid,
264
+ accountId: targetAccountId,
265
+ projectId,
266
+ installationState,
267
+ installed: false,
268
+ reinstalled: false,
269
+ error: isHubSpotHttpError(error)
270
+ ? error.message
271
+ : commands.project.installApp.jsonErrors.installFailed(appName, targetAccountId),
272
+ });
273
+ }
274
+ else {
275
+ uiLogger.error(commands.project.installApp.errors.installFailed(appName, targetAccountId));
276
+ logError(error, new ApiErrorContext({
277
+ accountId: targetAccountId,
278
+ projectName,
279
+ }));
280
+ }
281
+ return false;
282
+ }
283
+ }
@@ -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);