@hubspot/cli 7.7.27-experimental.2 → 7.7.29-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/api/__tests__/migrate.test.js +5 -5
- package/api/migrate.d.ts +10 -4
- package/api/migrate.js +2 -2
- package/commands/__tests__/create.test.js +20 -0
- package/commands/__tests__/testAccount.test.js +2 -0
- package/commands/app/__tests__/migrate.test.js +1 -0
- package/commands/create/function.js +2 -2
- package/commands/create/module.js +2 -2
- package/commands/create/template.js +2 -2
- package/commands/create.js +47 -0
- package/commands/getStarted.js +66 -4
- package/commands/mcp/setup.d.ts +0 -1
- package/commands/mcp/setup.js +3 -11
- package/commands/project/__tests__/create.test.js +57 -0
- package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
- package/commands/project/create.js +6 -1
- package/commands/project/deploy.js +31 -1
- package/commands/project/dev/deprecatedFlow.js +2 -1
- package/commands/project/dev/index.js +32 -12
- package/commands/project/dev/unifiedFlow.d.ts +1 -1
- package/commands/project/dev/unifiedFlow.js +10 -16
- package/commands/project/profile/delete.js +26 -14
- package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
- package/commands/testAccount/__tests__/importData.test.js +93 -0
- package/commands/testAccount/create.js +23 -13
- package/commands/testAccount/importData.d.ts +9 -0
- package/commands/testAccount/importData.js +61 -0
- package/commands/testAccount.js +2 -0
- package/lang/en.d.ts +162 -46
- package/lang/en.js +177 -59
- package/lang/en.lyaml +35 -14
- package/lib/__tests__/importData.test.d.ts +1 -0
- package/lib/__tests__/importData.test.js +89 -0
- package/lib/accountTypes.js +2 -3
- package/lib/app/__tests__/migrate.test.js +81 -36
- package/lib/app/migrate.d.ts +17 -4
- package/lib/app/migrate.js +97 -19
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/hasFeature.d.ts +1 -0
- package/lib/hasFeature.js +7 -0
- package/lib/importData.d.ts +3 -0
- package/lib/importData.js +50 -0
- package/lib/mcp/setup.d.ts +3 -5
- package/lib/mcp/setup.js +39 -139
- package/lib/process.js +15 -4
- package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
- package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
- package/lib/projects/__tests__/components.test.js +164 -7
- package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
- package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
- package/lib/projects/add/v3AddComponent.js +16 -4
- package/lib/projects/components.d.ts +1 -0
- package/lib/projects/components.js +27 -1
- package/lib/projects/localDev/AppDevModeInterface.js +35 -3
- package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
- package/lib/projects/localDev/LocalDevLogger.js +2 -19
- package/lib/projects/localDev/LocalDevManager.js +1 -1
- package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
- package/lib/projects/localDev/LocalDevProcess.js +3 -26
- package/lib/projects/localDev/LocalDevState.d.ts +6 -7
- package/lib/projects/localDev/LocalDevState.js +16 -15
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
- package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
- package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
- package/lib/projects/localDev/helpers/project.d.ts +12 -0
- package/lib/projects/localDev/helpers/project.js +173 -0
- package/lib/projects/urls.d.ts +1 -0
- package/lib/projects/urls.js +4 -0
- package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
- package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
- package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
- package/lib/prompts/confirmImportDataPrompt.js +12 -0
- package/lib/prompts/createFunctionPrompt.d.ts +2 -1
- package/lib/prompts/createFunctionPrompt.js +36 -7
- package/lib/prompts/createModulePrompt.d.ts +2 -1
- package/lib/prompts/createModulePrompt.js +48 -1
- package/lib/prompts/createTemplatePrompt.d.ts +3 -24
- package/lib/prompts/createTemplatePrompt.js +9 -1
- package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
- package/lib/prompts/importDataFilePathPrompt.js +24 -0
- package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
- package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
- package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
- package/lib/prompts/promptUtils.d.ts +7 -1
- package/lib/prompts/promptUtils.js +14 -1
- package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
- package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
- package/lib/ui/index.js +3 -6
- package/lib/ui/removeAnsiCodes.d.ts +1 -0
- package/lib/ui/removeAnsiCodes.js +4 -0
- package/mcp-server/server.js +2 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
- package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
- package/mcp-server/tools/cms/HsListTool.js +58 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
- package/mcp-server/tools/index.d.ts +1 -0
- package/mcp-server/tools/index.js +12 -0
- package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
- package/mcp-server/tools/project/DocFetchTool.js +49 -0
- package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
- package/mcp-server/tools/project/DocsSearchTool.js +62 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +2 -0
- package/mcp-server/tools/project/constants.js +6 -0
- package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
- package/mcp-server/utils/toolUsageTracking.js +2 -1
- package/package.json +9 -6
- package/types/Cms.d.ts +16 -0
- package/types/Cms.js +25 -1
- package/types/LocalDev.d.ts +0 -3
- package/types/Prompts.d.ts +1 -0
- package/ui/index.d.ts +1 -0
- package/ui/index.js +6 -0
package/lib/hasFeature.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { FEATURES } from './constants.js';
|
|
2
2
|
import { ValueOf } from '@hubspot/local-dev-lib/types/Utils';
|
|
3
3
|
export declare function hasFeature(accountId: number, feature: ValueOf<typeof FEATURES>): Promise<boolean>;
|
|
4
|
+
export declare function hasUnfiedAppsAccess(accountId: number): Promise<boolean>;
|
package/lib/hasFeature.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { http } from '@hubspot/local-dev-lib/http';
|
|
1
2
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
2
3
|
export async function hasFeature(accountId, feature) {
|
|
3
4
|
const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
|
|
4
5
|
return Boolean(enabledFeatures[feature]);
|
|
5
6
|
}
|
|
7
|
+
export async function hasUnfiedAppsAccess(accountId) {
|
|
8
|
+
const response = await http.get(accountId, {
|
|
9
|
+
url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
|
|
10
|
+
});
|
|
11
|
+
return Boolean(response.data);
|
|
12
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ImportRequest } from '@hubspot/local-dev-lib/types/Crm';
|
|
2
|
+
export declare function handleImportData(targetAccountId: number, dataFileNames: string[], importRequest: ImportRequest): Promise<void>;
|
|
3
|
+
export declare function handleTargetTestAccountSelectionFlow(derivedAccountId: number, userProvidedAccount: string | number | undefined): Promise<number>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getAccountConfig, getAccountId, getEnv, } from '@hubspot/local-dev-lib/config';
|
|
2
|
+
import { createImport } from '@hubspot/local-dev-lib/api/crm';
|
|
3
|
+
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
4
|
+
import { importDataTestAccountSelectPrompt } from './prompts/importDataTestAccountSelectPrompt.js';
|
|
5
|
+
import { lib } from '../lang/en.js';
|
|
6
|
+
import { isAppDeveloperAccount, isDeveloperTestAccount, isStandardAccount, } from './accountTypes.js';
|
|
7
|
+
import { uiLogger } from './ui/logger.js';
|
|
8
|
+
export async function handleImportData(targetAccountId, dataFileNames, importRequest) {
|
|
9
|
+
try {
|
|
10
|
+
const baseUrl = getHubSpotWebsiteOrigin(getEnv());
|
|
11
|
+
const response = await createImport(targetAccountId, importRequest, dataFileNames);
|
|
12
|
+
const importId = response.data.id;
|
|
13
|
+
uiLogger.success(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
uiLogger.error(lib.importData.errors.failedToImportData);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function handleTargetTestAccountSelectionFlow(derivedAccountId, userProvidedAccount) {
|
|
21
|
+
let targetAccountId = null;
|
|
22
|
+
if (userProvidedAccount) {
|
|
23
|
+
targetAccountId = getAccountId(userProvidedAccount);
|
|
24
|
+
}
|
|
25
|
+
// Only allow users to pass in test accounts
|
|
26
|
+
if (targetAccountId) {
|
|
27
|
+
const testAccount = getAccountConfig(targetAccountId);
|
|
28
|
+
if (!testAccount || !isDeveloperTestAccount(testAccount)) {
|
|
29
|
+
throw new Error(lib.importData.errors.notDeveloperTestAccount);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const targetProjectAccountConfig = getAccountConfig(derivedAccountId);
|
|
34
|
+
if (!targetProjectAccountConfig) {
|
|
35
|
+
throw new Error(lib.importData.errors.noAccountConfig(derivedAccountId));
|
|
36
|
+
}
|
|
37
|
+
if (isDeveloperTestAccount(targetProjectAccountConfig)) {
|
|
38
|
+
targetAccountId = derivedAccountId;
|
|
39
|
+
}
|
|
40
|
+
else if (!isStandardAccount(targetProjectAccountConfig) &&
|
|
41
|
+
!isAppDeveloperAccount(targetProjectAccountConfig)) {
|
|
42
|
+
throw new Error(lib.importData.errors.incorrectAccountType(derivedAccountId));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const { selectedAccountId } = await importDataTestAccountSelectPrompt(derivedAccountId);
|
|
46
|
+
targetAccountId = selectedAccountId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return targetAccountId;
|
|
50
|
+
}
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -15,11 +15,9 @@ interface McpCommand {
|
|
|
15
15
|
command: string;
|
|
16
16
|
args: string[];
|
|
17
17
|
}
|
|
18
|
-
export declare function addMintlifyMcpServer(installTargets: string[]): Promise<void>;
|
|
19
|
-
export declare function setupMintlify(derivedTargets?: string[]): Promise<boolean>;
|
|
20
18
|
export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
|
|
21
|
-
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
22
19
|
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function
|
|
20
|
+
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
|
+
export declare function setupCursor(mcpCommand?: McpCommand): Promise<boolean>;
|
|
22
|
+
export declare function setupWindsurf(mcpCommand?: McpCommand): Promise<boolean>;
|
|
25
23
|
export {};
|
package/lib/mcp/setup.js
CHANGED
|
@@ -4,17 +4,11 @@ import { promptUser } from '../prompts/promptUtils.js';
|
|
|
4
4
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
5
5
|
import { logError } from '../errorHandlers/index.js';
|
|
6
6
|
import { execAsync } from '../../mcp-server/utils/command.js';
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import os from 'os';
|
|
10
|
-
import fs from 'fs-extra';
|
|
11
|
-
import { existsSync } from 'fs';
|
|
12
7
|
const mcpServerName = 'hubspot-cli-mcp';
|
|
13
8
|
const claudeCode = 'claude';
|
|
14
9
|
const windsurf = 'windsurf';
|
|
15
10
|
const cursor = 'cursor';
|
|
16
11
|
const vscode = 'vscode';
|
|
17
|
-
const supportedMintlifyClients = [windsurf, cursor];
|
|
18
12
|
export const supportedTools = [
|
|
19
13
|
{ name: commands.mcp.setup.claudeCode, value: claudeCode },
|
|
20
14
|
{ name: commands.mcp.setup.cursor, value: cursor },
|
|
@@ -25,28 +19,6 @@ const defaultMcpCommand = {
|
|
|
25
19
|
command: 'hs',
|
|
26
20
|
args: ['mcp', 'start'],
|
|
27
21
|
};
|
|
28
|
-
export async function addMintlifyMcpServer(installTargets) {
|
|
29
|
-
await runSetupFunction(() => setupMintlify(installTargets));
|
|
30
|
-
}
|
|
31
|
-
export async function setupMintlify(derivedTargets = supportedMintlifyClients) {
|
|
32
|
-
uiLogger.info(commands.mcp.setup.installingDocSearch);
|
|
33
|
-
uiLogger.log('');
|
|
34
|
-
return new Promise(resolve => {
|
|
35
|
-
const subcommands = ['mint-mcp', 'add', 'hubspot-migration'];
|
|
36
|
-
const docsSearchClients = derivedTargets.filter(target => supportedMintlifyClients.includes(target));
|
|
37
|
-
const childProcess = spawn(`npx`, docsSearchClients && docsSearchClients.length
|
|
38
|
-
? [...subcommands, '--client', ...docsSearchClients]
|
|
39
|
-
: subcommands, {
|
|
40
|
-
stdio: 'inherit',
|
|
41
|
-
});
|
|
42
|
-
childProcess.on('exit', code => {
|
|
43
|
-
if (code !== 0) {
|
|
44
|
-
resolve(false);
|
|
45
|
-
}
|
|
46
|
-
resolve(true);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
22
|
export async function addMcpServerToConfig(targets) {
|
|
51
23
|
try {
|
|
52
24
|
let derivedTargets = [];
|
|
@@ -96,96 +68,6 @@ async function runSetupFunction(func) {
|
|
|
96
68
|
throw new Error();
|
|
97
69
|
}
|
|
98
70
|
}
|
|
99
|
-
function setupMcpConfigFile(config) {
|
|
100
|
-
try {
|
|
101
|
-
SpinniesManager.add('spinner', {
|
|
102
|
-
text: config.configuringMessage,
|
|
103
|
-
});
|
|
104
|
-
if (!existsSync(config.configPath)) {
|
|
105
|
-
fs.writeFileSync(config.configPath, JSON.stringify({}, null, 2));
|
|
106
|
-
}
|
|
107
|
-
let mcpConfig = {};
|
|
108
|
-
let configContent;
|
|
109
|
-
try {
|
|
110
|
-
configContent = fs.readFileSync(config.configPath, 'utf8');
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
SpinniesManager.fail('spinner', {
|
|
114
|
-
text: config.failedMessage,
|
|
115
|
-
});
|
|
116
|
-
logError(error);
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
// In the event the file exists, but is empty, initialize it to and empty object
|
|
121
|
-
if (configContent.trim() === '') {
|
|
122
|
-
mcpConfig = {};
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
mcpConfig = JSON.parse(configContent);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
SpinniesManager.fail('spinner', {
|
|
130
|
-
text: config.failedMessage,
|
|
131
|
-
});
|
|
132
|
-
uiLogger.error(commands.mcp.setup.errors.errorParsingJsonFIle(config.configPath, error instanceof Error ? error.message : `${error}`));
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
// Initialize mcpServers if it doesn't exist
|
|
136
|
-
if (!mcpConfig.mcpServers) {
|
|
137
|
-
mcpConfig.mcpServers = {};
|
|
138
|
-
}
|
|
139
|
-
// Add or update HubSpot CLI MCP server
|
|
140
|
-
mcpConfig.mcpServers[mcpServerName] = {
|
|
141
|
-
...config.mcpCommand,
|
|
142
|
-
};
|
|
143
|
-
// Write the updated config
|
|
144
|
-
fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
|
|
145
|
-
SpinniesManager.succeed('spinner', {
|
|
146
|
-
text: config.configuredMessage,
|
|
147
|
-
});
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
SpinniesManager.fail('spinner', {
|
|
152
|
-
text: config.failedMessage,
|
|
153
|
-
});
|
|
154
|
-
logError(error);
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
159
|
-
try {
|
|
160
|
-
SpinniesManager.add('vsCode', {
|
|
161
|
-
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
162
|
-
});
|
|
163
|
-
const mcpConfig = JSON.stringify({
|
|
164
|
-
name: mcpServerName,
|
|
165
|
-
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
166
|
-
});
|
|
167
|
-
await execAsync(`code --add-mcp '${mcpConfig}'`);
|
|
168
|
-
SpinniesManager.succeed('vsCode', {
|
|
169
|
-
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
170
|
-
});
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (error instanceof Error &&
|
|
175
|
-
error.message.includes('code: command not found')) {
|
|
176
|
-
SpinniesManager.fail('vsCode', {
|
|
177
|
-
text: commands.mcp.setup.spinners.vsCodeNotFound,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
SpinniesManager.fail('vsCode', {
|
|
182
|
-
text: commands.mcp.setup.spinners.failedToConfigureVsCode,
|
|
183
|
-
});
|
|
184
|
-
logError(error);
|
|
185
|
-
}
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
71
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
190
72
|
try {
|
|
191
73
|
SpinniesManager.add('claudeCode', {
|
|
@@ -206,15 +88,14 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
206
88
|
});
|
|
207
89
|
await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
|
|
208
90
|
}
|
|
209
|
-
await execAsync(`claude mcp add-json "${mcpServerName}"
|
|
91
|
+
await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
|
|
210
92
|
SpinniesManager.succeed('claudeCode', {
|
|
211
93
|
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
212
94
|
});
|
|
213
95
|
return true;
|
|
214
96
|
}
|
|
215
97
|
catch (error) {
|
|
216
|
-
if (error instanceof Error &&
|
|
217
|
-
error.message.includes('claude: command not found')) {
|
|
98
|
+
if (error instanceof Error && error.message.includes('claude')) {
|
|
218
99
|
SpinniesManager.fail('claudeCode', {
|
|
219
100
|
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
220
101
|
});
|
|
@@ -236,25 +117,44 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
236
117
|
return false;
|
|
237
118
|
}
|
|
238
119
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
120
|
+
async function setupVsCodeBasedIntegration(commandName, configuringText, configuredText, notFoundText, failedText, mcpCommand = defaultMcpCommand) {
|
|
121
|
+
try {
|
|
122
|
+
SpinniesManager.add(commandName, {
|
|
123
|
+
text: configuringText,
|
|
124
|
+
});
|
|
125
|
+
const mcpConfig = JSON.stringify({
|
|
126
|
+
name: mcpServerName,
|
|
127
|
+
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
128
|
+
});
|
|
129
|
+
await execAsync(`${commandName} --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
130
|
+
SpinniesManager.succeed(commandName, {
|
|
131
|
+
text: configuredText,
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof Error && error.message.includes(commandName)) {
|
|
137
|
+
SpinniesManager.fail(commandName, {
|
|
138
|
+
text: notFoundText,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
SpinniesManager.fail(commandName, {
|
|
143
|
+
text: failedText,
|
|
144
|
+
});
|
|
145
|
+
logError(error);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
151
|
+
return setupVsCodeBasedIntegration('code', commands.mcp.setup.spinners.configuringVsCode, commands.mcp.setup.spinners.configuredVsCode, commands.mcp.setup.spinners.vsCodeNotFound, commands.mcp.setup.spinners.failedToConfigureVsCode, mcpCommand);
|
|
152
|
+
}
|
|
153
|
+
export async function setupCursor(mcpCommand = defaultMcpCommand) {
|
|
154
|
+
return setupVsCodeBasedIntegration('cursor', commands.mcp.setup.spinners.configuringCursor, commands.mcp.setup.spinners.configuredCursor, commands.mcp.setup.spinners.cursorNotFound, commands.mcp.setup.spinners.failedToConfigureCursor, mcpCommand);
|
|
248
155
|
}
|
|
249
|
-
export function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
250
|
-
|
|
251
|
-
return setupMcpConfigFile({
|
|
252
|
-
configPath: windsurfConfigPath,
|
|
253
|
-
configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
|
|
254
|
-
configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
|
|
255
|
-
failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
|
|
256
|
-
mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
|
|
257
|
-
});
|
|
156
|
+
export async function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
157
|
+
return setupVsCodeBasedIntegration('windsurf', commands.mcp.setup.spinners.configuringWindsurf, commands.mcp.setup.spinners.configuredWindsurf, commands.mcp.setup.spinners.windsurfNotFound, commands.mcp.setup.spinners.failedToConfigureWindsurf, mcpCommand);
|
|
258
158
|
}
|
|
259
159
|
function buildCommandWithAgentString(mcpCommand, agent) {
|
|
260
160
|
const mcpCommandCopy = structuredClone(mcpCommand);
|
package/lib/process.js
CHANGED
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
2
|
import { logger, setLogLevel, LOG_LEVEL } from '@hubspot/local-dev-lib/logger';
|
|
3
3
|
import { i18n } from './lang.js';
|
|
4
|
+
import { logError } from './errorHandlers/index.js';
|
|
5
|
+
const SIGHUP = 'SIGHUP';
|
|
6
|
+
const uncaughtException = 'uncaughtException';
|
|
4
7
|
export const TERMINATION_SIGNALS = [
|
|
5
8
|
'beforeExit',
|
|
6
9
|
'SIGINT', // Terminal trying to interrupt (Ctrl + C)
|
|
7
10
|
'SIGUSR1', // Start Debugger User-defined signal 1
|
|
8
11
|
'SIGUSR2', // User-defined signal 2
|
|
9
|
-
|
|
12
|
+
uncaughtException,
|
|
10
13
|
'SIGTERM', // Represents a graceful termination
|
|
11
|
-
|
|
14
|
+
SIGHUP, // Parent terminal has been closed
|
|
12
15
|
];
|
|
13
16
|
export function handleExit(callback) {
|
|
14
17
|
let exitInProgress = false;
|
|
15
18
|
TERMINATION_SIGNALS.forEach(signal => {
|
|
16
19
|
process.removeAllListeners(signal);
|
|
17
|
-
process.on(signal, async () => {
|
|
20
|
+
process.on(signal, async (...args) => {
|
|
18
21
|
// Prevent duplicate exit handling
|
|
19
22
|
if (!exitInProgress) {
|
|
20
23
|
exitInProgress = true;
|
|
21
|
-
const isSIGHUP = signal ===
|
|
24
|
+
const isSIGHUP = signal === SIGHUP;
|
|
22
25
|
// Prevent logs when terminal closes
|
|
23
26
|
if (isSIGHUP) {
|
|
24
27
|
setLogLevel(LOG_LEVEL.NONE);
|
|
25
28
|
}
|
|
26
29
|
logger.debug(i18n(`lib.process.exitDebug`, { signal }));
|
|
30
|
+
if (signal === uncaughtException && args && args.length > 0) {
|
|
31
|
+
try {
|
|
32
|
+
logError(args[0]);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
logger.error(args[0]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
await callback({ isSIGHUP });
|
|
28
39
|
}
|
|
29
40
|
});
|
|
@@ -37,6 +37,7 @@ vi.mock('../../ui/logger');
|
|
|
37
37
|
vi.mock('../../errorHandlers/index');
|
|
38
38
|
vi.mock('../localDev/LocalDevState');
|
|
39
39
|
vi.mock('../localDev/LocalDevLogger');
|
|
40
|
+
vi.mock('../../ui/SpinniesManager');
|
|
40
41
|
describe('AppDevModeInterface', () => {
|
|
41
42
|
let appDevModeInterface;
|
|
42
43
|
let mockLocalDevState;
|
|
@@ -98,10 +99,9 @@ describe('AppDevModeInterface', () => {
|
|
|
98
99
|
getAppDataByUid: vi.fn(),
|
|
99
100
|
setAppDataForUid: vi.fn(),
|
|
100
101
|
addListener: vi.fn(),
|
|
101
|
-
};
|
|
102
|
-
mockLocalDevLogger = {
|
|
103
102
|
addUploadWarning: vi.fn(),
|
|
104
103
|
};
|
|
104
|
+
mockLocalDevLogger = {};
|
|
105
105
|
// Mock constructors
|
|
106
106
|
LocalDevState.mockImplementation(() => mockLocalDevState);
|
|
107
107
|
LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
|
|
@@ -211,7 +211,7 @@ describe('AppDevModeInterface', () => {
|
|
|
211
211
|
};
|
|
212
212
|
await appDevModeInterface.setup({});
|
|
213
213
|
expect(confirmPrompt).toHaveBeenCalled();
|
|
214
|
-
expect(
|
|
214
|
+
expect(mockLocalDevState.addUploadWarning).toHaveBeenCalled();
|
|
215
215
|
});
|
|
216
216
|
it('should exit if user declines marketplace warning', async () => {
|
|
217
217
|
const marketplaceAppNode = {
|
|
@@ -32,51 +32,14 @@ describe('LocalDevProcess', () => {
|
|
|
32
32
|
srcDir: 'src',
|
|
33
33
|
platformVersion: '1.0.0',
|
|
34
34
|
};
|
|
35
|
-
const mockSubbuildStatus = {
|
|
36
|
-
buildName: 'component1',
|
|
37
|
-
buildType: 'APP',
|
|
38
|
-
errorMessage: '',
|
|
39
|
-
finishedAt: new Date().toISOString(),
|
|
40
|
-
rootPath: '/test/path',
|
|
41
|
-
startedAt: new Date().toISOString(),
|
|
42
|
-
status: 'SUCCESS',
|
|
43
|
-
id: '123',
|
|
44
|
-
standardError: null,
|
|
45
|
-
visible: true,
|
|
46
|
-
};
|
|
47
|
-
const mockBuild = {
|
|
48
|
-
activitySource: { type: 'HUBSPOT' },
|
|
49
|
-
projectName: 'test-project',
|
|
50
|
-
uploadMessage: 'test-upload-message',
|
|
51
|
-
autoDeployId: 123,
|
|
52
|
-
buildId: 123,
|
|
53
|
-
createdAt: new Date().toISOString(),
|
|
54
|
-
deployableState: 'DEPLOYED',
|
|
55
|
-
finishedAt: new Date().toISOString(),
|
|
56
|
-
startedAt: new Date().toISOString(),
|
|
57
|
-
status: 'SUCCESS',
|
|
58
|
-
subbuildStatuses: [
|
|
59
|
-
{ ...mockSubbuildStatus, buildName: 'component1' },
|
|
60
|
-
{ ...mockSubbuildStatus, buildName: 'component2' },
|
|
61
|
-
],
|
|
62
|
-
deployStatusTaskLocator: {
|
|
63
|
-
id: '123',
|
|
64
|
-
links: [],
|
|
65
|
-
},
|
|
66
|
-
enqueuedAt: new Date().toISOString(),
|
|
67
|
-
isAutoDeployEnabled: false,
|
|
68
|
-
portalId: 123,
|
|
69
|
-
};
|
|
70
35
|
const mockOptions = {
|
|
71
36
|
projectDir: '/test/project',
|
|
72
37
|
projectConfig: mockProjectConfig,
|
|
73
38
|
targetProjectAccountId: 123,
|
|
74
39
|
targetTestingAccountId: 456,
|
|
75
40
|
projectId: 789,
|
|
76
|
-
isGithubLinked: false,
|
|
77
41
|
initialProjectNodes: {},
|
|
78
42
|
env: ENVIRONMENTS.PROD,
|
|
79
|
-
deployedBuild: mockBuild,
|
|
80
43
|
projectName: 'test-project',
|
|
81
44
|
};
|
|
82
45
|
beforeEach(() => {
|
|
@@ -87,7 +50,6 @@ describe('LocalDevProcess', () => {
|
|
|
87
50
|
devServerStartError: vi.fn(),
|
|
88
51
|
devServerCleanupError: vi.fn(),
|
|
89
52
|
missingComponentsWarning: vi.fn(),
|
|
90
|
-
noDeployedBuild: vi.fn(),
|
|
91
53
|
startupMessage: vi.fn(),
|
|
92
54
|
monitorConsoleOutput: vi.fn(),
|
|
93
55
|
cleanupStart: vi.fn(),
|
|
@@ -97,7 +59,6 @@ describe('LocalDevProcess', () => {
|
|
|
97
59
|
projectConfigMismatch: vi.fn(),
|
|
98
60
|
uploadError: vi.fn(),
|
|
99
61
|
uploadSuccess: vi.fn(),
|
|
100
|
-
clearUploadWarnings: vi.fn(),
|
|
101
62
|
fileChangeError: vi.fn(),
|
|
102
63
|
uploadWarning: vi.fn(),
|
|
103
64
|
};
|
|
@@ -118,14 +79,6 @@ describe('LocalDevProcess', () => {
|
|
|
118
79
|
});
|
|
119
80
|
});
|
|
120
81
|
describe('start()', () => {
|
|
121
|
-
it('should exit if no deployed build exists', async () => {
|
|
122
|
-
const processWithoutBuild = new LocalDevProcess({
|
|
123
|
-
...mockOptions,
|
|
124
|
-
deployedBuild: undefined,
|
|
125
|
-
});
|
|
126
|
-
await expect(processWithoutBuild.start()).rejects.toThrow('Process.exit called with code 0');
|
|
127
|
-
expect(mockLocalDevLogger.noDeployedBuild).toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
82
|
it('should exit if dev server setup fails', async () => {
|
|
130
83
|
mockDevServerManager.setup.mockRejectedValue(new Error('Setup failed'));
|
|
131
84
|
await expect(process.start()).rejects.toThrow('Process.exit called with code 1');
|
|
@@ -138,31 +91,6 @@ describe('LocalDevProcess', () => {
|
|
|
138
91
|
expect(mockLocalDevLogger.monitorConsoleOutput).toHaveBeenCalled();
|
|
139
92
|
expect(mockLocalDevLogger.missingComponentsWarning).not.toHaveBeenCalled();
|
|
140
93
|
});
|
|
141
|
-
it('should warn about missing components', async () => {
|
|
142
|
-
const mockNode = {
|
|
143
|
-
uid: 'component3',
|
|
144
|
-
componentType: 'APP',
|
|
145
|
-
localDev: {
|
|
146
|
-
componentRoot: '/test/path',
|
|
147
|
-
componentConfigPath: '/test/path/config.json',
|
|
148
|
-
configUpdatedSinceLastUpload: false,
|
|
149
|
-
},
|
|
150
|
-
componentDeps: {},
|
|
151
|
-
metaFilePath: '/test/path',
|
|
152
|
-
config: {},
|
|
153
|
-
files: [],
|
|
154
|
-
};
|
|
155
|
-
const processWithNode = new LocalDevProcess({
|
|
156
|
-
...mockOptions,
|
|
157
|
-
initialProjectNodes: {
|
|
158
|
-
component3: mockNode,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
await processWithNode.start();
|
|
162
|
-
expect(mockLocalDevLogger.missingComponentsWarning).toHaveBeenCalledWith([
|
|
163
|
-
'[App] component3',
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
94
|
});
|
|
167
95
|
describe('stop()', () => {
|
|
168
96
|
it('should exit with error if cleanup fails', async () => {
|
|
@@ -226,7 +154,8 @@ describe('LocalDevProcess', () => {
|
|
|
226
154
|
});
|
|
227
155
|
const success = await process.uploadProject();
|
|
228
156
|
expect(mockLocalDevLogger.uploadSuccess).toHaveBeenCalled();
|
|
229
|
-
expect
|
|
157
|
+
// @ts-expect-error accessing private property for testing
|
|
158
|
+
expect(process.state.uploadWarnings.size).toBe(0);
|
|
230
159
|
expect(success).toBe(true);
|
|
231
160
|
});
|
|
232
161
|
it('should reset projectNodesAtLastUpload', async () => {
|
|
@@ -300,10 +229,10 @@ describe('LocalDevProcess', () => {
|
|
|
300
229
|
process.state.projectNodes = {};
|
|
301
230
|
expect(listener).toHaveBeenCalled();
|
|
302
231
|
});
|
|
303
|
-
it('should call listener immediately
|
|
232
|
+
it('should call listener immediately', () => {
|
|
304
233
|
const listener = vi.fn();
|
|
305
234
|
const key = 'projectNodes';
|
|
306
|
-
process.addStateListener(key, listener
|
|
235
|
+
process.addStateListener(key, listener);
|
|
307
236
|
expect(listener).toHaveBeenCalledWith(process.projectNodes);
|
|
308
237
|
});
|
|
309
238
|
});
|
|
@@ -313,32 +242,13 @@ describe('LocalDevProcess', () => {
|
|
|
313
242
|
const key = 'projectNodes';
|
|
314
243
|
// Add the listener first
|
|
315
244
|
process.addStateListener(key, listener);
|
|
316
|
-
// Trigger state change to verify listener is called
|
|
317
|
-
// @ts-expect-error
|
|
318
|
-
process.state.projectNodes = {};
|
|
319
245
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
320
246
|
// Remove the listener
|
|
321
247
|
process.removeStateListener(key, listener);
|
|
322
248
|
// Trigger state change again to verify listener is no longer called
|
|
323
249
|
// @ts-expect-error
|
|
324
250
|
process.state.projectNodes = { newNode: { uid: 'newNode' } };
|
|
325
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
326
|
-
});
|
|
327
|
-
it('should not affect other listeners when removing one', () => {
|
|
328
|
-
const listener1 = vi.fn();
|
|
329
|
-
const listener2 = vi.fn();
|
|
330
|
-
const key = 'projectNodes';
|
|
331
|
-
// Add two listeners
|
|
332
|
-
process.addStateListener(key, listener1);
|
|
333
|
-
process.addStateListener(key, listener2);
|
|
334
|
-
// Remove only the first listener
|
|
335
|
-
process.removeStateListener(key, listener1);
|
|
336
|
-
// Trigger state change
|
|
337
|
-
// @ts-expect-error
|
|
338
|
-
process.state.projectNodes = {};
|
|
339
|
-
// Only listener2 should be called
|
|
340
|
-
expect(listener1).not.toHaveBeenCalled();
|
|
341
|
-
expect(listener2).toHaveBeenCalled();
|
|
251
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
342
252
|
});
|
|
343
253
|
});
|
|
344
254
|
});
|
|
@@ -66,7 +66,7 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
66
66
|
headers: { origin: 'https://app.hubspot.com' },
|
|
67
67
|
});
|
|
68
68
|
expect(mockWebSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
69
|
-
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function)
|
|
69
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
|
|
70
70
|
expect(mockWebSocket.close).not.toHaveBeenCalled();
|
|
71
71
|
});
|
|
72
72
|
it('should reject connection from invalid origin', async () => {
|
|
@@ -215,7 +215,7 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
215
215
|
expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
216
216
|
expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
217
217
|
// Each connection should trigger state listener setup
|
|
218
|
-
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(
|
|
218
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(9); // 3 listeners per connection * 3 connections
|
|
219
219
|
// Each connection should trigger dev server message
|
|
220
220
|
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
|
|
221
221
|
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
|
|
@@ -284,16 +284,16 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
284
284
|
const closeCallbacks2 = mockWebSocket2.on.mock.calls
|
|
285
285
|
.filter(call => call[0] === 'close')
|
|
286
286
|
.map(call => call[1]);
|
|
287
|
-
expect(closeCallbacks1).toHaveLength(
|
|
288
|
-
expect(closeCallbacks2).toHaveLength(
|
|
287
|
+
expect(closeCallbacks1).toHaveLength(3); // projectNodes and appData listeners
|
|
288
|
+
expect(closeCallbacks2).toHaveLength(3); // projectNodes and appData listeners
|
|
289
289
|
// Simulate first connection closing (call all close callbacks)
|
|
290
290
|
closeCallbacks1.forEach(callback => callback());
|
|
291
291
|
// Should have removed listeners for first connection (2 listeners: projectNodes and appData)
|
|
292
|
-
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(
|
|
292
|
+
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
|
|
293
293
|
// Simulate second connection closing
|
|
294
294
|
closeCallbacks2.forEach(callback => callback());
|
|
295
295
|
// Should have removed listeners for second connection as well
|
|
296
|
-
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(
|
|
296
|
+
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(6);
|
|
297
297
|
});
|
|
298
298
|
it('should broadcast state changes to all connected clients', () => {
|
|
299
299
|
// Establish connections
|