@hubspot/cli 7.7.27-experimental.2 → 7.7.29-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +0 -4
  2. package/api/__tests__/migrate.test.js +5 -5
  3. package/api/migrate.d.ts +10 -4
  4. package/api/migrate.js +2 -2
  5. package/commands/__tests__/create.test.js +20 -0
  6. package/commands/__tests__/testAccount.test.js +2 -0
  7. package/commands/app/__tests__/migrate.test.js +1 -0
  8. package/commands/create/function.js +2 -2
  9. package/commands/create/module.js +2 -2
  10. package/commands/create/template.js +2 -2
  11. package/commands/create.js +47 -0
  12. package/commands/getStarted.js +66 -4
  13. package/commands/mcp/setup.d.ts +0 -1
  14. package/commands/mcp/setup.js +3 -11
  15. package/commands/project/__tests__/create.test.js +57 -0
  16. package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
  17. package/commands/project/create.js +6 -1
  18. package/commands/project/deploy.js +31 -1
  19. package/commands/project/dev/deprecatedFlow.js +2 -1
  20. package/commands/project/dev/index.js +32 -12
  21. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  22. package/commands/project/dev/unifiedFlow.js +10 -16
  23. package/commands/project/profile/delete.js +26 -14
  24. package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
  25. package/commands/testAccount/__tests__/importData.test.js +93 -0
  26. package/commands/testAccount/create.js +23 -13
  27. package/commands/testAccount/importData.d.ts +9 -0
  28. package/commands/testAccount/importData.js +61 -0
  29. package/commands/testAccount.js +2 -0
  30. package/lang/en.d.ts +162 -46
  31. package/lang/en.js +177 -59
  32. package/lang/en.lyaml +35 -14
  33. package/lib/__tests__/importData.test.d.ts +1 -0
  34. package/lib/__tests__/importData.test.js +89 -0
  35. package/lib/accountTypes.js +2 -3
  36. package/lib/app/__tests__/migrate.test.js +81 -36
  37. package/lib/app/migrate.d.ts +17 -4
  38. package/lib/app/migrate.js +97 -19
  39. package/lib/constants.d.ts +1 -0
  40. package/lib/constants.js +1 -0
  41. package/lib/hasFeature.d.ts +1 -0
  42. package/lib/hasFeature.js +7 -0
  43. package/lib/importData.d.ts +3 -0
  44. package/lib/importData.js +50 -0
  45. package/lib/mcp/setup.d.ts +3 -5
  46. package/lib/mcp/setup.js +39 -139
  47. package/lib/process.js +15 -4
  48. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
  49. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
  50. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  51. package/lib/projects/__tests__/components.test.js +164 -7
  52. package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
  53. package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
  54. package/lib/projects/add/v3AddComponent.js +16 -4
  55. package/lib/projects/components.d.ts +1 -0
  56. package/lib/projects/components.js +27 -1
  57. package/lib/projects/localDev/AppDevModeInterface.js +35 -3
  58. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
  59. package/lib/projects/localDev/LocalDevLogger.js +2 -19
  60. package/lib/projects/localDev/LocalDevManager.js +1 -1
  61. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
  62. package/lib/projects/localDev/LocalDevProcess.js +3 -26
  63. package/lib/projects/localDev/LocalDevState.d.ts +6 -7
  64. package/lib/projects/localDev/LocalDevState.js +16 -15
  65. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  66. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  67. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
  68. package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
  69. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  70. package/lib/projects/localDev/helpers/project.js +173 -0
  71. package/lib/projects/urls.d.ts +1 -0
  72. package/lib/projects/urls.js +4 -0
  73. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  74. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  75. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  76. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  77. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  78. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  79. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  80. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  81. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  82. package/lib/prompts/createFunctionPrompt.js +36 -7
  83. package/lib/prompts/createModulePrompt.d.ts +2 -1
  84. package/lib/prompts/createModulePrompt.js +48 -1
  85. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  86. package/lib/prompts/createTemplatePrompt.js +9 -1
  87. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  88. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  89. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  90. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  91. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  92. package/lib/prompts/promptUtils.d.ts +7 -1
  93. package/lib/prompts/promptUtils.js +14 -1
  94. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  95. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  96. package/lib/ui/index.js +3 -6
  97. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  98. package/lib/ui/removeAnsiCodes.js +4 -0
  99. package/mcp-server/server.js +2 -1
  100. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  101. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  102. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  103. package/mcp-server/tools/cms/HsListTool.js +58 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  106. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  107. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  108. package/mcp-server/tools/index.d.ts +1 -0
  109. package/mcp-server/tools/index.js +12 -0
  110. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  111. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  112. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  113. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  114. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  115. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  116. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  117. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  118. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  119. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  120. package/mcp-server/tools/project/constants.d.ts +2 -0
  121. package/mcp-server/tools/project/constants.js +6 -0
  122. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  123. package/mcp-server/utils/toolUsageTracking.js +2 -1
  124. package/package.json +9 -6
  125. package/types/Cms.d.ts +16 -0
  126. package/types/Cms.js +25 -1
  127. package/types/LocalDev.d.ts +0 -3
  128. package/types/Prompts.d.ts +1 -0
  129. package/ui/index.d.ts +1 -0
  130. package/ui/index.js +6 -0
