@hubspot/cli 7.10.0-experimental.0 → 7.10.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 (32) hide show
  1. package/commands/project/__tests__/validate.test.js +285 -27
  2. package/commands/project/deploy.js +14 -6
  3. package/commands/project/dev/index.js +13 -4
  4. package/commands/project/migrate.js +4 -13
  5. package/commands/project/upload.js +8 -2
  6. package/commands/project/validate.js +72 -12
  7. package/lang/en.d.ts +13 -10
  8. package/lang/en.js +14 -11
  9. package/lib/__tests__/projectProfiles.test.js +273 -32
  10. package/lib/middleware/autoUpdateMiddleware.js +25 -22
  11. package/lib/middleware/fireAlarmMiddleware.js +4 -15
  12. package/lib/projectProfiles.d.ts +4 -3
  13. package/lib/projectProfiles.js +78 -32
  14. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -3
  15. package/lib/projects/localDev/LocalDevLogger.js +0 -9
  16. package/lib/projects/localDev/LocalDevProcess.d.ts +0 -1
  17. package/lib/projects/localDev/LocalDevProcess.js +1 -12
  18. package/lib/projects/localDev/LocalDevState.d.ts +0 -3
  19. package/lib/projects/localDev/LocalDevState.js +0 -9
  20. package/mcp-server/utils/config.js +1 -1
  21. package/package.json +3 -4
  22. package/types/LocalDev.d.ts +0 -1
  23. package/ui/components/BoxWithTitle.d.ts +2 -1
  24. package/ui/components/BoxWithTitle.js +2 -2
  25. package/ui/components/StatusMessageBoxes.d.ts +5 -4
  26. package/ui/components/StatusMessageBoxes.js +8 -8
  27. package/lib/projects/localDev/DevSessionManager.d.ts +0 -17
  28. package/lib/projects/localDev/DevSessionManager.js +0 -56
  29. package/lib/projects/localDev/helpers/devSessionsApi.d.ts +0 -9
  30. package/lib/projects/localDev/helpers/devSessionsApi.js +0 -19
  31. package/lib/ui/boxen.d.ts +0 -5
  32. package/lib/ui/boxen.js +0 -26
@@ -5,11 +5,14 @@ import { getProjectConfig, validateProjectConfig, } from '../../../lib/projects/
5
5
  import { uiLogger } from '../../../lib/ui/logger.js';
6
6
  import { commands } from '../../../lang/en.js';
7
7
  import { isV2Project } from '../../../lib/projects/platformVersion.js';
8
- import { loadAndValidateProfile } from '../../../lib/projectProfiles.js';
8
+ import { validateProjectForProfile } from '../../../lib/projectProfiles.js';
9
9
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
10
10
  import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
11
11
  import { handleTranslate } from '../../../lib/projects/upload.js';
12
12
  import projectValidateCommand from '../validate.js';
13
+ import { getAllHsProfiles } from '@hubspot/project-parsing-lib';
14
+ import SpinniesManager from '../../../lib/ui/SpinniesManager.js';
15
+ import { logError } from '../../../lib/errorHandlers/index.js';
13
16
  // Mock dependencies
14
17
  vi.mock('../../../lib/projects/upload.js');
15
18
  vi.mock('../../../lib/projects/config.js');
@@ -19,15 +22,35 @@ vi.mock('../../../lib/projectProfiles.js');
19
22
  vi.mock('../../../lib/errorHandlers/index.js');
20
23
  vi.mock('@hubspot/local-dev-lib/config');
21
24
  vi.mock('../../../lib/projects/platformVersion.js');
