@hubspot/cli 7.6.0-beta.9 → 7.6.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 (217) hide show
  1. package/api/__tests__/migrate.test.js +5 -5
  2. package/api/migrate.d.ts +4 -5
  3. package/api/migrate.js +2 -10
  4. package/commands/__tests__/getStarted.test.js +2 -2
  5. package/commands/__tests__/mcp.test.js +1 -1
  6. package/commands/__tests__/project.test.js +0 -3
  7. package/commands/app/__tests__/migrate.test.js +1 -1
  8. package/commands/app/migrate.js +4 -5
  9. package/commands/app/secret/add.js +2 -1
  10. package/commands/app/secret/delete.js +2 -1
  11. package/commands/app/secret/list.js +2 -1
  12. package/commands/app/secret/update.js +2 -1
  13. package/commands/app/secret.js +2 -1
  14. package/commands/app.js +2 -2
  15. package/commands/config/set.js +0 -1
  16. package/commands/getStarted.d.ts +0 -2
  17. package/commands/getStarted.js +72 -24
  18. package/commands/mcp/__tests__/setup.test.js +2 -2
  19. package/commands/mcp/setup.d.ts +0 -1
  20. package/commands/mcp/setup.js +14 -13
  21. package/commands/mcp.js +3 -3
  22. package/commands/project/__tests__/add.test.js +64 -0
  23. package/commands/project/__tests__/create.test.js +57 -0
  24. package/commands/project/__tests__/deploy.test.js +3 -5
  25. package/commands/project/__tests__/devUnifiedFlow.test.js +20 -11
  26. package/commands/project/__tests__/logs.test.js +0 -3
  27. package/commands/project/__tests__/migrate.test.js +1 -2
  28. package/commands/project/__tests__/migrateApp.test.js +1 -2
  29. package/commands/project/__tests__/profile.test.js +1 -1
  30. package/commands/project/add.d.ts +1 -1
  31. package/commands/project/add.js +4 -10
  32. package/commands/project/create.js +10 -11
  33. package/commands/project/deploy.js +11 -63
  34. package/commands/project/dev/deprecatedFlow.js +2 -1
  35. package/commands/project/dev/index.js +36 -15
  36. package/commands/project/dev/unifiedFlow.js +14 -10
  37. package/commands/project/download.js +1 -2
  38. package/commands/project/installDeps.js +1 -2
  39. package/commands/project/listBuilds.js +2 -2
  40. package/commands/project/logs.js +2 -2
  41. package/commands/project/migrate.js +41 -13
  42. package/commands/project/migrateApp.js +1 -2
  43. package/commands/project/open.js +1 -2
  44. package/commands/project/profile/add.js +3 -3
  45. package/commands/project/profile/delete.js +1 -2
  46. package/commands/project/profile.js +2 -3
  47. package/commands/project/upload.js +4 -4
  48. package/commands/project/validate.js +2 -2
  49. package/commands/project/watch.js +4 -4
  50. package/commands/project.js +1 -2
  51. package/commands/sandbox/delete.js +1 -1
  52. package/commands/testAccount/importData.d.ts +1 -1
  53. package/commands/testAccount/importData.js +1 -1
  54. package/commands/testAccount.js +1 -1
  55. package/lang/en.d.ts +104 -56
  56. package/lang/en.js +118 -68
  57. package/lang/en.lyaml +12 -12
  58. package/lib/__tests__/hasFeature.test.js +145 -7
  59. package/lib/__tests__/importData.test.js +1 -1
  60. package/lib/app/__tests__/migrate.test.js +26 -31
  61. package/lib/app/migrate.d.ts +3 -10
  62. package/lib/app/migrate.js +14 -25
  63. package/lib/constants.d.ts +9 -0
  64. package/lib/constants.js +9 -0
  65. package/lib/errorHandlers/index.d.ts +4 -0
  66. package/lib/errorHandlers/index.js +1 -1
  67. package/lib/hasFeature.js +6 -0
  68. package/lib/importData.js +1 -1
  69. package/lib/links.d.ts +1 -0
  70. package/lib/links.js +10 -3
  71. package/lib/mcp/setup.d.ts +0 -2
  72. package/lib/mcp/setup.js +4 -29
  73. package/lib/middleware/fireAlarmMiddleware.js +15 -5
  74. package/lib/projects/__tests__/AppDevModeInterface.test.js +72 -44
  75. package/lib/projects/__tests__/LocalDevProcess.test.js +228 -16
  76. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +16 -21
  77. package/lib/projects/__tests__/components.test.js +164 -7
  78. package/lib/projects/__tests__/deploy.test.js +229 -0
  79. package/lib/projects/__tests__/{localDevHelpers.test.js → localDevProjectHelpers.test.js} +5 -3
  80. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  81. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  82. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  83. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  84. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  85. package/lib/projects/add/legacyAddComponent.js +5 -1
  86. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  87. package/lib/projects/add/v3AddComponent.js +22 -5
  88. package/lib/projects/components.d.ts +1 -0
  89. package/lib/projects/components.js +27 -1
  90. package/lib/projects/create/__tests__/v3.test.js +174 -11
  91. package/lib/projects/create/index.js +2 -2
  92. package/lib/projects/create/legacy.js +1 -1
  93. package/lib/projects/create/v3.d.ts +2 -2
  94. package/lib/projects/create/v3.js +38 -13
  95. package/lib/projects/deploy.d.ts +13 -0
  96. package/lib/projects/deploy.js +63 -0
  97. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  98. package/lib/projects/localDev/AppDevModeInterface.js +132 -48
  99. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  100. package/lib/projects/localDev/LocalDevLogger.d.ts +4 -0
  101. package/lib/projects/localDev/LocalDevLogger.js +22 -0
  102. package/lib/projects/localDev/LocalDevManager.js +1 -1
  103. package/lib/projects/localDev/LocalDevProcess.d.ts +7 -5
  104. package/lib/projects/localDev/LocalDevProcess.js +93 -20
  105. package/lib/projects/localDev/LocalDevState.d.ts +13 -9
  106. package/lib/projects/localDev/LocalDevState.js +26 -17
  107. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -0
  108. package/lib/projects/localDev/LocalDevWebsocketServer.js +55 -23
  109. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -14
  110. package/lib/projects/localDev/helpers/account.js +233 -0
  111. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  112. package/lib/projects/localDev/helpers/project.js +176 -0
  113. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +4 -0
  114. package/lib/projects/localDev/localDevWebsocketServerUtils.js +10 -0
  115. package/lib/projects/platformVersion.d.ts +1 -0
  116. package/lib/projects/platformVersion.js +10 -0
  117. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  118. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +4 -14
  119. package/lib/projects/upload.js +1 -1
  120. package/lib/projects/urls.d.ts +2 -0
  121. package/lib/projects/urls.js +7 -0
  122. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  123. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  124. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  125. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  126. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  127. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  128. package/lib/prompts/installAppPrompt.d.ts +6 -1
  129. package/lib/prompts/installAppPrompt.js +6 -1
  130. package/lib/prompts/projectAddPrompt.js +3 -2
  131. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  132. package/lib/prompts/promptUtils.d.ts +7 -1
  133. package/lib/prompts/promptUtils.js +17 -1
  134. package/lib/prompts/selectProjectTemplatePrompt.js +3 -1
  135. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  136. package/lib/theme/__tests__/migrate.test.js +233 -0
  137. package/lib/theme/migrate.d.ts +13 -0
  138. package/lib/theme/migrate.js +90 -0
  139. package/lib/ui/index.js +3 -6
  140. package/lib/usageTracking.js +2 -2
  141. package/mcp-server/server.js +2 -1
  142. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  143. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  144. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  145. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  146. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  147. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  148. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  149. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  150. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  151. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  152. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  153. package/mcp-server/tools/cms/HsListTool.js +58 -0
  154. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  155. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  156. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  157. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  158. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  159. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  160. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  161. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  162. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  163. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  164. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  165. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  166. package/mcp-server/tools/index.d.ts +1 -0
  167. package/mcp-server/tools/index.js +16 -0
  168. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  169. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  170. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  171. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  172. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  173. package/mcp-server/tools/project/DocFetchTool.js +3 -3
  174. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  175. package/mcp-server/tools/project/DocsSearchTool.js +8 -8
  176. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  177. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  178. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  179. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  180. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  181. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  182. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  183. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  184. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +3 -3
  185. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +15 -13
  186. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  187. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  188. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  189. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  190. package/mcp-server/tools/project/constants.d.ts +1 -1
  191. package/mcp-server/tools/project/constants.js +14 -6
  192. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  193. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  194. package/mcp-server/utils/cliConfig.d.ts +1 -0
  195. package/mcp-server/utils/cliConfig.js +12 -0
  196. package/package.json +5 -4
  197. package/types/LocalDev.d.ts +21 -4
  198. package/types/Projects.d.ts +1 -0
  199. package/types/Prompts.d.ts +1 -0
  200. package/ui/components/BoxWithTitle.d.ts +8 -0
  201. package/ui/components/BoxWithTitle.js +9 -0
  202. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  203. package/ui/components/HorizontalSelectPrompt.js +30 -0
  204. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  205. package/ui/components/StatusMessageBoxes.js +31 -0
  206. package/ui/index.js +1 -1
  207. package/ui/lib/ui-testing-utils.d.ts +9 -0
  208. package/ui/lib/ui-testing-utils.js +47 -0
  209. package/ui/lib/useTerminalSize.d.ts +13 -0
  210. package/ui/lib/useTerminalSize.js +31 -0
  211. package/ui/styles.d.ts +18 -0
  212. package/ui/styles.js +18 -0
  213. package/ui/views/UiSandbox.d.ts +5 -0
  214. package/ui/views/UiSandbox.js +25 -0
  215. package/lib/projects/localDev/helpers.js +0 -388
  216. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
  217. /package/lib/projects/__tests__/{localDevHelpers.test.d.ts → localDevProjectHelpers.test.d.ts} +0 -0