@@ -1,7 +1,29 @@
1
1
  import fs from 'fs';
2
- import { handleComponentCollision } from '../components.js';
2
+ import { handleComponentCollision, updateHsMetaFilesWithAutoGeneratedFields, } from '../components.js';
3
+ import { uiLogger } from '../../ui/logger.js';
4
+ import { coerceToValidUid } from '@hubspot/project-parsing-lib';
3
5
  vi.mock('fs');
6
+ vi.mock('../../ui/logger.js');
7
+ vi.mock('@hubspot/project-parsing-lib', () => ({
8
+ coerceToValidUid: vi.fn(),
9
+ metafileExtension: '.module.meta.json',
10
+ }));
11
+ vi.mock('@hubspot/project-parsing-lib/src/lib/constants.js', () => ({
12
+ AppKey: 'app',
13
+ }));
14
+ vi.mock('../../../lang/en.js', () => ({
15
+ lib: {
16
+ projects: {
17
+ updateHsMetaFilesWithAutoGeneratedFields: {
18
+ header: 'Updating component metadata files...',
19
+ applicationLog: (type, uid, name) => `Updated ${type} component with uid: ${uid} and name: ${name}`,
20
+ componentLog: (type, uid) => `Updated ${type} component with uid: ${uid}`,
21
+ },
22
+ },
23
+ },
24
+ }));
4
25
  const mockedFs = vi.mocked(fs);
