@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.
Files changed (130) hide show
  1. package/README.md +0 -4
  2. package/api/__tests__/migrate.test.js +5 -5
  3. package/api/migrate.d.ts +10 -4
  4. package/api/migrate.js +2 -2
  5. package/commands/__tests__/create.test.js +20 -0
  6. package/commands/__tests__/testAccount.test.js +2 -0
  7. package/commands/app/__tests__/migrate.test.js +1 -0
  8. package/commands/create/function.js +2 -2
  9. package/commands/create/module.js +2 -2
  10. package/commands/create/template.js +2 -2
  11. package/commands/create.js +47 -0
  12. package/commands/getStarted.js +66 -4
  13. package/commands/mcp/setup.d.ts +0 -1
  14. package/commands/mcp/setup.js +3 -11
  15. package/commands/project/__tests__/create.test.js +57 -0
  16. package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
  17. package/commands/project/create.js +6 -1
  18. package/commands/project/deploy.js +31 -1
  19. package/commands/project/dev/deprecatedFlow.js +2 -1
  20. package/commands/project/dev/index.js +32 -12
  21. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  22. package/commands/project/dev/unifiedFlow.js +10 -16
  23. package/commands/project/profile/delete.js +26 -14
  24. package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
  25. package/commands/testAccount/__tests__/importData.test.js +93 -0
  26. package/commands/testAccount/create.js +23 -13
  27. package/commands/testAccount/importData.d.ts +9 -0
  28. package/commands/testAccount/importData.js +61 -0
  29. package/commands/testAccount.js +2 -0
  30. package/lang/en.d.ts +162 -46
  31. package/lang/en.js +177 -59
  32. package/lang/en.lyaml +35 -14
  33. package/lib/__tests__/importData.test.d.ts +1 -0
  34. package/lib/__tests__/importData.test.js +89 -0
  35. package/lib/accountTypes.js +2 -3
  36. package/lib/app/__tests__/migrate.test.js +81 -36
  37. package/lib/app/migrate.d.ts +17 -4
  38. package/lib/app/migrate.js +97 -19
  39. package/lib/constants.d.ts +1 -0
  40. package/lib/constants.js +1 -0
  41. package/lib/hasFeature.d.ts +1 -0
  42. package/lib/hasFeature.js +7 -0
  43. package/lib/importData.d.ts +3 -0
  44. package/lib/importData.js +50 -0
  45. package/lib/mcp/setup.d.ts +3 -5
  46. package/lib/mcp/setup.js +39 -139
  47. package/lib/process.js +15 -4
  48. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
  49. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
  50. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  51. package/lib/projects/__tests__/components.test.js +164 -7
  52. package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
  53. package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
  54. package/lib/projects/add/v3AddComponent.js +16 -4
  55. package/lib/projects/components.d.ts +1 -0
  56. package/lib/projects/components.js +27 -1
  57. package/lib/projects/localDev/AppDevModeInterface.js +35 -3
  58. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
  59. package/lib/projects/localDev/LocalDevLogger.js +2 -19
  60. package/lib/projects/localDev/LocalDevManager.js +1 -1
  61. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
  62. package/lib/projects/localDev/LocalDevProcess.js +3 -26
  63. package/lib/projects/localDev/LocalDevState.d.ts +6 -7
  64. package/lib/projects/localDev/LocalDevState.js +16 -15
  65. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  66. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  67. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
  68. package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
  69. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  70. package/lib/projects/localDev/helpers/project.js +173 -0
  71. package/lib/projects/urls.d.ts +1 -0
  72. package/lib/projects/urls.js +4 -0
  73. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  74. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  75. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  76. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  77. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  78. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  79. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  80. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  81. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  82. package/lib/prompts/createFunctionPrompt.js +36 -7
  83. package/lib/prompts/createModulePrompt.d.ts +2 -1
  84. package/lib/prompts/createModulePrompt.js +48 -1
  85. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  86. package/lib/prompts/createTemplatePrompt.js +9 -1
  87. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  88. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  89. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  90. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  91. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  92. package/lib/prompts/promptUtils.d.ts +7 -1
  93. package/lib/prompts/promptUtils.js +14 -1
  94. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  95. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  96. package/lib/ui/index.js +3 -6
  97. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  98. package/lib/ui/removeAnsiCodes.js +4 -0
  99. package/mcp-server/server.js +2 -1
  100. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  101. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  102. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  103. package/mcp-server/tools/cms/HsListTool.js +58 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  106. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  107. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  108. package/mcp-server/tools/index.d.ts +1 -0
  109. package/mcp-server/tools/index.js +12 -0
  110. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  111. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  112. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  113. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  114. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  115. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  116. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  117. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  118. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  119. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  120. package/mcp-server/tools/project/constants.d.ts +2 -0
  121. package/mcp-server/tools/project/constants.js +6 -0
  122. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  123. package/mcp-server/utils/toolUsageTracking.js +2 -1
  124. package/package.json +9 -6
  125. package/types/Cms.d.ts +16 -0
  126. package/types/Cms.js +25 -1
  127. package/types/LocalDev.d.ts +0 -3
  128. package/types/Prompts.d.ts +1 -0
  129. package/ui/index.d.ts +1 -0
  130. package/ui/index.js +6 -0
@@ -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
+ }
@@ -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 setupCursor(mcpCommand?: McpCommand): boolean;
24
- export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
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}" '${mcpConfig}' --scope user`);
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
- export function setupCursor(mcpCommand = defaultMcpCommand) {
240
- const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
241
- return setupMcpConfigFile({
242
- configPath: cursorConfigPath,
243
- configuringMessage: commands.mcp.setup.spinners.configuringCursor,
244
- configuredMessage: commands.mcp.setup.spinners.configuredCursor,
245
- failedMessage: commands.mcp.setup.spinners.failedToConfigureCursor,
246
- mcpCommand: buildCommandWithAgentString(mcpCommand, cursor),
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
- const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
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
- 'uncaughtException',
12
+ uncaughtException,
10
13
  'SIGTERM', // Represents a graceful termination
11
- 'SIGHUP', // Parent terminal has been closed
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 === 'SIGHUP';
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(mockLocalDevLogger.addUploadWarning).toHaveBeenCalled();
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(mockLocalDevLogger.clearUploadWarnings).toHaveBeenCalled();
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 when callOnInit is true', () => {
232
+ it('should call listener immediately', () => {
304
233
  const listener = vi.fn();
305
234
  const key = 'projectNodes';
306
- process.addStateListener(key, listener, true);
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); // Should still be 1, not 2
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), true);
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(6); // 2 listeners per connection * 3 connections
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(2); // projectNodes and appData listeners
288
- expect(closeCallbacks2).toHaveLength(2); // projectNodes and appData listeners
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(2);
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(4);
296
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(6);
297
297
  });
298
298
  it('should broadcast state changes to all connected clients', () => {
299
299
  // Establish connections