@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.
- package/commands/account/auth.js +2 -0
- package/commands/app/logs.js +4 -0
- package/commands/getStarted.js +3 -0
- package/commands/init.js +2 -0
- package/commands/mcp/setup.js +2 -2
- package/commands/project/appInstallStatus.js +2 -2
- package/commands/project/create.js +2 -0
- package/commands/project/dev/unifiedFlow.js +3 -1
- package/commands/project/installApp.d.ts +7 -0
- package/commands/project/installApp.js +181 -0
- package/commands/project/installDeps.js +2 -0
- package/commands/project/upload.d.ts +2 -0
- package/commands/project/upload.js +26 -5
- package/commands/project.js +2 -0
- package/commands/upgrade.js +2 -0
- package/lang/en.d.ts +66 -0
- package/lang/en.js +73 -7
- package/lib/accountTargetDiscovery.d.ts +15 -0
- package/lib/accountTargetDiscovery.js +175 -0
- package/lib/app/install.d.ts +22 -0
- package/lib/app/install.js +42 -0
- package/lib/app/logs.js +1 -1
- package/lib/auth/awaitPersonalAccessKeyOverWebsocket.js +3 -2
- package/lib/doctor/Doctor.d.ts +1 -0
- package/lib/doctor/Doctor.js +26 -0
- package/lib/getStartedV2Actions.js +2 -1
- package/lib/mcp/clients.d.ts +13 -0
- package/lib/mcp/clients.js +55 -0
- package/lib/mcp/promotion.d.ts +3 -0
- package/lib/mcp/promotion.js +109 -0
- package/lib/mcp/setup.d.ts +3 -2
- package/lib/mcp/setup.js +17 -12
- package/lib/projects/installApp.d.ts +58 -0
- package/lib/projects/installApp.js +283 -0
- package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -3
- package/lib/projects/localDev/AppDevModeInterface.js +22 -44
- package/lib/projects/localDev/LocalDevProcess.js +4 -2
- package/lib/projects/localDev/helpers/project.js +3 -2
- package/lib/projects/ui.d.ts +1 -0
- package/lib/projects/ui.js +14 -0
- package/lib/projects/upload.d.ts +10 -2
- package/lib/projects/upload.js +15 -4
- package/lib/prompts/projectDevTargetAccountPrompt.js +3 -2
- package/lib/theme/cmsDevServerProcess.js +1 -1
- package/lib/usageTracking.d.ts +1 -0
- package/lib/usageTracking.js +9 -0
- package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/package.json +5 -5
- package/types/AccountTargets.d.ts +37 -0
- package/types/AccountTargets.js +12 -0
- package/types/LocalDev.d.ts +3 -2
- 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
|
+
}
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { McpClientId } from './clients.js';
|
|
1
2
|
export declare const supportedTools: {
|
|
2
3
|
name: string;
|
|
3
|
-
value:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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(),
|
|
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(),
|
|
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 =
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
279
|
+
if (isOAuthApp(this.appNode)) {
|
|
302
280
|
await this.validateOauthAppRedirectUrl();
|
|
303
281
|
}
|
|
304
282
|
this.localDevState.addListener('devServerMessage', this.onDevServerMessage);
|