26
+ const mockCoerceToValidUid = vi.mocked(coerceToValidUid);
5
27
  describe('lib/projects/components', () => {
6
28
  describe('handleComponentCollision()', () => {
7
29
  const mockCollision = {
@@ -152,10 +174,9 @@ describe('lib/projects/components', () => {
152
174
  collisions: [
153
175
  'regular.js',
154
176
  'nested/path/file.ts',
155
- 'component.module.meta.json',
177
+ 'component.meta.json',
156
178
  'another.meta.json',
157
179
  'package.json',
158
- 'nested/package.json',
159
180
  ],
160
181
  };
161
182
  // Mock metafileExtension
@@ -166,16 +187,152 @@ describe('lib/projects/components', () => {
166
187
  const mockMetaContent = '{}';
167
188
  mockedFs.readFileSync.mockReturnValue(mockMetaContent);
168
189
  mockedFs.writeFileSync.mockImplementation(() => { });
190
+ // Track what files are being copied to debug the issue
169
191
  mockedFs.copyFileSync.mockImplementation(() => { });
170
192
  // Mock console.log for package.json handling
171
193
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
172
194
  handleComponentCollision(collision);
173
- // Should copy 2 source files
174
- expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2);
195
+ expect(mockedFs.readFileSync).toHaveBeenCalledTimes(2);
175
196
  // Should handle 2 metafiles
176
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/component.module.meta.json', 'utf-8');
177
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/another.meta.json', 'utf-8');
197
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith('/dest/path/package.json', 'utf-8');
198
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/package.json', 'utf-8');
178
199
  consoleSpy.mockRestore();
179
200
  });
180
201
  });
202
+ describe('updateHsMetaFilesWithAutoGeneratedFields()', () => {
203
+ const mockUiLogger = vi.mocked(uiLogger);
204
+ beforeEach(() => {
205
+ vi.resetAllMocks();
206
+ mockCoerceToValidUid.mockImplementation((input) => input);
207
+ });
208
+ afterEach(() => {
209
+ vi.restoreAllMocks();
210
+ });
211
+ it('updates component metadata files with project-specific UIDs', () => {
212
+ const projectName = 'my-project';
213
+ const hsMetaFilePaths = [
214
+ '/path/to/component1.meta.json',
215
+ '/path/to/component2.meta.json',
216
+ ];
217
+ const component1 = {
218
+ type: 'card',
219
+ uid: 'old-uid-1',
220
+ config: {
221
+ name: 'Old Name',
222
+ },
223
+ };
224
+ const component2 = {
225
+ type: 'function',
226
+ uid: 'old-uid-2',
227
+ };
228
+ mockedFs.readFileSync
229
+ .mockReturnValueOnce(JSON.stringify(component1))
230
+ .mockReturnValueOnce(JSON.stringify(component2));
231
+ mockedFs.writeFileSync.mockImplementation(() => { });
232
+ mockCoerceToValidUid
233
+ .mockReturnValueOnce('card-my-project')
234
+ .mockReturnValueOnce('function-my-project');
235
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
236
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
237
+ type: 'card',
238
+ uid: 'card-my-project',
239
+ config: {
240
+ name: 'Old Name',
241
+ },
242
+ }, null, 2));
243
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component2.meta.json', JSON.stringify({
244
+ type: 'function',
245
+ uid: 'function-my-project',
246
+ }, null, 2));
247
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
248
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid: card-my-project');
249
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid: function-my-project');
250
+ });
251
+ it('handles app components by updating both uid and config.name', () => {
252
+ const projectName = 'test-app';
253
+ const hsMetaFilePaths = ['/path/to/app.meta.json'];
254
+ const appComponent = {
255
+ type: 'app',
256
+ uid: 'old-app-uid',
257
+ config: {
258
+ name: 'Old App Name',
259
+ other: 'property',
260
+ },
261
+ };
262
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
263
+ mockedFs.writeFileSync.mockImplementation(() => { });
264
+ mockCoerceToValidUid.mockReturnValue('app-test-app');
265
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
266
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
267
+ type: 'app',
268
+ uid: 'app-test-app',
269
+ config: {
270
+ name: 'test-app-Application',
271
+ other: 'property',
272
+ },
273
+ }, null, 2));
274
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-test-app and name: test-app-Application');
275
+ });
276
+ it('handles UID collisions by using timestamps', () => {
277
+ const projectName = 'collision-project';
278
+ const hsMetaFilePaths = ['/path/to/component1.meta.json'];
279
+ const existingUids = ['card-collision-project'];
280
+ const component1 = { type: 'card', uid: 'old-uid-1' };
281
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(component1));
282
+ mockedFs.writeFileSync.mockImplementation(() => { });
283
+ // Mock Date.now to return consistent value for testing
284
+ vi.spyOn(Date, 'now').mockReturnValue(1234567890);
285
+ mockCoerceToValidUid
286
+ .mockReturnValueOnce('card-collision-project')
287
+ .mockReturnValueOnce('card-1234567890-collision-project');
288
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids);
289
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
290
+ type: 'card',
291
+ uid: 'card-1234567890-collision-project',
292
+ }, null, 2));
293
+ });
294
+ it('falls back to original uid when coerceToValidUid returns null', () => {
295
+ const projectName = 'fallback-project';
296
+ const hsMetaFilePaths = ['/path/to/component.meta.json'];
297
+ const component = {
298
+ type: 'card',
299
+ uid: 'original-uid',
300
+ };
301
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
302
+ mockedFs.writeFileSync.mockImplementation(() => { });
303
+ mockCoerceToValidUid.mockReturnValue(undefined);
304
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
305
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
306
+ type: 'card',
307
+ uid: 'original-uid',
308
+ }, null, 2));
309
+ });
310
+ it('handles empty hsMetaFilePaths array', () => {
311
+ const projectName = 'empty-project';
312
+ const hsMetaFilePaths = [];
313
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
314
+ expect(mockedFs.readFileSync).not.toHaveBeenCalled();
315
+ expect(mockedFs.writeFileSync).not.toHaveBeenCalled();
316
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
317
+ expect(mockUiLogger.log).toHaveBeenCalledWith('');
318
+ });
319
+ it('handles components without config property for app type', () => {
320
+ const projectName = 'no-config-project';
321
+ const hsMetaFilePaths = ['/path/to/app.meta.json'];
322
+ const appComponent = {
323
+ type: 'app',
324
+ uid: 'app-uid',
325
+ // No config property
326
+ };
327
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
328
+ mockedFs.writeFileSync.mockImplementation(() => { });
329
+ mockCoerceToValidUid.mockReturnValue('app-no-config-project');
330
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
331
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
332
+ type: 'app',
333
+ uid: 'app-no-config-project',
334
+ }, null, 2));
335
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-no-config-project');
336
+ });
337
+ });
181
338
  });