package/lib/mcp/setup.js CHANGED
@@ -4,17 +4,15 @@ 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
7
  import path from 'path';
9
8
  import os from 'os';
10
9
  import fs from 'fs-extra';
11
10
  import { existsSync } from 'fs';
12
- const mcpServerName = 'hubspot-cli-mcp';
11
+ const mcpServerName = 'HubSpotDev';
13
12
  const claudeCode = 'claude';
14
13
  const windsurf = 'windsurf';
15
14
  const cursor = 'cursor';
16
15
  const vscode = 'vscode';
17
- const supportedMintlifyClients = [windsurf, cursor];
18
16
  export const supportedTools = [
19
17
  { name: commands.mcp.setup.claudeCode, value: claudeCode },
20
18
  { name: commands.mcp.setup.cursor, value: cursor },
@@ -25,28 +23,6 @@ const defaultMcpCommand = {
25
23
  command: 'hs',
26
24
  args: ['mcp', 'start'],
27
25
  };
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
26
  export async function addMcpServerToConfig(targets) {
51
27
  try {
52
28
  let derivedTargets = [];
@@ -164,7 +140,7 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
164
140
  name: mcpServerName,
165
141
  ...buildCommandWithAgentString(mcpCommand, vscode),
166
142
  });
167
- await execAsync(`code --add-mcp '${mcpConfig}'`);
143
+ await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
168
144
  SpinniesManager.succeed('vsCode', {
169
145
  text: commands.mcp.setup.spinners.configuredVsCode,
170
146
  });
@@ -206,15 +182,14 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
206
182
  });
