@hubspot/cli 7.9.0-experimental.0 → 7.9.1-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 (196) hide show
  1. package/bin/cli.js +5 -4
  2. package/commands/__tests__/getStarted.test.js +10 -0
  3. package/commands/account/__tests__/rename.test.js +42 -0
  4. package/commands/account/auth.js +10 -14
  5. package/commands/account/clean.js +11 -19
  6. package/commands/account/createOverride.js +15 -11
  7. package/commands/account/info.js +8 -5
  8. package/commands/account/list.js +13 -18
  9. package/commands/account/remove.js +23 -22
  10. package/commands/account/removeOverride.js +6 -6
  11. package/commands/account/rename.d.ts +1 -1
  12. package/commands/account/rename.js +6 -3
  13. package/commands/account/use.js +19 -8
  14. package/commands/app/__tests__/migrate.test.js +8 -4
  15. package/commands/app/migrate.js +2 -2
  16. package/commands/auth.js +18 -14
  17. package/commands/config/migrate.js +5 -5
  18. package/commands/config/set.js +1 -2
  19. package/commands/customObject/createSchema.js +2 -3
  20. package/commands/customObject/updateSchema.js +2 -3
  21. package/commands/getStarted.js +10 -5
  22. package/commands/hubdb/__tests__/list.test.js +1 -0
  23. package/commands/hubdb/list.js +2 -2
  24. package/commands/hubdb.d.ts +1 -1
  25. package/commands/init.js +36 -32
  26. package/commands/project/__tests__/deploy.test.js +10 -5
  27. package/commands/project/__tests__/devUnifiedFlow.test.js +6 -4
  28. package/commands/project/__tests__/logs.test.js +4 -0
  29. package/commands/project/__tests__/validate.test.js +2 -2
  30. package/commands/project/cloneApp.js +2 -2
  31. package/commands/project/deploy.js +2 -2
  32. package/commands/project/dev/deprecatedFlow.js +4 -5
  33. package/commands/project/dev/index.js +14 -4
  34. package/commands/project/dev/unifiedFlow.js +4 -5
  35. package/commands/project/listBuilds.js +7 -1
  36. package/commands/project/logs.js +2 -3
  37. package/commands/project/profile/add.js +6 -7
  38. package/commands/project/profile/delete.js +2 -2
  39. package/commands/project/upload.js +9 -3
  40. package/commands/project/validate.js +9 -3
  41. package/commands/project/watch.js +7 -2
  42. package/commands/sandbox/__tests__/create.test.js +14 -5
  43. package/commands/sandbox/create.js +4 -5
  44. package/commands/sandbox/delete.js +23 -20
  45. package/commands/testAccount/__tests__/create.test.js +68 -0
  46. package/commands/testAccount/create.d.ts +8 -0
  47. package/commands/testAccount/create.js +134 -44
  48. package/commands/testAccount/delete.js +9 -8
  49. package/commands/testAccount/importData.d.ts +1 -1
  50. package/lang/en.d.ts +3204 -3205
  51. package/lang/en.js +33 -9
  52. package/lib/__tests__/buildAccount.test.js +22 -30
  53. package/lib/__tests__/commonOpts.test.js +9 -13
  54. package/lib/__tests__/developerTestAccounts.test.js +29 -17
  55. package/lib/__tests__/importData.test.js +20 -10
  56. package/lib/__tests__/oauth.test.js +19 -8
  57. package/lib/__tests__/sandboxSync.test.js +33 -11
  58. package/lib/__tests__/sandboxes.test.js +30 -19
  59. package/lib/__tests__/usageTracking.test.js +10 -10
  60. package/lib/__tests__/validation.test.js +32 -32
  61. package/lib/accountTypes.d.ts +9 -9
  62. package/lib/accountTypes.js +2 -4
  63. package/lib/app/__tests__/migrate.test.js +15 -0
  64. package/lib/app/__tests__/migrate_legacy.test.js +9 -0
  65. package/lib/app/migrate_legacy.d.ts +2 -2
  66. package/lib/buildAccount.d.ts +4 -4
  67. package/lib/buildAccount.js +7 -14
  68. package/lib/commonOpts.js +3 -3
  69. package/lib/configMigrate.d.ts +2 -2
  70. package/lib/configMigrate.js +42 -18
  71. package/lib/configOptions.js +3 -2
  72. package/lib/constants.d.ts +1 -0
  73. package/lib/constants.js +6 -0
  74. package/lib/developerTestAccounts.d.ts +3 -3
  75. package/lib/developerTestAccounts.js +4 -7
  76. package/lib/doctor/DiagnosticInfoBuilder.d.ts +1 -1
  77. package/lib/doctor/DiagnosticInfoBuilder.js +9 -6
  78. package/lib/doctor/Doctor.js +4 -3
  79. package/lib/doctor/__tests__/Diagnosis.test.js +4 -3
  80. package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +17 -9
  81. package/lib/doctor/__tests__/Doctor.test.js +14 -0
  82. package/lib/importData.js +8 -7
  83. package/lib/links.js +5 -5
  84. package/lib/mcp/__tests__/setup.test.js +127 -0
  85. package/lib/mcp/setup.d.ts +4 -12
  86. package/lib/mcp/setup.js +34 -1
  87. package/lib/middleware/__test__/commandTargetingUtils.test.js +3 -3
  88. package/lib/middleware/__test__/configMiddleware.test.js +23 -22
  89. package/lib/middleware/__test__/gitMiddleware.test.js +9 -7
  90. package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
  91. package/lib/middleware/autoUpdateMiddleware.js +10 -2
  92. package/lib/middleware/commandTargetingUtils.js +2 -2
  93. package/lib/middleware/configMiddleware.d.ts +6 -1
  94. package/lib/middleware/configMiddleware.js +36 -15
  95. package/lib/middleware/gitMiddleware.js +8 -4
  96. package/lib/oauth.d.ts +2 -2
  97. package/lib/oauth.js +8 -10
  98. package/lib/projects/__tests__/AppDevModeInterface.test.js +17 -6
  99. package/lib/projects/__tests__/DevServerManager.test.js +1 -0
  100. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  101. package/lib/projects/__tests__/components.test.js +148 -24
  102. package/lib/projects/__tests__/deploy.test.js +1 -0
  103. package/lib/projects/__tests__/projects.test.js +13 -42
  104. package/lib/projects/components.js +76 -20
  105. package/lib/projects/config.js +5 -9
  106. package/lib/projects/create/__tests__/v2.test.js +11 -0
  107. package/lib/projects/localDev/AppDevModeInterface.js +2 -2
  108. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +2 -2
  109. package/lib/projects/localDev/LocalDevLogger.js +4 -4
  110. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
  111. package/lib/projects/localDev/helpers/account.d.ts +10 -10
  112. package/lib/projects/localDev/helpers/account.js +6 -11
  113. package/lib/projects/urls.js +5 -6
  114. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
  115. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
  116. package/lib/prompts/__tests__/downloadProjectPrompt.test.js +7 -5
  117. package/lib/prompts/accountNamePrompt.js +3 -3
  118. package/lib/prompts/accountsPrompt.d.ts +1 -1
  119. package/lib/prompts/accountsPrompt.js +6 -7
  120. package/lib/prompts/confirmImportDataPrompt.js +2 -2
  121. package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
  122. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
  123. package/lib/prompts/downloadProjectPrompt.d.ts +1 -0
  124. package/lib/prompts/downloadProjectPrompt.js +5 -2
  125. package/lib/prompts/importDataTestAccountSelectPrompt.js +4 -5
  126. package/lib/prompts/personalAccessKeyPrompt.js +2 -2
  127. package/lib/prompts/projectDevTargetAccountPrompt.d.ts +3 -3
  128. package/lib/prompts/projectDevTargetAccountPrompt.js +5 -7
  129. package/lib/prompts/sandboxesPrompt.js +7 -8
  130. package/lib/prompts/setAsDefaultAccountPrompt.js +7 -6
  131. package/lib/sandboxSync.d.ts +2 -2
  132. package/lib/sandboxSync.js +3 -9
  133. package/lib/sandboxes.d.ts +4 -4
  134. package/lib/sandboxes.js +6 -11
  135. package/lib/serverlessLogs.js +2 -2
  136. package/lib/theme/__tests__/migrate.test.js +15 -0
  137. package/lib/ui/index.js +6 -3
  138. package/lib/usageTracking.js +15 -8
  139. package/lib/validation.js +13 -11
  140. package/mcp-server/tools/cms/HsCreateFunctionTool.js +8 -2
  141. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
  142. package/mcp-server/tools/cms/HsCreateModuleTool.js +8 -2
  143. package/mcp-server/tools/cms/HsCreateTemplateTool.js +8 -2
  144. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
  145. package/mcp-server/tools/cms/HsFunctionLogsTool.js +6 -2
  146. package/mcp-server/tools/cms/HsListFunctionsTool.js +5 -1
  147. package/mcp-server/tools/cms/HsListTool.js +5 -1
  148. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -0
  149. package/mcp-server/tools/index.js +4 -0
  150. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
  151. package/mcp-server/tools/project/AddFeatureToProjectTool.js +9 -3
  152. package/mcp-server/tools/project/CreateProjectTool.js +8 -2
  153. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
  154. package/mcp-server/tools/project/CreateTestAccountTool.js +150 -0
  155. package/mcp-server/tools/project/DeployProjectTool.d.ts +1 -1
  156. package/mcp-server/tools/project/DeployProjectTool.js +8 -2
  157. package/mcp-server/tools/project/DocFetchTool.d.ts +1 -1
  158. package/mcp-server/tools/project/DocFetchTool.js +9 -5
  159. package/mcp-server/tools/project/DocsSearchTool.d.ts +1 -1
  160. package/mcp-server/tools/project/DocsSearchTool.js +12 -8
  161. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +1 -1
  162. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +11 -7
  163. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +1 -1
  164. package/mcp-server/tools/project/GetApplicationInfoTool.js +11 -7
  165. package/mcp-server/tools/project/GetBuildStatusTool.d.ts +26 -0
  166. package/mcp-server/tools/project/GetBuildStatusTool.js +164 -0
  167. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +1 -1
  168. package/mcp-server/tools/project/GetConfigValuesTool.js +11 -7
  169. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +1 -1
  170. package/mcp-server/tools/project/GuidedWalkthroughTool.js +7 -3
  171. package/mcp-server/tools/project/UploadProjectTools.d.ts +9 -3
  172. package/mcp-server/tools/project/UploadProjectTools.js +51 -5
  173. package/mcp-server/tools/project/ValidateProjectTool.d.ts +1 -1
  174. package/mcp-server/tools/project/ValidateProjectTool.js +7 -3
  175. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
  176. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +454 -0
  177. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +5 -1
  178. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +25 -13
  179. package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +7 -5
  180. package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +7 -5
  181. package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.d.ts +1 -0
  182. package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.js +240 -0
  183. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +8 -6
  184. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
  185. package/mcp-server/utils/__tests__/content.test.js +21 -20
  186. package/mcp-server/utils/__tests__/feedbackTracking.test.js +33 -28
  187. package/mcp-server/utils/content.d.ts +1 -1
  188. package/mcp-server/utils/content.js +2 -2
  189. package/mcp-server/utils/feedbackTracking.d.ts +1 -1
  190. package/mcp-server/utils/feedbackTracking.js +3 -3
  191. package/mcp-server/utils/toolUsageTracking.js +4 -3
  192. package/package.json +8 -7
  193. package/mcp-server/utils/__tests__/cliConfig.test.js +0 -110
  194. package/mcp-server/utils/cliConfig.d.ts +0 -1
  195. package/mcp-server/utils/cliConfig.js +0 -12
  196. /package/{mcp-server/utils/__tests__/cliConfig.test.d.ts → lib/mcp/__tests__/setup.test.d.ts} +0 -0