@@ -0,0 +1,118 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
4
+ import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
5
+ import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
6
+ import { isDeepEqual } from '@hubspot/local-dev-lib/isDeepEqual';
7
+ import { translate } from '@hubspot/project-parsing-lib';
8
+ import { isDeployedProjectUpToDateWithLocal } from '../localDev/helpers/project.js';
9
+ // Mock all external dependencies
10
+ vi.mock('@hubspot/local-dev-lib/api/projects');
11
+ vi.mock('@hubspot/local-dev-lib/archive');
12
+ vi.mock('@hubspot/project-parsing-lib');
13
+ vi.mock('@hubspot/local-dev-lib/isDeepEqual');
14
+ vi.mock('fs-extra');
15
+ vi.mock('os');
16
+ vi.mock('../../utils/isDeepEqual.js');
17
+ describe('isDeployedProjectUpToDateWithLocal', () => {
18
+ const mockProjectName = 'test-project';
19
+ const mockAccountId = 123456;
20
+ const mockBuildId = 789;
21
+ const mockProjectConfig = {
22
+ name: mockProjectName,
23
+ srcDir: 'src',
24
+ platformVersion: '1.0.0',
25
+ };
26
+ const mockLocalNode = {
27
+ uid: 'component1',
28
+ componentType: 'APP',
29
+ localDev: {
30
+ componentRoot: '/local/path',
31
+ componentConfigPath: '/local/path/config.json',
32
+ configUpdatedSinceLastUpload: false,
33
+ },
34
+ componentDeps: {},
35
+ metaFilePath: '/local/path',
36
+ config: { name: 'Component 1' },
37
+ files: [],
38
+ };
39
+ const mockLocalProjectNodes = {
40
+ component1: mockLocalNode,
41
+ };
42
+ const mockTempDir = '/tmp/test-temp-dir';
43
+ const mockZippedProject = Buffer.from('fake-zip-data');
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ // Mock fs.mkdtemp
47
+ fs.mkdtemp.mockResolvedValue(mockTempDir);
48
+ // Mock fs.pathExists
49
+ fs.pathExists.mockResolvedValue(true);
50
+ // Mock fs.remove
51
+ fs.remove.mockResolvedValue(undefined);
52
+ // Mock os.tmpdir
53
+ os.tmpdir.mockReturnValue('/tmp');
54
+ });
55
+ afterEach(() => {
56
+ vi.restoreAllMocks();
57
+ });
58
+ describe('when projects are identical', () => {
59
+ it('should return true for identical projects', async () => {
60
+ // Mock downloadProject
61
+ downloadProject.mockResolvedValue({
62
+ data: mockZippedProject,
63
+ });
64
+ // Mock extractZipArchive
65
+ extractZipArchive.mockResolvedValue(undefined);
66
+ // Mock translate to return identical nodes
67
+ translate.mockResolvedValue({
68
+ intermediateNodesIndexedByUid: mockLocalProjectNodes,
69
+ });
70
+ // Mock isDeepEqual to return true for identical projects
71
+ isDeepEqual.mockReturnValue(true);
72
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
73
+ expect(result).toBe(true);
74
+ expect(isDeepEqual).toHaveBeenCalledWith(mockLocalProjectNodes, mockLocalProjectNodes, ['localDev']);
75
+ expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
76
+ });
77
+ });
78
+ describe('when projects are different', () => {
79
+ it('should return false for different projects', async () => {
80
+ // Mock downloadProject
81
+ downloadProject.mockResolvedValue({
82
+ data: mockZippedProject,
83
+ });
84
+ // Mock extractZipArchive
85
+ extractZipArchive.mockResolvedValue(undefined);
86
+ // Mock translate to return different nodes
87
+ const differentDeployedNodes = {};
88
+ translate.mockResolvedValue({
89
+ intermediateNodesIndexedByUid: differentDeployedNodes,
90
+ });
91
+ // Mock isDeepEqual to return false for different projects
92
+ isDeepEqual.mockReturnValue(false);
93
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
94
+ expect(result).toBe(false);
95
+ expect(isDeepEqual).toHaveBeenCalledWith(mockLocalProjectNodes, differentDeployedNodes, ['localDev']);
96
+ });
97
+ });
98
+ describe('error handling', () => {
99
+ it('should clean up temp directory even when errors occur', async () => {
100
+ // Mock downloadProject to throw an error after temp dir is created
101
+ downloadProject.mockRejectedValue(new Error('Download Error'));
102
+ await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Download Error');
103
+ expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
104
+ });
105
+ it('should handle translateForLocalDev errors', async () => {
106
+ // Mock downloadProject
107
+ downloadProject.mockResolvedValue({
108
+ data: mockZippedProject,
109
+ });
110
+ // Mock extractZipArchive
111
+ extractZipArchive.mockResolvedValue(undefined);
112
+ // Mock translate to throw an error
113
+ translate.mockRejectedValue(new Error('Translation Error'));
114
+ await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Translation Error');
115
+ expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
116
+ });
117
+ });
118
+ });
@@ -6,7 +6,7 @@ import path from 'path';
6
6
  import fs from 'fs';
