@hubspot/cli 8.2.0-experimental.0 → 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.
- package/commands/account/auth.js +2 -0
- package/commands/app/logs.js +4 -0
- package/commands/getStarted.js +2 -0
- package/commands/init.js +2 -0
- package/commands/mcp/setup.js +2 -2
- package/commands/project/create.js +2 -0
- 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.js +4 -0
- package/commands/project.js +2 -0
- package/commands/upgrade.js +2 -0
- package/lang/en.d.ts +58 -0
- package/lang/en.js +71 -2
- 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/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 +278 -0
- package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -3
- package/lib/projects/localDev/AppDevModeInterface.js +22 -44
- package/lib/projects/localDev/helpers/project.d.ts +4 -3
- package/lib/projects/localDev/helpers/project.js +46 -12
- 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/package.json +4 -4
- 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
- package/types/Projects.d.ts +5 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { getConfigAccountIfExists, getConfigDefaultAccountIfExists, } from '@hubspot/local-dev-lib/config';
|
|
3
|
+
import { getHsSettingsFileIfExists } from '@hubspot/local-dev-lib/config/hsSettings';
|
|
4
|
+
import { ENVIRONMENT_VARIABLES } from '@hubspot/local-dev-lib/constants/config';
|
|
5
|
+
import { getAllHsProfiles, loadHsProfileFile, } from '@hubspot/project-parsing-lib/profiles';
|
|
6
|
+
import { isTestAccountOrSandbox } from './accountTypes.js';
|
|
7
|
+
import { isDirectoryLinked } from './link/linkUtils.js';
|
|
8
|
+
import { lib } from '../lang/en.js';
|
|
9
|
+
import { ACCOUNT_TARGET_CATEGORIES, ACCOUNT_TARGET_SELECTION_SOURCES, } from '../types/AccountTargets.js';
|
|
10
|
+
function categorizeAccount(account) {
|
|
11
|
+
if (isTestAccountOrSandbox(account)) {
|
|
12
|
+
return ACCOUNT_TARGET_CATEGORIES.RECOMMENDED_TESTING;
|
|
13
|
+
}
|
|
14
|
+
if (!account.accountType) {
|
|
15
|
+
return ACCOUNT_TARGET_CATEGORIES.UNKNOWN;
|
|
16
|
+
}
|
|
17
|
+
return ACCOUNT_TARGET_CATEGORIES.PRODUCTION_WITH_CARE;
|
|
18
|
+
}
|
|
19
|
+
function candidateFromAccount(account, source, profileName) {
|
|
20
|
+
return {
|
|
21
|
+
accountId: account.accountId,
|
|
22
|
+
accountName: account.name,
|
|
23
|
+
accountType: account.accountType,
|
|
24
|
+
environment: account.env || undefined,
|
|
25
|
+
source,
|
|
26
|
+
category: categorizeAccount(account),
|
|
27
|
+
...(profileName ? { profileName } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function candidateFromAccountId(accountId, source, profileName) {
|
|
31
|
+
const account = getConfigAccountIfExists(accountId);
|
|
32
|
+
if (account) {
|
|
33
|
+
return candidateFromAccount(account, source, profileName);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
accountId,
|
|
37
|
+
source,
|
|
38
|
+
category: ACCOUNT_TARGET_CATEGORIES.UNKNOWN,
|
|
39
|
+
...(profileName ? { profileName } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function dedupeByAccountId(candidates) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return candidates.filter(candidate => {
|
|
45
|
+
if (seen.has(candidate.accountId)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
seen.add(candidate.accountId);
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function gatherProfileCandidates(projectDir, projectConfig) {
|
|
53
|
+
const projectSourceDir = path.join(projectDir, projectConfig.srcDir);
|
|
54
|
+
const profileNames = await getAllHsProfiles(projectSourceDir);
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (const name of profileNames) {
|
|
57
|
+
let profile;
|
|
58
|
+
try {
|
|
59
|
+
profile = loadHsProfileFile(projectSourceDir, name);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (profile && profile.accountId) {
|
|
65
|
+
candidates.push(candidateFromAccountId(profile.accountId, ACCOUNT_TARGET_SELECTION_SOURCES.PROFILE, name));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return candidates;
|
|
69
|
+
}
|
|
70
|
+
function resolveProfileRecommendation(profileCandidates, explicitProfileName) {
|
|
71
|
+
if (explicitProfileName) {
|
|
72
|
+
return profileCandidates.find(candidate => candidate.profileName === explicitProfileName);
|
|
73
|
+
}
|
|
74
|
+
if (profileCandidates.length === 1) {
|
|
75
|
+
return profileCandidates[0];
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function resolveNonProfileRecommendation({ explicitCandidate, envCandidate, linkedCandidates, linkedDefaultAccountId, defaultCandidate, }) {
|
|
80
|
+
if (explicitCandidate) {
|
|
81
|
+
return explicitCandidate;
|
|
82
|
+
}
|
|
83
|
+
if (envCandidate) {
|
|
84
|
+
return envCandidate;
|
|
85
|
+
}
|
|
86
|
+
if (linkedCandidates.length > 0) {
|
|
87
|
+
if (linkedDefaultAccountId !== undefined) {
|
|
88
|
+
const linkedDefault = linkedCandidates.find(candidate => candidate.accountId === linkedDefaultAccountId);
|
|
89
|
+
if (linkedDefault) {
|
|
90
|
+
return linkedDefault;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (linkedCandidates.length === 1) {
|
|
94
|
+
return linkedCandidates[0];
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return defaultCandidate;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Discovers the candidate target accounts for a command, plus the recommended
|
|
102
|
+
* one to use when a single account is the obvious choice.
|
|
103
|
+
*
|
|
104
|
+
* Profiles take precedence: when the project defines profiles, the candidates
|
|
105
|
+
* are the profile accounts only. Otherwise they are ranked explicit account,
|
|
106
|
+
* then env config, then linked directory, then global default.
|
|
107
|
+
*
|
|
108
|
+
* Takes plain options (not yargs argv) and never prompts or exits, so both the
|
|
109
|
+
* CLI and MCP can call it. `recommended` is omitted when no account is the
|
|
110
|
+
* obvious choice (for example, several profiles). Throws when an explicit
|
|
111
|
+
* account is provided but not found in the config.
|
|
112
|
+
*/
|
|
113
|
+
export async function discoverAccountTargets(options = {}) {
|
|
114
|
+
const { explicitAccount, useEnv, profileName, projectDir, projectConfig } = options;
|
|
115
|
+
if (projectDir && projectConfig) {
|
|
116
|
+
const profileCandidates = await gatherProfileCandidates(projectDir, projectConfig);
|
|
117
|
+
if (profileCandidates.length > 0) {
|
|
118
|
+
return {
|
|
119
|
+
candidates: dedupeByAccountId(profileCandidates),
|
|
120
|
+
recommended: resolveProfileRecommendation(profileCandidates, profileName),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const envConfigActive = Boolean(useEnv) ||
|
|
125
|
+
process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] ===
|
|
126
|
+
'true';
|
|
127
|
+
const ordered = [];
|
|
128
|
+
let explicitCandidate;
|
|
129
|
+
if (explicitAccount !== undefined && explicitAccount !== '') {
|
|
130
|
+
const account = getConfigAccountIfExists(explicitAccount);
|
|
131
|
+
if (!account) {
|
|
132
|
+
throw new Error(lib.accountTargetDiscovery.errors.explicitAccountNotFound(explicitAccount));
|
|
133
|
+
}
|
|
134
|
+
explicitCandidate = candidateFromAccount(account, ACCOUNT_TARGET_SELECTION_SOURCES.EXPLICIT_ACCOUNT);
|
|
135
|
+
ordered.push(explicitCandidate);
|
|
136
|
+
}
|
|
137
|
+
let envCandidate;
|
|
138
|
+
if (envConfigActive) {
|
|
139
|
+
const envAccount = getConfigDefaultAccountIfExists();
|
|
140
|
+
if (envAccount) {
|
|
141
|
+
envCandidate = candidateFromAccount(envAccount, ACCOUNT_TARGET_SELECTION_SOURCES.ENV_CONFIG);
|
|
142
|
+
ordered.push(envCandidate);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const settings = getHsSettingsFileIfExists();
|
|
146
|
+
const directoryIsLinked = isDirectoryLinked(settings);
|
|
147
|
+
const linkedCandidates = [];
|
|
148
|
+
if (directoryIsLinked) {
|
|
149
|
+
for (const accountId of settings.accounts) {
|
|
150
|
+
const candidate = candidateFromAccountId(accountId, ACCOUNT_TARGET_SELECTION_SOURCES.LINKED_DIRECTORY);
|
|
151
|
+
linkedCandidates.push(candidate);
|
|
152
|
+
ordered.push(candidate);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let defaultCandidate;
|
|
156
|
+
if (!envConfigActive) {
|
|
157
|
+
const defaultAccount = getConfigDefaultAccountIfExists();
|
|
158
|
+
if (defaultAccount) {
|
|
159
|
+
defaultCandidate = candidateFromAccount(defaultAccount, ACCOUNT_TARGET_SELECTION_SOURCES.GLOBAL_DEFAULT);
|
|
160
|
+
ordered.push(defaultCandidate);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
candidates: dedupeByAccountId(ordered),
|
|
165
|
+
recommended: resolveNonProfileRecommendation({
|
|
166
|
+
explicitCandidate,
|
|
167
|
+
envCandidate,
|
|
168
|
+
linkedCandidates,
|
|
169
|
+
linkedDefaultAccountId: directoryIsLinked
|
|
170
|
+
? settings.localDefaultAccount
|
|
171
|
+
: undefined,
|
|
172
|
+
defaultCandidate,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
|
|
2
|
+
import type { IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/translate';
|
|
3
|
+
import type { AppInstallationState, AppIRNode } from '../../types/ProjectComponents.js';
|
|
4
|
+
export type ScopeGroup = {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
type CanUseStaticAuthTestAccountInstallOptions = {
|
|
9
|
+
accountConfig?: HubSpotConfigAccount;
|
|
10
|
+
appNode?: AppIRNode | null;
|
|
11
|
+
parentAccountId?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function getAppNodeFromProjectNodes(projectNodes: {
|
|
14
|
+
[key: string]: IntermediateRepresentationNodeLocalDev;
|
|
15
|
+
}): AppIRNode | null;
|
|
16
|
+
export declare function isStaticAuthApp(appNode?: AppIRNode | null): boolean;
|
|
17
|
+
export declare function isOAuthApp(appNode?: AppIRNode | null): boolean;
|
|
18
|
+
export declare function isPrivateApp(appNode?: AppIRNode | null): boolean;
|
|
19
|
+
export declare function isMarketplaceApp(appNode?: AppIRNode | null): boolean;
|
|
20
|
+
export declare function getAppInstallationState(isInstalledWithScopeGroups: boolean, previouslyAuthorizedScopeGroups: ScopeGroup[]): AppInstallationState;
|
|
21
|
+
export declare function canUseStaticAuthTestAccountInstall({ accountConfig, appNode, parentAccountId, }: CanUseStaticAuthTestAccountInstallOptions): boolean;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, APP_INSTALLATION_STATES, } from '../constants.js';
|
|
2
|
+
import { isDeveloperTestAccount, isSandbox } from '../accountTypes.js';
|
|
3
|
+
import { isAppIRNode } from '../projects/structure.js';
|
|
4
|
+
export function getAppNodeFromProjectNodes(projectNodes) {
|
|
5
|
+
return Object.values(projectNodes).find(isAppIRNode) || null;
|
|
6
|
+
}
|
|
7
|
+
export function isStaticAuthApp(appNode) {
|
|
8
|
+
return appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.STATIC;
|
|
9
|
+
}
|
|
10
|
+
export function isOAuthApp(appNode) {
|
|
11
|
+
return appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.OAUTH;
|
|
12
|
+
}
|
|
13
|
+
export function isPrivateApp(appNode) {
|
|
14
|
+
return (appNode?.config.distribution?.toLowerCase() ===
|
|
15
|
+
APP_DISTRIBUTION_TYPES.PRIVATE);
|
|
16
|
+
}
|
|
17
|
+
export function isMarketplaceApp(appNode) {
|
|
18
|
+
return (appNode?.config.distribution?.toLowerCase() ===
|
|
19
|
+
APP_DISTRIBUTION_TYPES.MARKETPLACE);
|
|
20
|
+
}
|
|
21
|
+
export function getAppInstallationState(isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups) {
|
|
22
|
+
if (isInstalledWithScopeGroups) {
|
|
23
|
+
return APP_INSTALLATION_STATES.INSTALLED;
|
|
24
|
+
}
|
|
25
|
+
if (previouslyAuthorizedScopeGroups.length > 0) {
|
|
26
|
+
return APP_INSTALLATION_STATES.INSTALLED_WITH_OUTDATED_SCOPES;
|
|
27
|
+
}
|
|
28
|
+
return APP_INSTALLATION_STATES.NOT_INSTALLED;
|
|
29
|
+
}
|
|
30
|
+
export function canUseStaticAuthTestAccountInstall({ accountConfig, appNode, parentAccountId, }) {
|
|
31
|
+
if (!accountConfig || !isStaticAuthApp(appNode)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const accountCanUseTestInstall = isDeveloperTestAccount(accountConfig) || isSandbox(accountConfig);
|
|
35
|
+
if (!accountCanUseTestInstall) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (parentAccountId === undefined) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return accountConfig.parentAccountId === parentAccountId;
|
|
42
|
+
}
|
package/lib/app/logs.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'crypto';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
5
5
|
import CLIWebSocketServer from '../CLIWebSocketServer.js';
|
|
6
|
-
import {
|
|
6
|
+
import { ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES, ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES, ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION, } from '../constants.js';
|
|
7
7
|
import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
|
|
8
8
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
9
9
|
import { uiLogger } from '../ui/logger.js';
|
|
@@ -55,7 +55,8 @@ function createMessageHandler(server, cliCallbackToken, resolveWebsocketPak) {
|
|
|
55
55
|
}
|
|
56
56
|
async function awaitKeypressOrWebsocketPak(websocketPakPromise) {
|
|
57
57
|
if (!process.stdin.isTTY) {
|
|
58
|
-
|
|
58
|
+
const pak = await websocketPakPromise;
|
|
59
|
+
return { type: 'pak', pak };
|
|
59
60
|
}
|
|
60
61
|
SpinniesManager.init();
|
|
61
62
|
SpinniesManager.add(SPINNER_ID, {
|
package/lib/doctor/Doctor.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class Doctor {
|
|
|
17
17
|
private checkIfNodeIsInstalled;
|
|
18
18
|
private checkIfNpmIsInstalled;
|
|
19
19
|
private checkCLIVersion;
|
|
20
|
+
private checkMcpReadiness;
|
|
20
21
|
private checkIfNpmInstallRequired;
|
|
21
22
|
private isValidJsonFile;
|
|
22
23
|
private checkProjectConfigJsonFiles;
|
package/lib/doctor/Doctor.js
CHANGED
|
@@ -23,6 +23,8 @@ import { pkg } from '../jsonLoader.js';
|
|
|
23
23
|
import { lib } from '../../lang/en.js';
|
|
24
24
|
import { uiLink } from '../ui/index.js';
|
|
25
25
|
import { isServerRunningAtUrl } from '../http.js';
|
|
26
|
+
import { detectConfiguredMcpClients } from '../mcp/promotion.js';
|
|
27
|
+
import { MCP_CLIENTS } from '../mcp/clients.js';
|
|
26
28
|
import { WEBHOOKS_KEY, APP_KEY } from '@hubspot/project-parsing-lib/constants';
|
|
27
29
|
import { validateProjectConfig } from '../projects/config.js';
|
|
28
30
|
import { validateSourceDirectory, handleTranslate, } from '../projects/upload.js';
|
|
@@ -73,6 +75,7 @@ export class Doctor {
|
|
|
73
75
|
this.checkIfNodeIsInstalled(),
|
|
74
76
|
this.checkIfNpmIsInstalled(),
|
|
75
77
|
this.checkCLIVersion(),
|
|
78
|
+
this.checkMcpReadiness(),
|
|
76
79
|
];
|
|
77
80
|
}
|
|
78
81
|
performNetworkingChecks() {
|
|
@@ -252,6 +255,29 @@ export class Doctor {
|
|
|
252
255
|
});
|
|
253
256
|
}
|
|
254
257
|
}
|
|
258
|
+
async checkMcpReadiness() {
|
|
259
|
+
try {
|
|
260
|
+
const configuredClients = detectConfiguredMcpClients();
|
|
261
|
+
const allClientIds = MCP_CLIENTS.map(c => c.id);
|
|
262
|
+
const unconfiguredClients = allClientIds.filter(id => !configuredClients.includes(id));
|
|
263
|
+
if (configuredClients.length > 0) {
|
|
264
|
+
this.diagnosis?.addCliSection({
|
|
265
|
+
type: 'success',
|
|
266
|
+
message: lib.doctor.mcpChecks.configured(configuredClients),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (unconfiguredClients.length > 0) {
|
|
270
|
+
this.diagnosis?.addCliSection({
|
|
271
|
+
type: 'warning',
|
|
272
|
+
message: lib.doctor.mcpChecks.notConfigured(unconfiguredClients),
|
|
273
|
+
secondaryMessaging: lib.doctor.mcpChecks.notConfiguredSecondary,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
uiLogger.debug(getErrorMessage(e));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
255
281
|
async checkIfNpmInstallRequired() {
|
|
256
282
|
let foundError = false;
|
|
257
283
|
for (const packageFile of this.diagnosticInfo?.packageFiles || []) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const MCP_SERVER_NAME = "HubSpotDev";
|
|
2
|
+
export type McpClientId = 'codex' | 'claude' | 'cursor' | 'gemini' | 'vscode' | 'windsurf';
|
|
3
|
+
type McpClientDetection = {
|
|
4
|
+
type: 'json' | 'text';
|
|
5
|
+
pathSegments: string[];
|
|
6
|
+
};
|
|
7
|
+
export type McpClient = {
|
|
8
|
+
id: McpClientId;
|
|
9
|
+
detection: McpClientDetection;
|
|
10
|
+
};
|
|
11
|
+
export declare const MCP_CLIENTS: McpClient[];
|
|
12
|
+
export declare function getMcpClientPathSegments(id: McpClientId): string[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const MCP_SERVER_NAME = 'HubSpotDev';
|
|
2
|
+
function getVsCodeMcpConfigPathSegments() {
|
|
3
|
+
if (process.platform === 'darwin') {
|
|
4
|
+
return ['Library', 'Application Support', 'Code', 'User', 'mcp.json'];
|
|
5
|
+
}
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
return ['AppData', 'Roaming', 'Code', 'User', 'mcp.json'];
|
|
8
|
+
}
|
|
9
|
+
return ['.config', 'Code', 'User', 'mcp.json'];
|
|
10
|
+
}
|
|
11
|
+
export const MCP_CLIENTS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'codex',
|
|
14
|
+
detection: {
|
|
15
|
+
type: 'text',
|
|
16
|
+
pathSegments: ['.codex', 'config.toml'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'claude',
|
|
21
|
+
detection: { type: 'json', pathSegments: ['.claude.json'] },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'cursor',
|
|
25
|
+
detection: { type: 'json', pathSegments: ['.cursor', 'mcp.json'] },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'gemini',
|
|
29
|
+
detection: {
|
|
30
|
+
type: 'json',
|
|
31
|
+
pathSegments: ['.gemini', 'settings.json'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'vscode',
|
|
36
|
+
detection: {
|
|
37
|
+
type: 'json',
|
|
38
|
+
pathSegments: getVsCodeMcpConfigPathSegments(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'windsurf',
|
|
43
|
+
detection: {
|
|
44
|
+
type: 'json',
|
|
45
|
+
pathSegments: ['.codeium', 'windsurf', 'mcp_config.json'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
export function getMcpClientPathSegments(id) {
|
|
50
|
+
const client = MCP_CLIENTS.find(c => c.id === id);
|
|
51
|
+
if (!client) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return client.detection.pathSegments;
|
|
55
|
+
}
|
|
@@ -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): 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 {};
|