25
+ vi.mock('@hubspot/project-parsing-lib');
26
+ vi.mock('../../../lib/ui/SpinniesManager.js');
22
27
  describe('commands/project/validate', () => {
23
28
  const projectDir = '/test/project';
24
29
  let exitSpy;
30
+ const mockProjectConfig = {
31
+ name: 'test-project',
32
+ srcDir: 'src',
33
+ platformVersion: '2025.2',
34
+ };
35
+ const mockAccountConfig = {
36
+ accountType: 'STANDARD',
37
+ accountId: 123,
38
+ env: 'prod',
39
+ };
25
40
  beforeEach(() => {
26
41
  // Mock process.exit to throw to stop execution
27
42
  exitSpy = vi.spyOn(process, 'exit').mockImplementation(code => {
28
43
  throw new Error(`Process exited with code ${code}`);
29
44
  });
30
45
  vi.clearAllMocks();
46
+ // Set up default mocks
47
+ vi.mocked(getConfigAccountById).mockReturnValue(mockAccountConfig);
48
+ vi.mocked(trackCommandUsage);
49
+ vi.mocked(SpinniesManager.init);
50
+ vi.mocked(SpinniesManager.add);
51
+ vi.mocked(SpinniesManager.succeed);
52
+ vi.mocked(SpinniesManager.fail);
53
+ vi.mocked(validateProjectForProfile).mockResolvedValue([]);
31
54
  });
32
55
  afterEach(() => {
33
56
  exitSpy.mockRestore();
@@ -65,34 +88,269 @@ describe('commands/project/validate', () => {
65
88
  })).rejects.toThrow('Process exited with code 1');
66
89
  expect(uiLogger.error).toHaveBeenCalledWith(commands.project.validate.mustBeRanWithinAProject);
67
90
  });
91
+ it('should exit with error for non-V2 projects', async () => {
92
+ vi.mocked(getProjectConfig).mockResolvedValue({
93
+ projectConfig: {
94
+ name: 'test',
95
+ srcDir: 'src',
96
+ platformVersion: '2024.1',
97
+ },
98
+ projectDir,
99
+ });
100
+ vi.mocked(isV2Project).mockReturnValue(false);
101
+ await expect(
102
+ // @ts-expect-error partial mock
103
+ projectValidateCommand.handler({
104
+ derivedAccountId: 123,
105
+ d: false,
106
+ debug: false,
107
+ })).rejects.toThrow('Process exited with code 1');
108
+ expect(uiLogger.error).toHaveBeenCalledWith(commands.project.validate.badVersion);
109
+ });
110
+ it('should exit with error when validateProjectConfig throws', async () => {
111
+ vi.mocked(getProjectConfig).mockResolvedValue({
112
+ projectConfig: mockProjectConfig,
113
+ projectDir,
114
+ });
115
+ vi.mocked(isV2Project).mockReturnValue(true);
116
+ const error = new Error('Invalid project config');
117
+ vi.mocked(validateProjectConfig).mockImplementation(() => {
118
+ throw error;
119
+ });
120
+ await expect(
121
+ // @ts-expect-error partial mock
122
+ projectValidateCommand.handler({
123
+ derivedAccountId: 123,
124
+ d: false,
125
+ debug: false,
126
+ })).rejects.toThrow('Process exited with code 1');
127
+ expect(logError).toHaveBeenCalledWith(error);
128
+ });
68
129
  });