7
7
  import { projectAddPromptV3 } from '../../prompts/projectAddPrompt.js';
8
8
  import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
9
- import { handleComponentCollision } from '../components.js';
9
+ import { updateHsMetaFilesWithAutoGeneratedFields, handleComponentCollision, } from '../components.js';
10
10
  import { getProjectMetadata, } from '@hubspot/project-parsing-lib/src/lib/project.js';
11
11
  import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
12
12
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
@@ -20,10 +20,10 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
20
20
  throw new Error(commands.project.add.error.failedToFetchComponentList);
21
21
  }
22
22
  const projectSrcDirectory = path.join(projectDir, projectConfig.srcDir);
23
- const projectMetadata = await getProjectMetadata(projectSrcDirectory);
23
+ const currentProjectMetadata = await getProjectMetadata(projectSrcDirectory);
24
24
  let derivedAuthType;
25
25
  let derivedDistribution;
26
- const appsMetadata = projectMetadata.components[AppKey];
26
+ const appsMetadata = currentProjectMetadata.components[AppKey];
27
27
  const shouldCreateApp = appsMetadata.count === 0;
28
28
  if (shouldCreateApp) {
29
29
  const { authType, distribution } = await createV3App(args.auth, args.distribution);
@@ -45,7 +45,7 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
45
45
  derivedDistribution = apps[0].config?.distribution;
46
46
  derivedAuthType = apps[0].config?.auth?.type;
47
47
  }
48
- const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, projectMetadata);
48
+ const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
49
49
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
50
50
  try {
51
51
  const components = projectAddPromptResponse.componentTemplate?.map((componentTemplate) => {
@@ -71,6 +71,18 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
71
71
  branch: DEFAULT_PROJECT_TEMPLATE_BRANCH,
72
72
  handleCollision: handleComponentCollision,
73
73
  });
74
+ const updatedProjectMetadata = await getProjectMetadata(projectSrcDirectory);
75
+ const newHsMetaFiles = updatedProjectMetadata.hsMetaFiles.filter(hsMetaFile => !currentProjectMetadata.hsMetaFiles.includes(hsMetaFile));
76
+ const existingUids = currentProjectMetadata.hsMetaFiles.map(hsMetaFile => {
77
+ try {
78
+ const { uid } = JSON.parse(fs.readFileSync(hsMetaFile, 'utf8'));
79
+ return uid;
80
+ }
81
+ catch (err) {
82
+ return '';
83
+ }
84
+ });
85
+ updateHsMetaFilesWithAutoGeneratedFields(projectConfig.name, newHsMetaFiles, existingUids);
74
86
  uiLogger.success(commands.project.add.success(projectAddPromptResponse.componentTemplate
75
87
  .map(template => `'${template.label}'`)
76
88
  .join(', '), projectAddPromptResponse.componentTemplate.length > 1));
@@ -1,2 +1,3 @@
1
1
  import { Collision } from '@hubspot/local-dev-lib/types/Archive';
2
2
  export declare function handleComponentCollision({ dest, src, collisions }: Collision): void;
3
+ export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[]): void;
@@ -1,6 +1,9 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { metafileExtension } from '@hubspot/project-parsing-lib';
3
+ import { coerceToValidUid, metafileExtension, } from '@hubspot/project-parsing-lib';
4
+ import { uiLogger } from '../ui/logger.js';
5
+ import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
6
+ import { lib } from '../../lang/en.js';
4
7
  // Handles a collision between component source files
5
8
  export function handleComponentCollision({ dest, src, collisions }) {
6
9
  const hsMetaFiles = [];
@@ -74,3 +77,26 @@ function handlePackageJsonCollisions(dest, src, packageJsonFiles) {
74
77
  fs.writeFileSync(path.join(dest, file), JSON.stringify(existingPackageJsonContents, null, 2));
75
78
  });
76
79
  }