@@ -0,0 +1,127 @@
1
+ import { execAsync } from '../../../mcp-server/utils/command.js';
2
+ import { setupCodex, supportedTools } from '../setup.js';
3
+ import SpinniesManager from '../../ui/SpinniesManager.js';
4
+ import { logError } from '../../errorHandlers/index.js';
5
+ import { commands } from '../../../lang/en.js';
6
+ // Mock dependencies
7
+ vi.mock('../../../mcp-server/utils/command.js');
8
+ vi.mock('../../ui/SpinniesManager.js');
9
+ vi.mock('../../ui/logger.js');
10
+ vi.mock('../../errorHandlers/index.js');
11
+ vi.mock('../../../lang/en.js', () => ({
12
+ commands: {
13
+ mcp: {
14
+ setup: {
15
+ codex: 'Codex CLI',
16
+ claudeCode: 'Claude Code',
17
+ cursor: 'Cursor',
18
+ vsCode: 'VS Code',
19
+ windsurf: 'Windsurf',
20
+ success: vi.fn(targets => `Success message for ${targets.join(', ')}`),
21
+ spinners: {
22
+ configuringCodex: 'Configuring Codex...',
23
+ configuredCodex: 'Configured Codex',
24
+ codexNotFound: 'Codex command not found - skipping configuration',
25
+ codexInstallFailed: 'Failed to configure Codex',
26
+ },
27
+ },
28
+ },
29
+ },
30
+ }));
31
+ const mockedExecAsync = vi.mocked(execAsync);
32
+ const mockedSpinniesManager = vi.mocked(SpinniesManager);
33
+ const mockedLogError = vi.mocked(logError);
34
+ describe('lib/mcp/setup', () => {
35
+ beforeEach(() => {
36
+ vi.resetAllMocks();
37
+ });
38
+ describe('supportedTools', () => {
39
+ it('should include Codex in the supported tools list', () => {
40
+ const codexTool = supportedTools.find(tool => tool.value === 'codex');
41
+ expect(codexTool).toBeDefined();
42
+ expect(codexTool?.name).toBe(commands.mcp.setup.codex);
43
+ expect(codexTool?.value).toBe('codex');
44
+ });
45
+ it('should have Codex as the first tool in the list', () => {
46
+ expect(supportedTools[0].value).toBe('codex');
47
+ });
48
+ it('should contain all expected tools', () => {
49
+ const toolValues = supportedTools.map(tool => tool.value);
50
+ expect(toolValues).toContain('codex');
51
+ expect(toolValues).toContain('claude');
52
+ expect(toolValues).toContain('cursor');
53
+ expect(toolValues).toContain('vscode');
54
+ expect(toolValues).toContain('windsurf');
55
+ });
56
+ });
57
+ describe('setupCodex', () => {
58
+ const mockMcpCommand = {
59
+ command: 'test-command',
60
+ args: ['--arg1', '--arg2'],
61
+ };
62
+ it('should successfully configure Codex when command is available', async () => {
63
+ mockedExecAsync.mockResolvedValueOnce({
64
+ stdout: 'codex version 1.0.0',
65
+ stderr: '',
66
+ }); // version check
67
+ mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' }); // mcp add command
68
+ const result = await setupCodex(mockMcpCommand);
69
+ expect(result).toBe(true);
70
+ expect(mockedSpinniesManager.add).toHaveBeenCalledWith('codexSpinner', {
71
+ text: commands.mcp.setup.spinners.configuringCodex,
72
+ });
73
+ expect(mockedExecAsync).toHaveBeenCalledWith('codex --version');
74
+ expect(mockedExecAsync).toHaveBeenCalledWith('codex mcp add "HubSpotDev" -- test-command --arg1 --arg2 --ai-agent codex');
75
+ expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('codexSpinner', {
76
+ text: commands.mcp.setup.spinners.configuredCodex,
77
+ });
78
+ });
79
+ it('should use default mcp command when none provided', async () => {
80
+ mockedExecAsync.mockResolvedValueOnce({
81
+ stdout: 'codex version 1.0.0',
82
+ stderr: '',
83
+ }); // version check
84
+ mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' }); // mcp add command
85
+ const result = await setupCodex();
86
+ expect(result).toBe(true);
87
+ expect(mockedExecAsync).toHaveBeenCalledWith('codex mcp add "HubSpotDev" -- hs mcp start --ai-agent codex');
88
+ });
89
+ it('should handle codex command not found', async () => {
90
+ const error = new Error('Command not found: codex');
91
+ mockedExecAsync.mockRejectedValueOnce(error);
92
+ const result = await setupCodex(mockMcpCommand);
93
+ expect(result).toBe(false);
94
+ expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
95
+ text: commands.mcp.setup.spinners.codexNotFound,
96
+ });
97
+ });
98
+ it('should handle codex installation failure', async () => {
99
+ const error = new Error('Some other error');
100
+ mockedExecAsync.mockResolvedValueOnce({
101
+ stdout: 'codex version 1.0.0',
102
+ stderr: '',
103
+ }); // version check passes
104
+ mockedExecAsync.mockRejectedValueOnce(error); // mcp add fails
105
+ const result = await setupCodex(mockMcpCommand);
106
+ expect(result).toBe(false);
107
+ expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
108
+ text: commands.mcp.setup.spinners.codexInstallFailed,
109
+ });
110
+ expect(mockedLogError).toHaveBeenCalledWith(error);
111
+ });
112
+ it('should handle unexpected errors during spinner setup', async () => {
113
+ const error = new Error('Unexpected error');
114
+ mockedSpinniesManager.add.mockImplementationOnce(() => {
115
+ throw error;
116
+ });
117
+ const result = await setupCodex(mockMcpCommand);
118
+ expect(result).toBe(false);
119
+ expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
120
+ text: commands.mcp.setup.spinners.codexInstallFailed,
121
+ });
122
+ expect(mockedLogError).toHaveBeenCalledWith(error);
123
+ });
124
+ });
125
+ // Note: addMcpServerToConfig integration tests would require mocking many dependencies
126
+ // and complex setup. The setupCodex function tests above cover the new functionality.
127
+ });
@@ -1,16 +1,7 @@
1
- export declare const supportedTools: ({
2
- name: "Claude Code";
1
+ export declare const supportedTools: {
2
+ name: string;
3
3
  value: string;
4
- } | {
5
- name: "Cursor";
6
- value: string;
7
- } | {
8
- name: "Windsurf";
9
- value: string;
10
- } | {
11
- name: "VSCode";
12
- value: string;
13
- })[];
4
+ }[];
14
5
  interface McpCommand {
15
6
  command: string;
16
7
  args: string[];
@@ -20,4 +11,5 @@ export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
20
11
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
21
12
  export declare function setupCursor(mcpCommand?: McpCommand): boolean;
22
13
  export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
14
+ export declare function setupCodex(mcpCommand?: McpCommand): Promise<boolean>;
23
15
  export {};
package/lib/mcp/setup.js CHANGED
@@ -13,11 +13,13 @@ const claudeCode = 'claude';
13
13
  const windsurf = 'windsurf';
14
14
  const cursor = 'cursor';
15
15
  const vscode = 'vscode';
16
+ const codex = 'codex';
16
17
  export const supportedTools = [
18
+ { name: commands.mcp.setup.codex, value: codex },
17
19
  { name: commands.mcp.setup.claudeCode, value: claudeCode },
18
20
  { name: commands.mcp.setup.cursor, value: cursor },
19
- { name: commands.mcp.setup.windsurf, value: windsurf },
20
21
  { name: commands.mcp.setup.vsCode, value: vscode },
22
+ { name: commands.mcp.setup.windsurf, value: windsurf },
21
23
  ];
22
24
  const defaultMcpCommand = {
23
25
  command: 'hs',
@@ -56,6 +58,9 @@ export async function addMcpServerToConfig(targets) {
56
58
  if (derivedTargets.includes(vscode)) {
57
59
  await runSetupFunction(setupVsCode);
58
60
  }
61
+ if (derivedTargets.includes(codex)) {
62
+ await runSetupFunction(setupCodex);
63
+ }
59
64
  uiLogger.info(commands.mcp.setup.success(derivedTargets));
60
65
  return derivedTargets;
61
66
  }
@@ -231,6 +236,34 @@ export function setupWindsurf(mcpCommand = defaultMcpCommand) {
231
236
  mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
232
237
  });
233
238
  }