69
- it('should call validateSourceDirectory with correct parameters', async () => {
70
- const mockProjectConfig = {
71
- name: 'test-project',
72
- srcDir: 'src',
73
- platformVersion: '2025.2',
74
- };
75
- vi.mocked(getProjectConfig).mockResolvedValue({
76
- projectConfig: mockProjectConfig,
77
- projectDir,
130
+ describe('profile validation', () => {
131
+ describe('when a specific profile is provided', () => {
132
+ it('should validate only the specified profile', async () => {
133
+ vi.mocked(getProjectConfig).mockResolvedValue({
134
+ projectConfig: mockProjectConfig,
135
+ projectDir,
136
+ });
137
+ vi.mocked(isV2Project).mockReturnValue(true);
138
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
139
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod', 'qa']);
140
+ vi.mocked(validateProjectForProfile).mockResolvedValue([]);
141
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
142
+ await expect(projectValidateCommand.handler({
143
+ derivedAccountId: 123,
144
+ profile: 'dev',
145
+ d: false,
146
+ debug: false,
147
+ })).rejects.toThrow('Process exited with code 0');
148
+ // Should call validateProjectForProfile for the specified profile
149
+ expect(validateProjectForProfile).toHaveBeenCalledWith(mockProjectConfig, projectDir, 'dev', 123);
150
+ expect(uiLogger.success).toHaveBeenCalledWith(commands.project.validate.success(mockProjectConfig.name));
151
+ });
152
+ it('should handle profile validation failure', async () => {
153
+ vi.mocked(getProjectConfig).mockResolvedValue({
154
+ projectConfig: mockProjectConfig,
155
+ projectDir,
156
+ });
157
+ vi.mocked(isV2Project).mockReturnValue(true);
158
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
159
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod']);
160
+ const error = new Error('Profile not found');
161
+ vi.mocked(validateProjectForProfile).mockResolvedValue([error.message]);
162
+ await expect(projectValidateCommand.handler({
163
+ derivedAccountId: 123,
164
+ profile: 'dev',
165
+ d: false,
166
+ debug: false,
167
+ })).rejects.toThrow('Process exited with code 1');
168
+ // The error message is logged as a string, not the Error object
169
+ expect(uiLogger.error).toHaveBeenCalledWith(error.message);
170
+ });
171
+ it('should handle translate failure for a profile', async () => {
172
+ vi.mocked(getProjectConfig).mockResolvedValue({
173
+ projectConfig: mockProjectConfig,
174
+ projectDir,
175
+ });
176
+ vi.mocked(isV2Project).mockReturnValue(true);
177
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
178
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod']);
179
+ const error = new Error('Translation failed');
180
+ vi.mocked(validateProjectForProfile).mockResolvedValue([
181
+ commands.project.validate.failure(mockProjectConfig.name),
182
+ error,
183
+ ]);
184
+ await expect(projectValidateCommand.handler({
185
+ derivedAccountId: 123,
186
+ profile: 'dev',
187
+ d: false,
188
+ debug: false,
189
+ })).rejects.toThrow('Process exited with code 1');
190
+ // The error object is logged via logError
191
+ expect(logError).toHaveBeenCalledWith(error);
192
+ });
78
193
  });
79
- vi.mocked(isV2Project).mockReturnValue(true);
80
- vi.mocked(validateProjectConfig).mockReturnValue(undefined);
81
- vi.mocked(loadAndValidateProfile).mockResolvedValue(123);
82
- vi.mocked(getConfigAccountById).mockReturnValue({
83
- accountType: 'STANDARD',
84
- accountId: 123,
85
- env: 'prod',
194
+ describe('when no profile is provided and project has profiles', () => {
195
+ it('should validate all profiles', async () => {
196
+ vi.mocked(getProjectConfig).mockResolvedValue({
197
+ projectConfig: mockProjectConfig,
198
+ projectDir,
199
+ });
200
+ vi.mocked(isV2Project).mockReturnValue(true);
201
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
202
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod', 'qa']);
203
+ vi.mocked(validateProjectForProfile).mockResolvedValue([]);
204
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
205
+ await expect(projectValidateCommand.handler({
206
+ derivedAccountId: 123,
207
+ d: false,
208
+ debug: false,
209
+ })).rejects.toThrow('Process exited with code 0');
210
+ // Should validate all three profiles
211
+ expect(validateProjectForProfile).toHaveBeenCalledTimes(3);
212
+ expect(validateProjectForProfile).toHaveBeenCalledWith(mockProjectConfig, projectDir, 'dev', 123, true);
213
+ expect(validateProjectForProfile).toHaveBeenCalledWith(mockProjectConfig, projectDir, 'prod', 123, true);
214
+ expect(validateProjectForProfile).toHaveBeenCalledWith(mockProjectConfig, projectDir, 'qa', 123, true);
215
+ // Should show success for all profiles
216
+ expect(SpinniesManager.succeed).toHaveBeenCalledWith('validatingAllProfiles', expect.any(Object));
217
+ expect(uiLogger.success).toHaveBeenCalledWith(commands.project.validate.success(mockProjectConfig.name));
218
+ });
219
+ it('should handle failure when validating multiple profiles', async () => {
220
+ vi.mocked(getProjectConfig).mockResolvedValue({
221
+ projectConfig: mockProjectConfig,
222
+ projectDir,
223
+ });
224
+ vi.mocked(isV2Project).mockReturnValue(true);
225
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
226
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod']);
227
+ vi.mocked(validateProjectForProfile)
228
+ .mockResolvedValueOnce([]) // dev succeeds
229
+ .mockResolvedValueOnce(['Profile not found']); // prod fails
230
+ await expect(projectValidateCommand.handler({
231
+ derivedAccountId: 123,
232
+ d: false,
233
+ debug: false,
234
+ })).rejects.toThrow('Process exited with code 1');
235
+ expect(SpinniesManager.fail).toHaveBeenCalledWith('validatingAllProfiles', expect.any(Object));
236
+ });
237
+ it('should continue validating remaining profiles after one fails', async () => {
238
+ vi.mocked(getProjectConfig).mockResolvedValue({
239
+ projectConfig: mockProjectConfig,
240
+ projectDir,
241
+ });
242
+ vi.mocked(isV2Project).mockReturnValue(true);
243
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
244
+ vi.mocked(getAllHsProfiles).mockResolvedValue(['dev', 'prod', 'qa']);
245
+ vi.mocked(validateProjectForProfile)
246
+ .mockResolvedValueOnce([]) // dev succeeds
247
+ .mockResolvedValueOnce(['Profile not found']) // prod fails
248
+ .mockResolvedValueOnce([]); // qa succeeds
249
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
250
+ await expect(projectValidateCommand.handler({
251
+ derivedAccountId: 123,
252
+ d: false,
253
+ debug: false,
254
+ })).rejects.toThrow('Process exited with code 1');
255
+ // All three profiles should be attempted
256
+ expect(validateProjectForProfile).toHaveBeenCalledTimes(3);
257
+ });
258
+ });
259
+ describe('when no profile is provided and project has no profiles', () => {
260
+ it('should validate without a profile', async () => {
261
+ vi.mocked(getProjectConfig).mockResolvedValue({
262
+ projectConfig: mockProjectConfig,
263
+ projectDir,
264
+ });
265
+ vi.mocked(isV2Project).mockReturnValue(true);
266
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
267
+ vi.mocked(getAllHsProfiles).mockResolvedValue([]);
268
+ vi.mocked(handleTranslate).mockResolvedValue(undefined);
269
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
270
+ await expect(projectValidateCommand.handler({
271
+ derivedAccountId: 123,
272
+ d: false,
273
+ debug: false,
274
+ })).rejects.toThrow('Process exited with code 0');
275
+ // Should call handleTranslate without a profile
276
+ expect(handleTranslate).toHaveBeenCalledWith(projectDir, mockProjectConfig, 123, false, undefined);
277
+ expect(uiLogger.success).toHaveBeenCalledWith(commands.project.validate.success(mockProjectConfig.name));
278
+ });
279
+ it('should handle validation failure when no profiles exist', async () => {
280
+ vi.mocked(getProjectConfig).mockResolvedValue({
281
+ projectConfig: mockProjectConfig,
282
+ projectDir,
283
+ });
284
+ vi.mocked(isV2Project).mockReturnValue(true);
285
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
286
+ vi.mocked(getAllHsProfiles).mockResolvedValue([]);
287
+ const error = new Error('Translation failed');
288
+ vi.mocked(handleTranslate).mockRejectedValue(error);
289
+ await expect(projectValidateCommand.handler({
290
+ derivedAccountId: 123,
291
+ d: false,
292
+ debug: false,
293
+ })).rejects.toThrow('Process exited with code 1');
294
+ expect(uiLogger.error).toHaveBeenCalledWith(commands.project.validate.failure(mockProjectConfig.name));
295
+ expect(logError).toHaveBeenCalledWith(error);
296
+ });
297
+ });
298
+ });
299
+ describe('source directory validation', () => {
300
+ it('should call validateSourceDirectory with correct parameters', async () => {
301
+ vi.mocked(getProjectConfig).mockResolvedValue({
302
+ projectConfig: mockProjectConfig,
303
+ projectDir,
304
+ });
305
+ vi.mocked(isV2Project).mockReturnValue(true);
306
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
307
+ vi.mocked(getAllHsProfiles).mockResolvedValue([]);
308
+ vi.mocked(handleTranslate).mockResolvedValue(undefined);
309
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
310
+ await expect(projectValidateCommand.handler({
311
+ derivedAccountId: 123,
312
+ d: false,
313
+ debug: false,
314
+ })).rejects.toThrow('Process exited with code 0');
315
+ const expectedSrcDir = path.resolve(projectDir, mockProjectConfig.srcDir);
316
+ expect(validateSourceDirectory).toHaveBeenCalledWith(expectedSrcDir, mockProjectConfig, projectDir);
317
+ });
318
+ it('should exit with error when validateSourceDirectory throws', async () => {
319
+ vi.mocked(getProjectConfig).mockResolvedValue({
320
+ projectConfig: mockProjectConfig,
321
+ projectDir,
322
+ });
323
+ vi.mocked(isV2Project).mockReturnValue(true);
324
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
325
+ vi.mocked(getAllHsProfiles).mockResolvedValue([]);
326
+ vi.mocked(handleTranslate).mockResolvedValue(undefined);
327
+ const error = new Error('Invalid source directory');
328
+ vi.mocked(validateSourceDirectory).mockRejectedValue(error);
329
+ await expect(projectValidateCommand.handler({
330
+ derivedAccountId: 123,
331
+ d: false,
332
+ debug: false,
333
+ })).rejects.toThrow('Process exited with code 1');
334
+ expect(logError).toHaveBeenCalledWith(error);
335
+ });
336
+ });
337
+ describe('command usage tracking', () => {
338
+ it('should track command usage with account type', async () => {
339
+ vi.mocked(getProjectConfig).mockResolvedValue({
340
+ projectConfig: mockProjectConfig,
341
+ projectDir,
342
+ });
343
+ vi.mocked(isV2Project).mockReturnValue(true);
344
+ vi.mocked(validateProjectConfig).mockReturnValue(undefined);
345
+ vi.mocked(getAllHsProfiles).mockResolvedValue([]);
346
+ vi.mocked(handleTranslate).mockResolvedValue(undefined);
347
+ vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
348
+ await expect(projectValidateCommand.handler({
349
+ derivedAccountId: 123,
350
+ d: false,
351
+ debug: false,
352
+ })).rejects.toThrow('Process exited with code 0');
353
+ expect(trackCommandUsage).toHaveBeenCalledWith('project-validate', { type: 'STANDARD' }, 123);
86
354
  });
87
- vi.mocked(trackCommandUsage);
88
- vi.mocked(validateSourceDirectory).mockResolvedValue(undefined);
89
- vi.mocked(handleTranslate).mockResolvedValue(undefined);
90
- await expect(projectValidateCommand.handler({
91
- derivedAccountId: 123,
92
- d: false,
93
- debug: false,
94
- })).rejects.toThrow('Process exited with code 0');
95
- const expectedSrcDir = path.resolve(projectDir, mockProjectConfig.srcDir);
96
- expect(validateSourceDirectory).toHaveBeenCalledWith(expectedSrcDir, mockProjectConfig, projectDir);
97
355
  });
98
356
  });
@@ -7,11 +7,10 @@ import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
7
7
  import { getProjectConfig } from '../../lib/projects/config.js';
8
8
  import { projectNamePrompt } from '../../lib/prompts/projectNamePrompt.js';
9
9
  import { promptUser } from '../../lib/prompts/promptUtils.js';
10
- import { uiLine } from '../../lib/ui/index.js';
11
10
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
12
11
  import { uiLogger } from '../../lib/ui/logger.js';
13
12
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
14
- import { loadProfile, logProfileFooter, logProfileHeader, exitIfUsingProfiles, } from '../../lib/projectProfiles.js';
13
+ import { loadProfile, logProfileFooter, logProfileHeader, enforceProfileUsage, } from '../../lib/projectProfiles.js';
15
14
  import { PROJECT_DEPLOY_TEXT } from '../../lib/constants.js';
16
15
  import { commands } from '../../lang/en.js';
17
16
  import { handleProjectDeploy, validateBuildIdForDeploy, logDeployErrors, } from '../../lib/projects/deploy.js';
@@ -27,9 +26,12 @@ async function handler(args) {
27
26
  if (isV2Project(projectConfig?.platformVersion)) {
28
27
  if (args.profile) {
29
28
  logProfileHeader(args.profile);
30
- const profile = loadProfile(projectConfig, projectDir, args.profile);
31
- if (!profile) {
32
- uiLine();
29
+ let profile;
30
+ try {
31
+ profile = loadProfile(projectConfig, projectDir, args.profile);
32
+ }
33
+ catch (error) {
34
+ logError(error);
33
35
  process.exit(EXIT_CODES.ERROR);
34
36
  }
35
37
  targetAccountId = profile.accountId;
@@ -37,7 +39,13 @@ async function handler(args) {
37
39
  }
38
40
  else {
39
41
  // A profile must be specified if this project has profiles configured
40
- await exitIfUsingProfiles(projectConfig, projectDir);
42
+ try {
43
+ await enforceProfileUsage(projectConfig, projectDir);
44
+ }
45
+ catch (error) {
46
+ logError(error);
47
+ process.exit(EXIT_CODES.ERROR);
48
+ }
41
49
  }
42
50
  }
43
51
  if (!targetAccountId) {
@@ -7,7 +7,7 @@ import { deprecatedProjectDevFlow } from './deprecatedFlow.js';
7
7
  import { unifiedProjectDevFlow } from './unifiedFlow.js';
8
8
  import { isV2Project } from '../../../lib/projects/platformVersion.js';
9
9
  import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
10
- import { loadProfile, exitIfUsingProfiles, } from '../../../lib/projectProfiles.js';
10
+ import { loadProfile, enforceProfileUsage, } from '../../../lib/projectProfiles.js';
11
11
  import { commands } from '../../../lang/en.js';
12
12
  import { uiLogger } from '../../../lib/ui/logger.js';
13
13
  import { logError } from '../../../lib/errorHandlers/index.js';
@@ -64,8 +64,11 @@ async function handler(args) {
64
64
  }
65
65
  if (!targetProjectAccountId && isV2Project(projectConfig.platformVersion)) {
66
66
  if (args.profile) {
67
- profile = loadProfile(projectConfig, projectDir, args.profile);
68
- if (!profile) {
67
+ try {
68
+ profile = loadProfile(projectConfig, projectDir, args.profile);
69
+ }
70
+ catch (error) {
71
+ logError(error);
69
72
  uiLine();
70
73
  process.exit(EXIT_CODES.ERROR);
71
74
  }
@@ -75,7 +78,13 @@ async function handler(args) {
75
78
  }
76
79
  else {
77
80
  // A profile must be specified if this project has profiles configured
78
- await exitIfUsingProfiles(projectConfig, projectDir);
81
+ try {
82
+ await enforceProfileUsage(projectConfig, projectDir);
83
+ }
84
+ catch (error) {
85
+ logError(error);
86
+ process.exit(EXIT_CODES.ERROR);
87
+ }
79
88
  }
80
89
  }
81
90
  if (!targetProjectAccountId) {
@@ -7,7 +7,6 @@ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
7
7
  import { uiCommandReference } from '../../lib/ui/index.js';
8
8
  import { commands, lib } from '../../lang/en.js';
9
9
  import { uiLogger } from '../../lib/ui/logger.js';
10
- import { logInBox } from '../../lib/ui/boxen.js';
11
10
  import { renderInline } from '../../ui/index.js';
12
11
  import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
13
12
  import { getHasMigratableThemes, migrateThemes2025_2, } from '../../lib/theme/migrate.js';
@@ -26,18 +25,10 @@ async function handler(args) {
26
25
  return process.exit(EXIT_CODES.ERROR);
27
26
  }
28
27
  if (projectConfig?.projectConfig) {
29
- if (!process.env.HUBSPOT_ENABLE_INK) {
30
- await logInBox({
31
- contents: lib.migrate.projectMigrationWarning,
32
- options: { title: lib.migrate.projectMigrationWarningTitle },
33
- });
34
- }
35
- else {
36
- await renderInline(getWarningBox({
37
- title: lib.migrate.projectMigrationWarningTitle,
38
- message: lib.migrate.projectMigrationWarning,
39
- }));
40
- }
28
+ await renderInline(getWarningBox({
29
+ title: lib.migrate.projectMigrationWarningTitle,
30
+ message: lib.migrate.projectMigrationWarning,
31
+ }));
41
32
  }
42
33
  try {
43
34
  const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes(projectConfig);
@@ -28,8 +28,14 @@ async function handler(args) {
28
28
  process.exit(EXIT_CODES.ERROR);
29
29
  }
30
30
  let targetAccountId;
31
- if (isV2Project(projectConfig.platformVersion)) {
32
- targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
31
+ try {
32
+ if (isV2Project(projectConfig.platformVersion)) {
33
+ targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
34
+ }
35
+ }
36
+ catch (err) {
37
+ logError(err);
38
+ process.exit(EXIT_CODES.ERROR);
33
39
  }
34
40
  targetAccountId = targetAccountId || derivedAccountId;
35
41
  const accountConfig = getConfigAccountById(targetAccountId);
@@ -8,13 +8,19 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
8
8
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
9
9
  import { validateSourceDirectory, handleTranslate, } from '../../lib/projects/upload.js';
10
10
  import { commands } from '../../lang/en.js';
11
- import { loadAndValidateProfile } from '../../lib/projectProfiles.js';
11
+ import { validateProjectForProfile } from '../../lib/projectProfiles.js';
12
12
  import { logError } from '../../lib/errorHandlers/index.js';
13
+ import { getAllHsProfiles } from '@hubspot/project-parsing-lib';
14
+ import SpinniesManager from '../../lib/ui/SpinniesManager.js';
13
15
  const command = 'validate';
14
16
  const describe = commands.project.validate.describe;
15
17
  async function handler(args) {
18
+ SpinniesManager.init();
16
19
  const { derivedAccountId, profile } = args;
17
20
  const { projectConfig, projectDir } = await getProjectConfig();
21
+ const accountConfig = getConfigAccountById(derivedAccountId);
22
+ const accountType = accountConfig && accountConfig.accountType;
23
+ trackCommandUsage('project-validate', { type: accountType }, derivedAccountId);
18
24
  if (!projectConfig || !projectDir) {
19
25
  uiLogger.error(commands.project.validate.mustBeRanWithinAProject);
20
26
  process.exit(EXIT_CODES.ERROR);
@@ -30,25 +36,79 @@ async function handler(args) {
30
36
  logError(error);
31
37
  process.exit(EXIT_CODES.ERROR);
32
38
  }
33
- let targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
34
- targetAccountId = targetAccountId || derivedAccountId;
35
- const accountConfig = getConfigAccountById(targetAccountId);
36
- const accountType = accountConfig && accountConfig.accountType;
37
- trackCommandUsage('project-validate', { type: accountType }, targetAccountId);
39
+ let validationSucceeded = true;
38
40
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
39
- try {
40
- await validateSourceDirectory(srcDir, projectConfig, projectDir);
41
+ // Get all of the profiles except the provided profile
42
+ const profiles = (await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir))).filter(profileName => profileName !== profile);
43
+ // If a profile is specified, only validate that profile
44
+ if (profile) {
45
+ const validationErrors = await validateProjectForProfile(projectConfig, projectDir, profile, derivedAccountId);
46
+ if (validationErrors.length) {
47
+ validationErrors.forEach(error => {
48
+ uiLogger.log('');
49
+ if (error instanceof Error) {
50
+ logError(error);
51
+ }
52
+ else {
53
+ uiLogger.error(error);
54
+ }
55
+ });
56
+ validationSucceeded = false;
57
+ }
41
58
  }
42
- catch (e) {
43
- logError(e);
59
+ else if (profiles.length > 0) {
60
+ // If no profile was specified and the project has profiles, validate all of them
61
+ SpinniesManager.add('validatingAllProfiles', {
62
+ text: commands.project.validate.spinners.validatingAllProfiles,
63
+ });
64
+ const errors = [];
65
+ for (const profileName of profiles) {
66
+ const validationErrors = await validateProjectForProfile(projectConfig, projectDir, profileName, derivedAccountId, true);
67
+ if (validationErrors.length) {
68
+ errors.push(...validationErrors);
69
+ validationSucceeded = false;
70
+ }
71
+ }
72
+ if (validationSucceeded) {
73
+ SpinniesManager.succeed('validatingAllProfiles', {
74
+ text: commands.project.validate.spinners.allProfilesValidationSucceeded,
75
+ });
76
+ }
77
+ else {
78
+ SpinniesManager.fail('validatingAllProfiles', {
79
+ text: commands.project.validate.spinners.allProfilesValidationFailed,
80
+ });
81
+ }
82
+ errors.forEach(error => {
83
+ uiLogger.log('');
84
+ if (error instanceof Error) {
85
+ logError(error);
86
+ }
87
+ else {
88
+ uiLogger.error(error);
89
+ }
90
+ });
91
+ }
92
+ else if (profiles.length === 0) {
93
+ // If the project has no profiles, validate the project without a profile
94
+ try {
95
+ await handleTranslate(projectDir, projectConfig, derivedAccountId, false, undefined);
96
+ }
97
+ catch (e) {
98
+ uiLogger.error(commands.project.validate.failure(projectConfig.name));
99
+ logError(e);
100
+ validationSucceeded = false;
101
+ uiLogger.log('');
102
+ }
103
+ }
104
+ if (!validationSucceeded) {
44
105
  process.exit(EXIT_CODES.ERROR);
45
106
  }
46
107
  try {
47
- await handleTranslate(projectDir, projectConfig, targetAccountId, false, profile);
108
+ await validateSourceDirectory(srcDir, projectConfig, projectDir);
48
109
  }
49
110
  catch (e) {
50
111
  logError(e);
51
- uiLogger.error(commands.project.validate.failure(projectConfig.name));
52
112
  process.exit(EXIT_CODES.ERROR);
53
113
  }
54
114
  uiLogger.success(commands.project.validate.success(projectConfig.name));
package/lang/en.d.ts CHANGED
@@ -1802,7 +1802,16 @@ export declare const commands: {
1802
1802
  default: string;
1803
1803
  };
1804
1804
  success: (projectName: string) => string;
1805
- failure: (projectName: string) => string;
1805
+ failure: (projectName: string, profileName?: string) => string;
1806
+ spinners: {
1807
+ validatingProfile: (profileName: string) => string;
1808
+ profileValidationFailed: (profileName: string) => string;
1809
+ profileValidationSucceeded: (profileName: string) => string;
1810
+ invalidWithProfile: (profileName: string, projectName: string) => string;
1811
+ validatingAllProfiles: string;
1812
+ allProfilesValidationSucceeded: string;
1813
+ allProfilesValidationFailed: string;
1814
+ };
1806
1815
  options: {
1807
1816
  profile: {
1808
1817
  describe: string;
@@ -2844,11 +2853,6 @@ export declare const lib: {
2844
2853
  startError: (message: string) => string;
2845
2854
  fileChangeError: (message: string) => string;
2846
2855
  };
2847
- devSession: {
2848
- registrationError: (message: string) => string;
2849
- heartbeatError: (message: string) => string;
2850
- deletionError: (message: string) => string;
2851
- };
2852
2856
  };
2853
2857
  AppDevModeInterface: {
2854
2858
  autoInstallStaticAuthApp: {
@@ -2961,7 +2965,7 @@ export declare const lib: {
2961
2965
  updateNotification: {
2962
2966
  notifyTitle: string;
2963
2967
  cmsUpdateNotification: (packageName: string) => string;
2964
- cliUpdateNotification: string;
2968
+ cliUpdateNotification: (currentVersion: string, updateCommand: string, latestVersion: string) => string;
2965
2969
  };
2966
2970
  autoUpdateCLI: {
2967
2971
  updateAvailable: (latestVersion: string) => string;
@@ -2986,7 +2990,9 @@ export declare const lib: {
2986
2990
  noProjectConfig: string;
2987
2991
  profileNotFound: (profileName: string) => string;
2988
2992
  missingAccountId: (profileName: string) => string;
2993
+ listedAccountNotFound: (accountId: number, profileName: string) => string;
2989
2994
  failedToLoadProfile: (profileName: string) => string;
2995
+ profileNotValid: (profileName: string, errors: string[]) => string;
2990
2996
  };
2991
2997
  };
2992
2998
  };
@@ -3077,9 +3083,6 @@ export declare const lib: {
3077
3083
  legacyFileDetected: (filename: string, platformVersion: string) => string;
3078
3084
  };
3079
3085
  };
3080
- boxen: {
3081
- failedToLoad: string;
3082
- };
3083
3086
  importData: {
3084
3087
  errors: {
3085
3088
  incorrectAccountType: (derivedAccountId: number) => string;