80
+ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids = []) {
81
+ uiLogger.log('');
82
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.header);
83
+ for (const hsMetaFile of hsMetaFilePaths) {
84
+ const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
85
+ let uid = coerceToValidUid(`${component.type}-${projectName}`) || component.uid;
86
+ if (existingUids.includes(uid)) {
87
+ uid =
88
+ coerceToValidUid(`${component.type}-${Date.now()}-${projectName}`) ||
89
+ component.uid;
90
+ }
91
+ component.uid = uid;
92
+ if (component.type === AppKey && component.config) {
93
+ component.config.name = `${projectName}-Application`;
94
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
95
+ }
96
+ else {
97
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
98
+ }
99
+ fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
100
+ }
101
+ uiLogger.log('');
102
+ }
@@ -14,6 +14,7 @@ import { lib } from '../../../lang/en.js';
14
14
  import { uiLogger } from '../../ui/logger.js';
15
15
  import { getOauthAppInstallUrl, getStaticAuthAppInstallUrl, } from '../../app/urls.js';
16
16
  import { isDeveloperTestAccount, isSandbox } from '../../accountTypes.js';
17
+ import SpinniesManager from '../../ui/SpinniesManager.js';
17
18
  class AppDevModeInterface {
18
19
  localDevState;
19
20
  localDevLogger;
@@ -86,7 +87,21 @@ class AppDevModeInterface {
86
87
  });
87
88
  }