239
+ export async function setupCodex(mcpCommand = defaultMcpCommand) {
240
+ try {
241
+ SpinniesManager.add('codexSpinner', {
242
+ text: commands.mcp.setup.spinners.configuringCodex,
243
+ });
244
+ // Check if codex command is available
245
+ await execAsync('codex --version');
246
+ await execAsync(`codex mcp add "${mcpServerName}" -- ${mcpCommand.command} ${mcpCommand.args.join(' ')} --ai-agent codex`);
247
+ SpinniesManager.succeed('codexSpinner', {
248
+ text: commands.mcp.setup.spinners.configuredCodex,
249
+ });
250
+ return true;
251
+ }
252
+ catch (error) {
253
+ if (error instanceof Error && error.message.includes('codex')) {
254
+ SpinniesManager.fail('codexSpinner', {
255
+ text: commands.mcp.setup.spinners.codexNotFound,
256
+ });
257
+ }
258
+ else {
259
+ SpinniesManager.fail('codexSpinner', {
260
+ text: commands.mcp.setup.spinners.codexInstallFailed,
261
+ });
262
+ logError(error);
263
+ }
264
+ return false;
265
+ }
266
+ }
234
267
  function buildCommandWithAgentString(mcpCommand, agent) {
235
268
  const mcpCommandCopy = structuredClone(mcpCommand);
236
269
  mcpCommandCopy.args.push('--ai-agent', agent);
@@ -1,7 +1,7 @@
1
1
  import * as cliConfig from '@hubspot/local-dev-lib/config';
2
2
  import { isTargetedCommand, shouldLoadConfigForCommand, shouldRunAccountValidationForCommand, shouldRunConfigValidationForCommand, } from '../commandTargetingUtils.js';
3
3
  vi.mock('@hubspot/local-dev-lib/config');
4
- const configFileExistsSpy = vi.spyOn(cliConfig, 'configFileExists');
4
+ const globalConfigFileExistsSpy = vi.spyOn(cliConfig, 'globalConfigFileExists');
5
5
  const targetCommandMap = {
6
6
  init: true,
7
7
  account: {
@@ -56,12 +56,12 @@ describe('lib/middleware/commandTargetingUtils', () => {
56
56
  expect(result).toBe(false);
57
57
  });
58
58
  it('should return false when the user is trying to migrate the global config', () => {
59
- configFileExistsSpy.mockReturnValue(false);
59
+ globalConfigFileExistsSpy.mockReturnValue(false);
60
60
  const result = shouldLoadConfigForCommand(['config', 'migrate']);
61
61
  expect(result).toBe(false);
62
62
  });
63
63
  it('should return true when the global config file does exist', () => {
64
- configFileExistsSpy.mockReturnValue(true);
64
+ globalConfigFileExistsSpy.mockReturnValue(true);
65
65
  const result = shouldLoadConfigForCommand(['account', 'use']);
66
66
  expect(result).toBe(true);
67
67
  });
@@ -2,7 +2,7 @@ import { uiLogger } from '../../ui/logger.js';
2
2
  import * as cliConfig from '@hubspot/local-dev-lib/config';
3
3
  import * as validation from '../../validation.js';
4
4
  import { EXIT_CODES } from '../../enums/exitCodes.js';
5
- import { handleDeprecatedEnvVariables, injectAccountIdMiddleware, loadAndValidateConfigMiddleware, validateAccountOptions, } from '../configMiddleware.js';
5
+ import { handleDeprecatedEnvVariables, injectAccountIdMiddleware, validateConfigMiddleware, validateAccountOptions, } from '../configMiddleware.js';
6
6
  vi.mock('../../ui/logger.js', () => ({
7
7
  uiLogger: {
8
8
  error: vi.fn(),
@@ -12,10 +12,10 @@ vi.mock('../../ui/logger.js', () => ({
12
12
  vi.mock('@hubspot/local-dev-lib/config');
13
13
  vi.mock('../../validation');
14
14
  const validateAccountSpy = vi.spyOn(validation, 'validateAccount');
15
- const loadConfigSpy = vi.spyOn(cliConfig, 'loadConfig');
16
- const getAccountIdSpy = vi.spyOn(cliConfig, 'getAccountId');
15
+ const getConfigAccountIfExistsSpy = vi.spyOn(cliConfig, 'getConfigAccountIfExists');
16
+ const globalConfigFileExistsSpy = vi.spyOn(cliConfig, 'globalConfigFileExists');
17
17
  const configFileExistsSpy = vi.spyOn(cliConfig, 'configFileExists');
18
- const getConfigPathSpy = vi.spyOn(cliConfig, 'getConfigPath');
18
+ const getConfigFilePathSpy = vi.spyOn(cliConfig, 'getConfigFilePath');
19
19
  const validateConfigSpy = vi.spyOn(cliConfig, 'validateConfig');
20
20
  const processExitSpy = vi.spyOn(process, 'exit');
21
21
  describe('lib/middleware/configMiddleware', () => {
@@ -23,7 +23,7 @@ describe('lib/middleware/configMiddleware', () => {
23
23
  processExitSpy.mockImplementation(code => {
24
24
  throw new Error(`Process.exit called with code ${code}`);
25
25
  });
26
- getConfigPathSpy.mockReturnValue('/path/to/config');
26
+ getConfigFilePathSpy.mockReturnValue('/path/to/config');
27
27
  });
28
28
  describe('handleDeprecatedEnvVariables()', () => {
29
29
  it('should handle deprecated HUBSPOT_PORTAL_ID environment variable', () => {
@@ -76,11 +76,13 @@ describe('lib/middleware/configMiddleware', () => {
76
76
  await injectAccountIdMiddleware(argv);
77
77
  expect(argv.userProvidedAccount).toBeUndefined();
78
78
  expect(argv.derivedAccountId).toBe(123);
79
- expect(getAccountIdSpy).not.toHaveBeenCalled();
79
+ expect(getConfigAccountIfExistsSpy).not.toHaveBeenCalled();
80
80
  process.env = originalEnv;
81
81
  });
82
82
  it('should use getAccountId when useEnv is false', async () => {
83
- getAccountIdSpy.mockReturnValue(456);
83
+ getConfigAccountIfExistsSpy.mockReturnValue({
84
+ accountId: 456,
85
+ });
84
86
  const argv = {
85
87
  _: ['some-command'],
86
88
  account: 'test-account',
@@ -90,41 +92,40 @@ describe('lib/middleware/configMiddleware', () => {
90
92
  await injectAccountIdMiddleware(argv);
91
93
  expect(argv.userProvidedAccount).toBe('test-account');
92
94
  expect(argv.derivedAccountId).toBe(456);
93
- expect(getAccountIdSpy).toHaveBeenCalledWith('test-account');
95
+ expect(getConfigAccountIfExistsSpy).toHaveBeenCalledWith('test-account');
94
96
  });
95
97
  });
96
- describe('loadAndValidateConfigMiddleware()', () => {
97
- it('should exit with error when config file exists and --config flag is used', async () => {
98
+ describe('validateConfigMiddleware()', () => {
99
+ it('should allow using --config flag', async () => {
98
100
  configFileExistsSpy.mockReturnValue(true);
101
+ validateConfigSpy.mockReturnValue({ isValid: true, errors: [] });
99
102
  const argv = {
100
103
  _: ['some-command'],
101
104
  config: 'custom-config.json',
102
105
  $0: 'hs',
103
106
  };
104
- await expect(loadAndValidateConfigMiddleware(argv)).rejects.toThrow();
105
- expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
106
- expect(uiLogger.error).toHaveBeenCalledWith('A configuration file already exists at /path/to/config. To specify a new configuration file, delete the existing one and try again.');
107
+ await validateConfigMiddleware(argv);
108
+ // Should not throw or exit - this is now allowed
109
+ expect(processExitSpy).not.toHaveBeenCalled();
110
+ expect(uiLogger.error).not.toHaveBeenCalled();
107
111
  });
108
- it('should load config and validate for non-init commands', async () => {
109
- configFileExistsSpy.mockReturnValue(false);
110
- loadConfigSpy.mockReturnValue({});
111
- validateConfigSpy.mockReturnValue(true);
112
+ it('should validate config for non-init commands', async () => {
113
+ globalConfigFileExistsSpy.mockReturnValue(true);
114
+ validateConfigSpy.mockReturnValue({ isValid: true, errors: [] });
112
115
  const argv = {
113
116
  _: ['some-command'],
114
117
  $0: 'hs',
115
118
  };
116
- await loadAndValidateConfigMiddleware(argv);
117
- expect(loadConfigSpy).toHaveBeenCalled();
119
+ await validateConfigMiddleware(argv);
118
120
  expect(validateConfigSpy).toHaveBeenCalled();
119
121
  });
120
122
  it('should skip validation for init command', async () => {
121
- configFileExistsSpy.mockReturnValue(false);
123
+ globalConfigFileExistsSpy.mockReturnValue(false);
122
124
  const argv = {
123
125
  _: ['init'],
124
126
  $0: 'hs',
125
127
  };
126
- await loadAndValidateConfigMiddleware(argv);
127
- expect(loadConfigSpy).not.toHaveBeenCalled();
128
+ await validateConfigMiddleware(argv);
128
129
  expect(validateConfigSpy).not.toHaveBeenCalled();
129
130
  });
130
131
  });
@@ -3,19 +3,19 @@ import * as gitUI from '../../ui/git.js';
3
3
  import { checkAndWarnGitInclusionMiddleware } from '../gitMiddleware.js';
4
4
  vi.mock('@hubspot/local-dev-lib/config');
5
5
  vi.mock('../../ui/git');
6
- const getConfigPathSpy = vi.spyOn(config, 'getConfigPath');
6
+ const getConfigFilePathSpy = vi.spyOn(config, 'getConfigFilePath');
7
7
  const checkAndWarnGitInclusionSpy = vi.spyOn(gitUI, 'checkAndWarnGitInclusion');
8
8
  describe('lib/middleware/gitMiddleware', () => {
9
9
  describe('checkAndWarnGitInclusionMiddleware()', () => {
10
10
  it('should call checkAndWarnGitInclusion when command is provided and config path exists', () => {
11
11
  const mockConfigPath = '/path/to/config.js';
12
- getConfigPathSpy.mockReturnValue(mockConfigPath);
12
+ getConfigFilePathSpy.mockReturnValue(mockConfigPath);
13
13
  const argv = {
14
14
  _: ['some-command'],
15
15
  $0: 'hs',
16
16
  };
17
17
  checkAndWarnGitInclusionMiddleware(argv);
18
- expect(getConfigPathSpy).toHaveBeenCalledTimes(1);
18
+ expect(getConfigFilePathSpy).toHaveBeenCalledTimes(1);
19
19
  expect(checkAndWarnGitInclusionSpy).toHaveBeenCalledWith(mockConfigPath);
20
20
  });
21
21
  it('should not call checkAndWarnGitInclusion when no command is provided', () => {
@@ -24,17 +24,19 @@ describe('lib/middleware/gitMiddleware', () => {
24
24
  $0: 'hs',
25
25
  };
26
26
  checkAndWarnGitInclusionMiddleware(argv);
27
- expect(getConfigPathSpy).not.toHaveBeenCalled();
27
+ expect(getConfigFilePathSpy).not.toHaveBeenCalled();
28
28
  expect(checkAndWarnGitInclusionSpy).not.toHaveBeenCalled();
29
29
  });
30
- it('should not call checkAndWarnGitInclusion when config path is null', () => {
31
- getConfigPathSpy.mockReturnValue(null);
30
+ it('should not call checkAndWarnGitInclusion when config path does not exist', () => {
31
+ getConfigFilePathSpy.mockImplementation(() => {
32
+ throw new Error('Config path does not exist');
33
+ });
32
34
  const argv = {
33
35
  _: ['some-command'],
34
36
  $0: 'hs',
35
37
  };
36
38
  checkAndWarnGitInclusionMiddleware(argv);
37
- expect(getConfigPathSpy).toHaveBeenCalledTimes(1);
39
+ expect(getConfigFilePathSpy).toHaveBeenCalledTimes(1);
38
40
  expect(checkAndWarnGitInclusionSpy).not.toHaveBeenCalled();
39
41
  });
40
42
  });
@@ -1,2 +1,4 @@
1
1
  import { Arguments } from 'yargs';
2
- export declare function autoUpdateCLI(argv: Arguments): Promise<void>;
2
+ export declare function autoUpdateCLI(argv: Arguments<{
3
+ useEnv?: boolean;
4
+ }>): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import updateNotifier from 'update-notifier';
2
- import { isConfigFlagEnabled } from '@hubspot/local-dev-lib/config';
2
+ import { getConfig } from '@hubspot/local-dev-lib/config';
3
3
  import { pkg } from '../jsonLoader.js';
4
4
  import { UI_COLORS } from '../ui/index.js';
5
5
  import SpinniesManager from '../ui/SpinniesManager.js';
@@ -45,10 +45,18 @@ const preventAutoUpdateForCommand = (commandParts) => {
45
45
  export async function autoUpdateCLI(argv) {
46
46
  // This lets us back to default update-notifier behavior
47
47
  let showManualInstallHelp = true;
48
+ let config;
49
+ try {
50
+ config = getConfig();
51
+ }
52
+ catch (e) {
53
+ debugError(e);
54
+ }
48
55
  if (notifier &&
49
56
  notifier.update &&
57
+ !argv.useEnv &&
50
58
  !process.env.SKIP_HUBSPOT_CLI_AUTO_UPDATES &&
51
- isConfigFlagEnabled('allowAutoUpdates') &&
59
+ config?.allowAutoUpdates !== false &&
52
60
  !preventAutoUpdateForCommand(argv._)) {
53
61
  // Ignore all update notifications if the current version is a pre-release
54
62
  if (!notifier.update.current.includes('-')) {
@@ -1,4 +1,4 @@
1
- import { configFileExists } from '@hubspot/local-dev-lib/config';
1
+ import { globalConfigFileExists } from '@hubspot/local-dev-lib/config';
2
2
  export function isTargetedCommand(commandParts, targetCommandMap) {
3
3
  const currentCommandPart = commandParts[0];
4
4
  if (!targetCommandMap[currentCommandPart]) {
@@ -23,7 +23,7 @@ const SKIP_CONFIG_LOADING_COMMANDS = {
23
23
  };
24
24
  // Returns true if the command requires a config file to be present
25
25
  export function shouldLoadConfigForCommand(commandParts) {
26
- const globalConfigExists = configFileExists(true);
26
+ const globalConfigExists = globalConfigFileExists();
27
27
  // the user is trying to migrate the global config
28
28
  const isGlobalConfigMigration = !globalConfigExists &&
29
29
  isTargetedCommand(commandParts, {
@@ -3,11 +3,16 @@ import { CLIOptions } from '@hubspot/local-dev-lib/types/CLIOptions';
3
3
  export declare function handleDeprecatedEnvVariables(argv: Arguments<{
4
4
  useEnv?: boolean;
5
5
  }>): void;
6
+ export declare function handleCustomConfigLocationMiddleware(argv: Arguments<{
7
+ useEnv?: boolean;
8
+ config?: string;
9
+ }>): void;
6
10
  /**
7
11
  * Auto-injects the derivedAccountId flag into all commands
8
12
  */
9
13
  export declare function injectAccountIdMiddleware(argv: Arguments<{
10
14
  account?: string;
15
+ config?: string;
11
16
  }>): Promise<void>;
12
- export declare function loadAndValidateConfigMiddleware(argv: Arguments<CLIOptions>): Promise<void>;
17
+ export declare function validateConfigMiddleware(argv: Arguments<CLIOptions>): Promise<void>;
13
18
  export declare function validateAccountOptions(argv: Arguments): Promise<void>;
@@ -1,9 +1,11 @@
1
- import { loadConfig, getAccountId, configFileExists, getConfigPath, validateConfig, } from '@hubspot/local-dev-lib/config';
1
+ import path from 'path';
2
+ import { getConfigAccountIfExists, validateConfig, getConfigDefaultAccountIfExists, configFileExists, } from '@hubspot/local-dev-lib/config';
3
+ import { getCwd } from '@hubspot/local-dev-lib/path';
2
4
  import { validateAccount } from '../validation.js';
3
5
  import { EXIT_CODES } from '../enums/exitCodes.js';
4
6
  import { commands } from '../../lang/en.js';
5
7
  import { uiDeprecatedTag } from '../ui/index.js';
6
- import { isTargetedCommand, shouldLoadConfigForCommand, shouldRunAccountValidationForCommand, shouldRunConfigValidationForCommand, } from './commandTargetingUtils.js';
8
+ import { shouldLoadConfigForCommand, shouldRunAccountValidationForCommand, shouldRunConfigValidationForCommand, } from './commandTargetingUtils.js';
7
9
  import { parseStringToNumber } from '../parsing.js';
8
10
  import { uiLogger } from '../ui/logger.js';
9
11
  import { lib } from '../../lang/en.js';
@@ -17,6 +19,18 @@ export function handleDeprecatedEnvVariables(argv) {
17
19
  process.env.HUBSPOT_ACCOUNT_ID = process.env.HUBSPOT_PORTAL_ID;
18
20
  }
19
21
  }
22
+ export function handleCustomConfigLocationMiddleware(argv) {
23
+ const { useEnv, config } = argv;
24
+ if (useEnv) {
25
+ process.env.USE_ENVIRONMENT_HUBSPOT_CONFIG = 'true';
26
+ }
27
+ else if (config && typeof config === 'string') {
28
+ const absoluteConfigPath = path.isAbsolute(config)
29
+ ? config
30
+ : path.join(getCwd(), config);
31
+ process.env.HUBSPOT_CONFIG_PATH = absoluteConfigPath;
32
+ }
33
+ }
20
34
  /**
21
35
  * Auto-injects the derivedAccountId flag into all commands
22
36
  */
@@ -33,10 +47,23 @@ export async function injectAccountIdMiddleware(argv) {
33
47
  }
34
48
  }
35
49
  else {
36
- argv.derivedAccountId = getAccountId(account);
50
+ // Wrap in try-catch to handle cases where config file doesn't exist yet (e.g., during hs init)
51
+ try {
52
+ let accountInConfig = account
53
+ ? getConfigAccountIfExists(account)
54
+ : undefined;
55
+ if (!accountInConfig) {
56
+ accountInConfig = getConfigDefaultAccountIfExists();
57
+ }
58
+ argv.derivedAccountId = accountInConfig?.accountId;
59
+ }
60
+ catch (err) {
61
+ // Config file doesn't exist yet, which is fine for commands like hs init
62
+ argv.derivedAccountId = undefined;
63
+ }
37
64
  }
38
65
  }
39
- export async function loadAndValidateConfigMiddleware(argv) {
66
+ export async function validateConfigMiddleware(argv) {
40
67
  // Skip this when no command is provided
41
68
  if (!argv._.length || argv.help) {
42
69
  return;
@@ -45,23 +72,17 @@ export async function loadAndValidateConfigMiddleware(argv) {
45
72
  if (!shouldLoadConfigForCommand(argv._)) {
46
73
  return;
47
74
  }
48
- // If the config file exists and the --config flag is used, exit with an error
49
- if (configFileExists(true) &&
50
- argv.config &&
51
- !isTargetedCommand(argv._, { config: { migrate: true } })) {
52
- uiLogger.error(commands.generalErrors.loadConfigMiddleware.configFileExists(getConfigPath()));
53
- process.exit(EXIT_CODES.ERROR);
54
- }
55
- const config = loadConfig(argv.config, argv);
56
75
  // We don't run validation for auth because users should be able to run it when
57
76
  // no accounts are configured, but we still want to exit if the config file is not found
58
- if (isTargetedCommand(argv._, { auth: true }) && !config) {
77
+ if (!process.env.USE_ENVIRONMENT_HUBSPOT_CONFIG && !configFileExists()) {
78
+ console.error('Config file not found, run hs account auth to configure your account');
59
79
  process.exit(EXIT_CODES.ERROR);
60
80
  }
61
81
  // Only validate the config if the command requires it
62
82
  if (shouldRunConfigValidationForCommand(argv._)) {
63
- const configIsValid = validateConfig();
64
- if (!configIsValid) {
83
+ const { isValid, errors } = validateConfig();
84
+ if (!isValid) {
85
+ uiLogger.error(commands.generalErrors.validateConfigMiddleware.configValidationFailed(errors));
65
86
  process.exit(EXIT_CODES.ERROR);
66
87
  }
67
88
  }
@@ -1,15 +1,19 @@
1
- import { getConfigPath, configFileExists } from '@hubspot/local-dev-lib/config';
1
+ import { getConfigFilePath, globalConfigFileExists, } from '@hubspot/local-dev-lib/config';
2
2
  import { checkAndWarnGitInclusion } from '../ui/git.js';
3
+ import { debugError } from '../errorHandlers/index.js';
3
4
  export function checkAndWarnGitInclusionMiddleware(argv) {
4
5
  // Skip this when no command is provided
5
6
  if (argv._.length) {
6
7
  // Skip if using global config
7
- if (configFileExists(true)) {
8
+ if (globalConfigFileExists()) {
8
9
  return;
9
10
  }
10
- const configPath = getConfigPath();
11
- if (configPath) {
11
+ try {
12
+ const configPath = getConfigFilePath();
12
13
  checkAndWarnGitInclusion(configPath);
13
14
  }
15
+ catch (error) {
16
+ debugError(error);
17
+ }
14
18
  }
15
19
  }
package/lib/oauth.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { OAuth2ManagerAccountConfig } from '@hubspot/local-dev-lib/types/Accounts';
2
- export declare function authenticateWithOauth(accountConfig: OAuth2ManagerAccountConfig): Promise<void>;
1
+ import { OAuthConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ export declare function authenticateWithOauth(accountConfig: OAuthConfigAccount): Promise<void>;
package/lib/oauth.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import express from 'express';
2
2
  import open from 'open';
3
3
  import { OAuth2Manager } from '@hubspot/local-dev-lib/models/OAuth2Manager';
4
- import { getAccountConfig } from '@hubspot/local-dev-lib/config';
5
- import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
4
+ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
6
5
  import { addOauthToAccountConfig } from '@hubspot/local-dev-lib/oauth';
7
6
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
8
7
  import { uiLogger } from './ui/logger.js';
@@ -14,15 +13,15 @@ import { EXIT_CODES } from './enums/exitCodes.js';
14
13
  const PORT = 3000;
15
14
  const redirectUri = `http://localhost:${PORT}/oauth-callback`;
16
15
  function buildAuthUrl(oauthManager) {
17
- const { env: accountEnv, clientId, scopes: accountScopes, } = oauthManager.account;
16
+ const { env: accountEnv, auth } = oauthManager.account;
18
17
  const env = accountEnv || ENVIRONMENTS.PROD;
19
- const scopes = accountScopes || DEFAULT_OAUTH_SCOPES;
20
- if (!clientId) {
18
+ const scopes = auth.scopes.length > 0 ? auth.scopes : DEFAULT_OAUTH_SCOPES;
19
+ if (!auth.clientId) {
21
20
  uiLogger.error(lib.oauth.missingClientId);
22
21
  process.exit(EXIT_CODES.ERROR);
23
22
  }
24
23
  return (`${getHubSpotWebsiteOrigin(env)}/oauth/${oauthManager.account.accountId}/authorize` +
25
- `?client_id=${encodeURIComponent(clientId)}` + // app's client ID
24
+ `?client_id=${encodeURIComponent(auth.clientId)}` + // app's client ID
26
25
  `&scope=${encodeURIComponent(scopes.join(' '))}` + // scopes being requested by the app
27
26
  `&redirect_uri=${encodeURIComponent(redirectUri)}` // where to send the user after the consent page
28
27
  );
@@ -45,8 +44,8 @@ async function authorize(oauthManager) {
45
44
  if (req.query.code) {
46
45
  const authCodeProof = {
47
46
  grant_type: 'authorization_code',
48
- client_id: oauthManager.account.clientId,
49
- client_secret: oauthManager.account.clientSecret,
47
+ client_id: oauthManager.account.auth.clientId,
48
+ client_secret: oauthManager.account.auth.clientSecret,
50
49
  redirect_uri: redirectUri,
51
50
  code: req.query.code,
52
51
  };
@@ -82,8 +81,7 @@ async function authorize(oauthManager) {
82
81
  });
83
82
  }
84
83
  function setupOauth(accountConfig) {
85
- const accountId = getAccountIdentifier(accountConfig);
86
- const config = getAccountConfig(accountId);
84
+ const config = getConfigAccountById(accountConfig.accountId);
87
85
  return new OAuth2Manager({
88
86
  ...accountConfig,
89
87
  env: accountConfig.env || config?.env || ENVIRONMENTS.PROD,