207
183
  await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
208
184
  }
209
- await execAsync(`claude mcp add-json "${mcpServerName}" '${mcpConfig}' --scope user`);
185
+ await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
210
186
  SpinniesManager.succeed('claudeCode', {
211
187
  text: commands.mcp.setup.spinners.configuredClaudeCode,
212
188
  });
213
189
  return true;
214
190
  }
215
191
  catch (error) {
216
- if (error instanceof Error &&
217
- error.message.includes('claude: command not found')) {
192
+ if (error instanceof Error && error.message.includes('claude')) {
218
193
  SpinniesManager.fail('claudeCode', {
219
194
  text: commands.mcp.setup.spinners.claudeCodeNotFound,
220
195
  });
@@ -3,6 +3,8 @@ import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
3
3
  import { debugError } from '../errorHandlers/index.js';
4
4
  import pkg from '../../package.json' with { type: 'json' };
5
5
  import { logInBox } from '../ui/boxen.js';
6
+ import { renderInline } from '../../ui/index.js';
7
+ import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
6
8
  /*
7
9
  * Versions can be formatted like this:
8
10
  * =7.2.2 -> targets the exact version 7.2.2
@@ -98,12 +100,20 @@ async function logFireAlarms(accountId, command, version) {
98
100
  }
99
101
  return acc;
100
102
  }, '');
101
- await logInBox({
102
- contents: notifications,
103
- options: {
103
+ if (!process.env.HUBSPOT_ENABLE_INK) {
104
+ await logInBox({
105
+ contents: notifications,
106
+ options: {
107
+ title: 'Notifications',
108
+ },
109
+ });
110
+ }
111
+ else {
112
+ await renderInline(getWarningBox({
104
113
  title: 'Notifications',
105
- },
106
- });
114
+ message: notifications,
115
+ }));
116
+ }
107
117
  }
108
118
  }
109
119
  export async function checkFireAlarms(argv) {
@@ -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;
@@ -99,6 +100,7 @@ describe('AppDevModeInterface', () => {
99
100
  setAppDataForUid: vi.fn(),
100
101
  addListener: vi.fn(),
101
102
  addUploadWarning: vi.fn(),
103
+ removeListener: vi.fn(),
102
104
  };
103
105
  mockLocalDevLogger = {};
104
106
  // Mock constructors
@@ -238,16 +240,21 @@ describe('AppDevModeInterface', () => {
238
240
  await newAppDevModeInterface.setup({});
239
241
  expect(process.exit).toHaveBeenCalledWith(0);
240
242
  });
241
- it('should auto-install static auth app on test account', async () => {
242
- fetchAppInstallationData.mockResolvedValue({
243
- data: {
244
- isInstalledWithScopeGroups: false,
245
- previouslyAuthorizedScopeGroups: [],
246
- },
247
- });
248
- await appDevModeInterface.setup({});
249
- expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
250
- });
243
+ // @TODO: Restore test account auto install functionality
244
+ // it('should auto-install static auth app on test account', async () => {
245
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
246
+ // data: {
247
+ // isInstalledWithScopeGroups: false,
248
+ // previouslyAuthorizedScopeGroups: [],
249
+ // },
250
+ // });
251
+ // await appDevModeInterface.setup({});
252
+ // expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
253
+ // 123,
254
+ // 67890,
255
+ // [1, 2, 3]
256
+ // );
257
+ // });
251
258
  it('should open browser for OAuth app installation', async () => {
252
259
  const oauthAppNode = {
253
260
  ...mockAppNode,
@@ -286,7 +293,12 @@ describe('AppDevModeInterface', () => {
286
293
  },
287
294
  });
288
295
  await appDevModeInterface.setup({});
289
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
296
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
297
+ appUid: 'test-app-uid',
298
+ projectAccountId: 12345,
299
+ projectName: 'test-project',
300
+ testingAccountId: 67890,
301
+ });
290
302
  });
291
303
  it('should handle errors during setup', async () => {
292
304
  const error = new Error('Setup failed');
@@ -316,39 +328,46 @@ describe('AppDevModeInterface', () => {
316
328
  await appDevModeInterface.setup({});
317
329
  expect(process.exit).toHaveBeenCalledWith(1);
318
330
  });
319
- it('should exit if user declines auto-install', async () => {
320
- // Set up conditions for automatic installation
321
- getAccountConfig.mockReturnValue({
322
- parentAccountId: 12345, // matches targetProjectAccountId
323
- });
324
- isDeveloperTestAccount.mockReturnValue(true);
325
- fetchAppInstallationData.mockResolvedValue({
326
- data: {
327
- isInstalledWithScopeGroups: false,
328
- previouslyAuthorizedScopeGroups: [],
329
- },
330
- });
331
- installAppAutoPrompt.mockResolvedValue(false);
332
- // Create a new instance to trigger the exit during setup
333
- const newAppDevModeInterface = new AppDevModeInterface({
334
- localDevState: mockLocalDevState,
335
- localDevLogger: mockLocalDevLogger,
336
- });
337
- // The setup method catches the error, so we check that process.exit was called
338
- await newAppDevModeInterface.setup({});
339
- expect(process.exit).toHaveBeenCalledWith(0);
340
- });
341
- it('should fallback to browser install if auto-install fails', async () => {
342
- fetchAppInstallationData.mockResolvedValue({
343
- data: {
344
- isInstalledWithScopeGroups: false,
345
- previouslyAuthorizedScopeGroups: [],
346
- },
347
- });
348
- installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
349
- await appDevModeInterface.setup({});
350
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
351
- });
331
+ // @TODO: Restore test account auto install functionality
332
+ // it('should exit if user declines auto-install', async () => {
333
+ // // Set up conditions for automatic installation
334
+ // (getAccountConfig as Mock).mockReturnValue({
335
+ // parentAccountId: 12345, // matches targetProjectAccountId
336
+ // });
337
+ // (isDeveloperTestAccount as Mock).mockReturnValue(true);
338
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
339
+ // data: {
340
+ // isInstalledWithScopeGroups: false,
341
+ // previouslyAuthorizedScopeGroups: [],
342
+ // },
343
+ // });
344
+ // (installAppAutoPrompt as Mock).mockResolvedValue(false);
345
+ // // Create a new instance to trigger the exit during setup
346
+ // const newAppDevModeInterface = new AppDevModeInterface({
347
+ // localDevState: mockLocalDevState,
348
+ // localDevLogger: mockLocalDevLogger,
349
+ // });
350
+ // // The setup method catches the error, so we check that process.exit was called
351
+ // await newAppDevModeInterface.setup({});
352
+ // expect(process.exit).toHaveBeenCalledWith(0);
353
+ // });
354
+ // @TODO: Restore test account auto install functionality
355
+ // it('should fallback to browser install if auto-install fails', async () => {
356
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
357
+ // data: {
358
+ // isInstalledWithScopeGroups: false,
359
+ // previouslyAuthorizedScopeGroups: [],
360
+ // },
361
+ // });
362
+ // (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
363
+ // new Error('Install failed')
364
+ // );
365
+ // await appDevModeInterface.setup({});
366
+ // expect(installAppBrowserPrompt).toHaveBeenCalledWith(
367
+ // 'http://static-install-url',
368
+ // false
369
+ // );
370
+ // });
352
371
  });
353
372
  describe('start()', () => {
354
373
  it('should return early if no app node exists', async () => {
@@ -386,6 +405,15 @@ describe('AppDevModeInterface', () => {
386
405
  await appDevModeInterface.cleanup();
387
406
  expect(UIEDevModeInterface.cleanup).toHaveBeenCalled();
388
407
  });
408
+ it('should remove state listeners', async () => {
409
+ await appDevModeInterface.cleanup();
410
+ expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('devServerMessage',
411
+ // @ts-expect-error access private method for testing
412
+ appDevModeInterface.onDevServerMessage);
413
+ expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('projectNodes',
414
+ // @ts-expect-error
415
+ appDevModeInterface.onChangeProjectNodes);
416
+ });
389
417
  });
390
418
  describe('isAutomaticallyInstallable()', () => {
391
419
  it('should return true for static auth app on test account with correct parent', () => {
@@ -1,7 +1,10 @@
1
1
  import path from 'path';
2
2
  import { translateForLocalDev } from '@hubspot/project-parsing-lib';
3
3
  import { handleProjectUpload } from '../upload.js';
4
+ import { handleProjectDeploy } from '../deploy.js';
4
5
  import { getProjectConfig } from '../config.js';
6
+ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
7
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
5
8
  import LocalDevProcess from '../localDev/LocalDevProcess.js';
6
9
  import LocalDevLogger from '../localDev/LocalDevLogger.js';
7
10
  import DevServerManagerV2 from '../localDev/DevServerManagerV2.js';
@@ -19,7 +22,10 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => ({
19
22
  vi.mock('open');
20
23
  vi.mock('@hubspot/project-parsing-lib');
21
24
  vi.mock('../upload');
25
+ vi.mock('../deploy');
22
26
  vi.mock('../config');
27
+ vi.mock('@hubspot/local-dev-lib/api/projects');
28
+ vi.mock('@hubspot/local-dev-lib/errors/index');
23
29
  vi.mock('../localDev/LocalDevLogger');
24
30
  vi.mock('../localDev/DevServerManagerV2');
25
31
  // Tests for LocalDevProcess and LocalDevState
@@ -37,10 +43,35 @@ describe('LocalDevProcess', () => {
37
43
  projectConfig: mockProjectConfig,
38
44
  targetProjectAccountId: 123,
39
45
  targetTestingAccountId: 456,
40
- projectId: 789,
46
+ projectData: {
47
+ id: 789,
48
+ name: 'test-project',
49
+ portalId: 123,
50
+ createdAt: 0,
51
+ deletedAt: 0,
52
+ isLocked: false,
53
+ updatedAt: 0,
54
+ latestBuild: {
55
+ activitySource: { type: 'HUBSPOT_USER', userId: 456 },
56
+ buildId: 123,
57
+ createdAt: '2023-01-01T00:00:00Z',
58
+ deployableState: 'DEPLOYABLE',
59
+ deployStatusTaskLocator: { id: 'task-123', links: [] },
60
+ enqueuedAt: '2023-01-01T00:00:00Z',
61
+ finishedAt: '2023-01-01T00:05:00Z',
62
+ isAutoDeployEnabled: false,
63
+ portalId: 123,
64
+ projectName: 'test-project',
65
+ startedAt: '2023-01-01T00:01:00Z',
66
+ status: 'SUCCESS',
67
+ subbuildStatuses: [],
68
+ uploadMessage: 'Build completed',
69
+ autoDeployId: 0,
70
+ },
71
+ },
41
72
  initialProjectNodes: {},
73
+ initialProjectProfileData: {},
42
74
  env: ENVIRONMENTS.PROD,
43
- projectName: 'test-project',
44
75
  };
45
76
  beforeEach(() => {
46
77
  vi.clearAllMocks();
@@ -61,6 +92,9 @@ describe('LocalDevProcess', () => {
61
92
  uploadSuccess: vi.fn(),
62
93
  fileChangeError: vi.fn(),
63
94
  uploadWarning: vi.fn(),
95
+ deployInitiated: vi.fn(),
96
+ deployError: vi.fn(),
97
+ deploySuccess: vi.fn(),
64
98
  };
65
99
  mockDevServerManager = {
66
100
  setup: vi.fn().mockResolvedValue(undefined),
@@ -71,6 +105,8 @@ describe('LocalDevProcess', () => {
71
105
  // Mock constructors
72
106
  LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
73
107
  DevServerManagerV2.mockImplementation(() => mockDevServerManager);
108
+ // Mock external functions
109
+ isHubSpotHttpError.mockReturnValue(false);
74
110
  // Create process instance
75
111
  process = new LocalDevProcess(mockOptions);
76
112
  // Mock process.exit
@@ -140,9 +176,14 @@ describe('LocalDevProcess', () => {
140
176
  handleProjectUpload.mockResolvedValue({
141
177
  uploadError: new Error('Upload failed'),
142
178
  });
143
- const success = await process.uploadProject();
179
+ const result = await process.uploadProject();
144
180
  expect(mockLocalDevLogger.uploadError).toHaveBeenCalledWith(new Error('Upload failed'));
145
- expect(success).toBe(false);
181
+ expect(result).toEqual({
182
+ uploadSuccess: false,
183
+ buildSuccess: false,
184
+ deploySuccess: false,
185
+ deployId: undefined,
186
+ });
146
187
  });
147
188
  it('should handle successful upload', async () => {
148
189
  await process.handleConfigFileChange();
@@ -151,38 +192,120 @@ describe('LocalDevProcess', () => {
151
192
  });
152
193
  handleProjectUpload.mockResolvedValue({
153
194
  uploadError: null,
195
+ result: {
196
+ deployResult: {
197
+ id: 'deploy-123',
198
+ deployId: 123,
199
+ status: 'SUCCESS',
200
+ },
201
+ },
202
+ });
203
+ fetchProject.mockResolvedValue({
204
+ data: {
205
+ id: 789,
206
+ name: 'test-project',
207
+ portalId: 123,
208
+ createdAt: 0,
209
+ deletedAt: 0,
210
+ isLocked: false,
211
+ updatedAt: 0,
212
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
213
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
214
+ },
154
215
  });
155
- const success = await process.uploadProject();
216
+ const result = await process.uploadProject();
217
+ expect(fetchProject).toHaveBeenCalledWith(mockOptions.targetProjectAccountId, mockOptions.projectConfig.name);
156
218
  expect(mockLocalDevLogger.uploadSuccess).toHaveBeenCalled();
157
219
  // @ts-expect-error accessing private property for testing
158
220
  expect(process.state.uploadWarnings.size).toBe(0);
159
- expect(success).toBe(true);
221
+ expect(result).toEqual({
222
+ uploadSuccess: true,
223
+ buildSuccess: true,
224
+ deploySuccess: true,
225
+ deployId: 123,
226
+ });
160
227
  });
161
- it('should reset projectNodesAtLastUpload', async () => {
162
- const mockNodes = { node1: { uid: 'node1' } };
163
- const initialProjectNodes = { existingNode: { uid: 'existingNode' } };
228
+ it('should reset projectNodesAtLastUpload if deploy is successful', async () => {
229
+ const mockInitialNodes = {
230
+ node1: {
231
+ uid: 'node1',
232
+ componentType: 'APP',
233
+ localDev: {
234
+ componentRoot: '/test/path',
235
+ componentConfigPath: '/test/path/config.json',
236
+ configUpdatedSinceLastUpload: false,
237
+ },
238
+ componentDeps: {},
239
+ metaFilePath: '/test/path',
240
+ config: { name: 'Node 1' },
241
+ files: [],
242
+ },
243
+ };
244
+ const mockNewNodes = {
245
+ node1: {
246
+ uid: 'node2',
247
+ componentType: 'APP',
248
+ localDev: {
249
+ componentRoot: '/test/path',
250
+ componentConfigPath: '/test/path/config.json',
251
+ configUpdatedSinceLastUpload: false,
252
+ },
253
+ componentDeps: {},
254
+ metaFilePath: '/test/path',
255
+ config: { name: 'Node 2' },
256
+ files: [],
257
+ },
258
+ };
164
259
  // @ts-expect-error accessing private property for testing
165
- process.state._projectNodesAtLastUpload = initialProjectNodes;
260
+ process.state.projectNodesAtLastDeploy = mockInitialNodes;
166
261
  getProjectConfig.mockResolvedValue({
167
262
  projectConfig: mockOptions.projectConfig,
168
263
  });
169
264
  handleProjectUpload.mockResolvedValue({
170
265
  uploadError: null,
266
+ result: {
267
+ deployResult: {
268
+ id: 'deploy-123',
269
+ deployId: 456,
270
+ status: 'SUCCESS',
271
+ },
272
+ },
171
273
  });
172
274
  translateForLocalDev.mockResolvedValue({
173
- intermediateNodesIndexedByUid: mockNodes,
275
+ intermediateNodesIndexedByUid: mockNewNodes,
174
276
  });
175
- const success = await process.uploadProject();
176
- // Verify translateForLocalDev was called without projectNodesAtLastUpload option
277
+ fetchProject.mockResolvedValue({
278
+ data: {
279
+ id: 789,
280
+ name: 'test-project',
281
+ portalId: 123,
282
+ createdAt: 0,
283
+ deletedAt: 0,
284
+ isLocked: false,
285
+ updatedAt: 0,
286
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
287
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
288
+ },
289
+ });
290
+ const result = await process.uploadProject();
291
+ // Verify translateForLocalDev was called during updateProjectNodesAfterDeploy
177
292
  expect(translateForLocalDev).toHaveBeenCalledWith({
178
293
  projectSourceDir: path.join(mockOptions.projectDir, mockOptions.projectConfig.srcDir),
179
294
  platformVersion: mockOptions.projectConfig.platformVersion,
180
295
  accountId: mockOptions.targetProjectAccountId,
181
- }, { projectNodesAtLastUpload: undefined });
296
+ }, {
297
+ profile: undefined,
298
+ projectNodesAtLastUpload: undefined,
299
+ });
182
300
  // Verify projectNodesAtLastUpload was reset to the new nodes
183
301
  // @ts-expect-error accessing private property for testing
184
- expect(process.state.projectNodesAtLastUpload).toEqual(mockNodes);
185
- expect(success).toBe(true);
302
+ expect(process.state.projectNodesAtLastDeploy).toEqual(mockNewNodes);
303
+ expect(result).toEqual({
304
+ uploadSuccess: true,
305
+ buildSuccess: true,
306
+ deploySuccess: true,
307
+ deployId: 456,
308
+ });
186
309
  });
187
310
  });
188
311
  describe('handleFileChange()', () => {
@@ -251,4 +374,93 @@ describe('LocalDevProcess', () => {
251
374
  expect(listener).toHaveBeenCalledTimes(1);
252
375
  });
253
376
  });
377
+ describe('deployLatestBuild()', () => {
378
+ beforeEach(() => {
379
+ vi.clearAllMocks();
380
+ });
381
+ it('should successfully deploy latest build', async () => {
382
+ const mockDeploy = {
383
+ deployId: 456,
384
+ buildId: 123,
385
+ status: 'SUCCESS',
386
+ enqueuedAt: '2023-01-01T00:00:00Z',
387
+ startedAt: '2023-01-01T00:01:00Z',
388
+ finishedAt: '2023-01-01T00:05:00Z',
389
+ portalId: 123,
390
+ projectName: 'test-project',
391
+ userId: 789,
392
+ source: 'HUBSPOT_USER',
393
+ subdeployStatuses: [],
394
+ };
395
+ handleProjectDeploy.mockResolvedValue(mockDeploy);
396
+ const result = await process.deployLatestBuild();
397
+ expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
398
+ expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
399
+ 'test-project', // projectName
400
+ 123, // buildId
401
+ true, // useV3Api
402
+ false // force
403
+ );
404
+ expect(mockLocalDevLogger.deploySuccess).toHaveBeenCalled();
405
+ expect(result).toEqual({
406
+ success: true,
407
+ deployId: 456,
408
+ });
409
+ });
410
+ it('should deploy with force parameter', async () => {
411
+ const mockDeploy = {
412
+ deployId: 456,
413
+ buildId: 123,
414
+ status: 'SUCCESS',
415
+ enqueuedAt: '2023-01-01T00:00:00Z',
416
+ startedAt: '2023-01-01T00:01:00Z',
417
+ finishedAt: '2023-01-01T00:05:00Z',
418
+ portalId: 123,
419
+ projectName: 'test-project',
420
+ userId: 789,
421
+ source: 'HUBSPOT_USER',
422
+ subdeployStatuses: [],
423
+ };
424
+ handleProjectDeploy.mockResolvedValue(mockDeploy);
425
+ const result = await process.deployLatestBuild(true);
426
+ expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
427
+ 'test-project', // projectName
428
+ 123, // buildId
429
+ true, // useV3Api
430
+ true // force
431
+ );
432
+ expect(result).toEqual({
433
+ success: true,
434
+ deployId: 456,
435
+ });
436
+ });
437
+ it('should return error when no build exists', async () => {
438
+ // Create a process without latestBuild
439
+ const optionsWithoutBuild = {
440
+ ...mockOptions,
441
+ projectData: {
442
+ ...mockOptions.projectData,
443
+ latestBuild: undefined,
444
+ },
445
+ };
446
+ const processWithoutBuild = new LocalDevProcess(optionsWithoutBuild);
447
+ const result = await processWithoutBuild.deployLatestBuild();
448
+ expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
449
+ expect(mockLocalDevLogger.deployError).toHaveBeenCalledWith('Error deploying project. No build was found to deploy.');
450
+ expect(result).toEqual({
451
+ success: false,
452
+ });
453
+ expect(handleProjectDeploy).not.toHaveBeenCalled();
454
+ });
455
+ it('should handle deploy failure when no deploy object returned', async () => {
456
+ handleProjectDeploy.mockResolvedValue(undefined);
457
+ const result = await process.deployLatestBuild();
458
+ expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
459
+ expect(handleProjectDeploy).toHaveBeenCalled();
460
+ expect(result).toEqual({
461
+ success: false,
462
+ });
463
+ expect(mockLocalDevLogger.deploySuccess).not.toHaveBeenCalled();
464
+ });
465
+ });
254
466
  });
@@ -30,8 +30,16 @@ describe('LocalDevWebsocketServer', () => {
30
30
  mockLocalDevProcess = {
31
31
  addStateListener: vi.fn(),
32
32
  removeStateListener: vi.fn(),
33
- uploadProject: vi.fn(),
33
+ uploadProject: vi.fn().mockResolvedValue({}),
34
34
  sendDevServerMessage: vi.fn(),
35
+ projectData: {
36
+ name: 'test-project',
37
+ id: 123,
38
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
39
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
40
+ },
41
+ targetProjectAccountId: 456,
42
+ targetTestingAccountId: 789,
35
43
  };
36
44
  // Mock WebSocketServer constructor
37
45
  WebSocketServer.mockImplementation(() => mockWebSocketServer);
@@ -225,23 +233,6 @@ describe('LocalDevWebsocketServer', () => {
225
233
  expect(mockWebSocket3.close).not.toHaveBeenCalled();
226
234
  });
227
235
  it('should send project data to each connection independently', () => {
228
- // Setup mock project data properties as getters
229
- Object.defineProperty(mockLocalDevProcess, 'projectName', {
230
- get: () => 'test-project',
231
- configurable: true,
232
- });
233
- Object.defineProperty(mockLocalDevProcess, 'projectId', {
234
- get: () => 123,
235
- configurable: true,
236
- });
237
- Object.defineProperty(mockLocalDevProcess, 'targetProjectAccountId', {
238
- get: () => 456,
239
- configurable: true,
240
- });
241
- Object.defineProperty(mockLocalDevProcess, 'targetTestingAccountId', {
242
- get: () => 789,
243
- configurable: true,
244
- });
245
236
  // Establish multiple connections
246
237
  connectionCallback(mockWebSocket1, {
247
238
  headers: { origin: 'https://app.hubspot.com' },
@@ -255,6 +246,8 @@ describe('LocalDevWebsocketServer', () => {
255
246
  data: {
256
247
  projectName: 'test-project',
257
248
  projectId: 123,
249
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
250
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
258
251
  targetProjectAccountId: 456,
259
252
  targetTestingAccountId: 789,
260
253
  },
@@ -264,6 +257,8 @@ describe('LocalDevWebsocketServer', () => {
264
257
  data: {
265
258
  projectName: 'test-project',
266
259
  projectId: 123,
260
+ latestBuild: { id: 'build-1', status: 'SUCCESS' },
261
+ deployedBuild: { id: 'build-1', status: 'SUCCESS' },
267
262
  targetProjectAccountId: 456,
268
263
  targetTestingAccountId: 789,
269
264
  },
@@ -284,11 +279,11 @@ describe('LocalDevWebsocketServer', () => {
284
279
  const closeCallbacks2 = mockWebSocket2.on.mock.calls
285
280
  .filter(call => call[0] === 'close')
286
281
  .map(call => call[1]);
287
- expect(closeCallbacks1).toHaveLength(3); // projectNodes and appData listeners
288
- expect(closeCallbacks2).toHaveLength(3); // projectNodes and appData listeners
282
+ expect(closeCallbacks1).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
283
+ expect(closeCallbacks2).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
289
284
  // Simulate first connection closing (call all close callbacks)
290
285
  closeCallbacks1.forEach(callback => callback());
291
- // Should have removed listeners for first connection (2 listeners: projectNodes and appData)
286
+ // Should have removed listeners for first connection (3 listeners: projectNodes, appData, and uploadWarnings)
292
287
  expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
293
288
  // Simulate second connection closing
294
289
  closeCallbacks2.forEach(callback => callback());