88
89
  async fetchAppData() {
89
- const { data: { results: portalApps }, } = await fetchPublicAppsForPortal(this.localDevState.targetProjectAccountId);
90
+ SpinniesManager.add('fetchAppData', {
91
+ text: lib.AppDevModeInterface.fetchAppData.checking(this.appNode?.config.name || ''),
92
+ });
93
+ let portalApps = [];
94
+ try {
95
+ const { data: { results }, } = await fetchPublicAppsForPortal(this.localDevState.targetProjectAccountId);
96
+ portalApps = results;
97
+ }
98
+ catch (e) {
99
+ SpinniesManager.fail('fetchAppData', {
100
+ text: lib.AppDevModeInterface.fetchAppData.error,
101
+ });
102
+ logError(e);
103
+ process.exit(EXIT_CODES.ERROR);
104
+ }
90
105
  const appData = portalApps.find(({ sourceId }) => sourceId === this.appNode?.uid);
91
106
  if (!appData) {
92
107
  return;
@@ -105,15 +120,18 @@ class AppDevModeInterface {
105
120
  if (!this.appData || !this.marketplaceAppInstalls) {
106
121
  return;
107
122
  }
123
+ SpinniesManager.fail('fetchAppData', {
124
+ text: lib.AppDevModeInterface.fetchAppData.activeInstallations(this.appNode?.config.name || '', this.marketplaceAppInstalls),
125
+ failColor: 'yellow',
126
+ });
108
127
  uiLine();
109
- uiLogger.warn(lib.LocalDevManager.activeInstallWarning.installCount(this.appData.name, this.marketplaceAppInstalls));
110
128
  uiLogger.log(lib.LocalDevManager.activeInstallWarning.explanation);
111
129
  uiLine();
112
130
  const proceed = await confirmPrompt(lib.LocalDevManager.activeInstallWarning.confirmationPrompt, { defaultAnswer: false });
113
131
  if (!proceed) {
114
132
  process.exit(EXIT_CODES.SUCCESS);
115
133
  }
116
- this.localDevLogger.addUploadWarning(lib.AppDevModeInterface.defaultMarketplaceAppWarning(this.marketplaceAppInstalls));
134
+ this.localDevState.addUploadWarning(lib.AppDevModeInterface.defaultMarketplaceAppWarning(this.marketplaceAppInstalls));
117
135
  }
118
136
  async autoInstallStaticAuthApp() {
119
137
  const shouldInstall = await installAppAutoPrompt();
@@ -177,8 +195,22 @@ class AppDevModeInterface {
177
195
  }
178
196
  const { needsInstall, isReinstall } = await this.checkTestAccountAppInstallation();
179
197
  if (needsInstall) {
198
+ if (SpinniesManager.pick('fetchAppData')) {
199
+ SpinniesManager.fail('fetchAppData', {
200
+ text: lib.AppDevModeInterface.fetchAppData.notInstalled(this.appNode.config.name, this.localDevState.targetTestingAccountId),
201
+ failColor: 'white',
202
+ });
203
+ }
180
204
  await this.installAppOrOpenInstallUrl(isReinstall || false);
181
205
  }
206
+ else {
207
+ if (SpinniesManager.pick('fetchAppData')) {
208
+ SpinniesManager.succeed('fetchAppData', {
209
+ text: lib.AppDevModeInterface.fetchAppData.success(this.appNode.config.name, this.localDevState.targetTestingAccountId),
210
+ });
211
+ }
212
+ uiLogger.log('');
213
+ }
182
214
  }
183
215
  catch (e) {
184
216
  logError(e);
@@ -2,20 +2,16 @@ import LocalDevState from './LocalDevState.js';
2
2
  declare class LocalDevLogger {
3
3
  private state;
4
4
  private mostRecentUploadWarning;
5
- private uploadWarnings;
6
5
  constructor(state: LocalDevState);
7
6
  private logUploadInstructions;
8
7
  private handleError;
9
8
  getUploadCommand(): string;
10
9
  uploadWarning(): void;
11
- addUploadWarning(warning: string): void;
12
- clearUploadWarnings(): void;
13
10
  missingComponentsWarning(components: string[]): void;
14
11
  fileChangeError(e: unknown): void;
15
12
  devServerSetupError(e: unknown): void;
16
13
  devServerStartError(e: unknown): void;
17
14
  devServerCleanupError(e: unknown): void;
18
- noDeployedBuild(): void;
19
15
  resetSpinnies(): void;
20
16
  startupMessage(): void;
21
17
  cleanupStart(): void;
@@ -11,11 +11,9 @@ import { CONFIG_LOCAL_STATE_FLAGS } from '../../constants.js';
11
11
  class LocalDevLogger {
12
12
  state;
13
13
  mostRecentUploadWarning;
14
- uploadWarnings;
15
14
  constructor(state) {
16
15
  this.state = state;
17
16
  this.mostRecentUploadWarning = null;
18
- this.uploadWarnings = new Set();
19
17
  }
20
18
  logUploadInstructions(warning) {
21
19
  uiLogger.log('');
@@ -23,12 +21,7 @@ class LocalDevLogger {
23
21
  uiLogger.log('');
24
22
  uiLogger.log(lib.LocalDevManager.uploadWarning.instructionsHeader);
25
23
  uiLogger.log(lib.LocalDevManager.uploadWarning.stopDev);
26
- if (this.state.isGithubLinked) {
27
- uiLogger.log(lib.LocalDevManager.uploadWarning.pushToGithub);
28
- }
29
- else {
30
- uiLogger.log(lib.LocalDevManager.uploadWarning.runUpload(this.getUploadCommand()));
31
- }
24
+ uiLogger.log(lib.LocalDevManager.uploadWarning.runUpload(this.getUploadCommand()));
32
25
  uiLogger.log(lib.LocalDevManager.uploadWarning.restartDev);
33
26
  }
34
27
  handleError(e, langFunction) {
@@ -47,7 +40,7 @@ class LocalDevLogger {
47
40
  uploadWarning() {
48
41
  // At the moment, there is only one additional warning. We may need to do this in a
49
42
  // more robust way in the future
50
- const additionalWarnings = Array.from(this.uploadWarnings).join('\n\n');
43
+ const additionalWarnings = Array.from(this.state.uploadWarnings).join('\n\n');
51
44
  const warning = `${lib.LocalDevManager.uploadWarning.defaultWarning} ${additionalWarnings}`;
52
45
  // Avoid logging the warning to the console if it is currently the most
53
46
  // recently logged warning. We do not want to spam the console with the same message.
@@ -56,12 +49,6 @@ class LocalDevLogger {
56
49
  this.mostRecentUploadWarning = warning;
57
50
  }
58
51
  }
59
- addUploadWarning(warning) {
60
- this.uploadWarnings.add(warning);
61
- }
62
- clearUploadWarnings() {
63
- this.uploadWarnings.clear();
64
- }
65
52
  missingComponentsWarning(components) {
66
53
  const warning = lib.LocalDevManager.uploadWarning.missingComponents(components.join(', '));
67
54
  if (warning !== this.mostRecentUploadWarning) {
@@ -81,10 +68,6 @@ class LocalDevLogger {
81
68
  devServerCleanupError(e) {
82
69
  this.handleError(e, lib.LocalDevManager.devServer.cleanupError);
83
70
  }
84
- noDeployedBuild() {
85
- uiLogger.error(lib.LocalDevManager.noDeployedBuild(this.state.projectConfig.name, uiAccountDescription(this.state.targetProjectAccountId), this.getUploadCommand()));
86
- uiLogger.log('');
87
- }
88
71
  resetSpinnies() {
89
72
  SpinniesManager.stopAll();
90
73
  SpinniesManager.init();
@@ -9,7 +9,7 @@ import { PROJECT_CONFIG_FILE } from '../../constants.js';
9
9
  import SpinniesManager from '../../ui/SpinniesManager.js';
10
10
  import DevServerManager from './DevServerManager.js';
11
11
  import { EXIT_CODES } from '../../enums/exitCodes.js';
12
- import { getAccountHomeUrl } from './helpers.js';
12
+ import { getAccountHomeUrl } from '../urls.js';
13
13
  import { componentIsApp, componentIsPublicApp, CONFIG_FILES, getAppCardConfigs, getComponentUid, } from '../../projects/structure.js';
14
14
  import { ComponentTypes, } from '../../../types/Projects.js';
15
15
  import { UI_COLORS, uiCommandReference, uiAccountDescription, uiBetaTag, uiLink, uiLine, } from '../../ui/index.js';
@@ -19,7 +19,6 @@ declare class LocalDevProcess {
19
19
  private setupDevServers;
20
20
  private startDevServers;
21
21
  private cleanupDevServers;
22
- private compareLocalProjectToDeployed;
23
22
  private projectConfigValidForUpload;
24
23
  private getIntermediateRepresentation;
25
24
  private updateProjectNodes;
@@ -30,7 +29,7 @@ declare class LocalDevProcess {
30
29
  start(): Promise<void>;
31
30
  stop(showProgress?: boolean): Promise<void>;
32
31
  uploadProject(): Promise<boolean>;
33
- addStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>, callOnInit?: boolean): void;
32
+ addStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
34
33
  sendDevServerMessage(message: LocalDevServerMessage): void;
35
34
  removeStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
36
35
  }
@@ -6,7 +6,6 @@ import LocalDevState from './LocalDevState.js';
6
6
  import LocalDevLogger from './LocalDevLogger.js';
7
7
  import DevServerManagerV2 from './DevServerManagerV2.js';
8
8
  import { EXIT_CODES } from '../../enums/exitCodes.js';
9
- import { mapToUserFriendlyName } from '@hubspot/project-parsing-lib/src/lib/transform.js';
10
9
  import { getProjectConfig } from '../config.js';
11
10
  import { handleProjectUpload } from '../upload.js';
12
11
  import { pollProjectBuildAndDeploy } from '../buildAndDeploy.js';
@@ -75,20 +74,6 @@ class LocalDevProcess {
75
74
  return false;
76
75
  }
77
76
  }
78
- compareLocalProjectToDeployed() {
79
- const deployedComponentNames = this.state.deployedBuild.subbuildStatuses.map(subbuildStatus => subbuildStatus.buildName);
80
- const missingProjectNodes = [];
81
- Object.values(this.projectNodes).forEach(node => {
82
- if (!deployedComponentNames.includes(node.uid)) {
83
- const userFriendlyName = mapToUserFriendlyName(node.componentType);
84
- const label = userFriendlyName ? `[${userFriendlyName}] ` : '';
85
- missingProjectNodes.push(`${label}${node.uid}`);
86
- }
87
- });
88
- if (missingProjectNodes.length) {
89
- this.logger.missingComponentsWarning(missingProjectNodes);
90
- }
91
- }
92
77
  async projectConfigValidForUpload() {
93
78
  const { projectConfig } = await getProjectConfig();
94
79
  if (!projectConfig) {
@@ -143,11 +128,6 @@ class LocalDevProcess {
143
128
  }
144
129
  async start() {
145
130
  this.logger.resetSpinnies();
146
- // Local dev currently relies on the existence of a deployed build in the target account
147
- if (!this.state.deployedBuild) {
148
- this.logger.noDeployedBuild();
149
- process.exit(EXIT_CODES.SUCCESS);
150
- }
151
131
  const setupSucceeded = await this.setupDevServers();
152
132
  if (!setupSucceeded) {
153
133
  process.exit(EXIT_CODES.ERROR);
@@ -158,9 +138,6 @@ class LocalDevProcess {
158
138
  }
159
139
  await this.startDevServers();
160
140
  this.logger.monitorConsoleOutput();
161
- // Verify that there are no mismatches between components in the local project
162
- // and components in the deployed build of the project.
163
- this.compareLocalProjectToDeployed();
164
141
  }
165
142
  async stop(showProgress = true) {
166
143
  if (showProgress) {
@@ -199,11 +176,11 @@ class LocalDevProcess {
199
176
  }
200
177
  await this.updateProjectNodesAfterUpload();
201
178
  this.logger.uploadSuccess();
202
- this.logger.clearUploadWarnings();
179
+ this.state.clearUploadWarnings();
203
180
  return true;
204
181
  }
205
- addStateListener(key, listener, callOnInit = false) {
206
- this.state.addListener(key, listener, callOnInit);
182
+ addStateListener(key, listener) {
183
+ this.state.addListener(key, listener);
207
184
  }
208
185
  sendDevServerMessage(message) {
209
186
  this.state.devServerMessage = message;