@hubspot/cli 8.0.2-experimental.0 → 8.0.3-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 (69) hide show
  1. package/commands/__tests__/getStarted.test.js +2 -2
  2. package/commands/__tests__/project.test.js +30 -0
  3. package/commands/account/auth.js +8 -97
  4. package/commands/account/use.js +19 -4
  5. package/commands/cms/module/marketplace-validate.js +23 -5
  6. package/commands/cms/theme/marketplace-validate.js +25 -6
  7. package/commands/mcp/setup.js +1 -2
  8. package/commands/mcp.js +1 -2
  9. package/commands/project.js +22 -1
  10. package/lang/en.d.ts +22 -1
  11. package/lang/en.js +26 -5
  12. package/lib/__tests__/accountAuth.test.d.ts +1 -0
  13. package/lib/__tests__/accountAuth.test.js +258 -0
  14. package/lib/accountAuth.d.ts +10 -0
  15. package/lib/accountAuth.js +105 -0
  16. package/lib/app/urls.d.ts +1 -0
  17. package/lib/app/urls.js +4 -0
  18. package/lib/errors/ProjectErrors.d.ts +15 -0
  19. package/lib/errors/ProjectErrors.js +30 -0
  20. package/lib/getStarted/getStartedV2.js +3 -41
  21. package/lib/getStartedV2Actions.d.ts +29 -0
  22. package/lib/getStartedV2Actions.js +104 -9
  23. package/lib/marketplaceValidate.d.ts +1 -1
  24. package/lib/marketplaceValidate.js +23 -41
  25. package/lib/projects/ProjectLogsManager.d.ts +12 -3
  26. package/lib/projects/ProjectLogsManager.js +70 -12
  27. package/lib/projects/__tests__/ProjectLogsManager.test.js +131 -18
  28. package/lib/projects/__tests__/platformVersion.test.js +37 -1
  29. package/lib/projects/__tests__/projects.test.js +6 -2
  30. package/lib/projects/components.d.ts +6 -0
  31. package/lib/projects/components.js +1 -1
  32. package/lib/projects/config.js +9 -2
  33. package/lib/projects/localDev/helpers/project.d.ts +4 -1
  34. package/lib/projects/localDev/helpers/project.js +13 -8
  35. package/lib/projects/platformVersion.d.ts +8 -0
  36. package/lib/projects/platformVersion.js +31 -2
  37. package/lib/prompts/accountsPrompt.d.ts +2 -1
  38. package/lib/prompts/accountsPrompt.js +10 -2
  39. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
  40. package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -10
  41. package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
  42. package/mcp-server/tools/project/CreateProjectTool.js +5 -10
  43. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
  44. package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
  45. package/mcp-server/tools/project/GetBuildLogsTool.js +3 -4
  46. package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
  47. package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
  48. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
  49. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
  50. package/mcp-server/tools/project/constants.d.ts +12 -1
  51. package/mcp-server/tools/project/constants.js +12 -16
  52. package/package.json +2 -2
  53. package/ui/components/ActionSection.d.ts +1 -1
  54. package/ui/components/BoxWithTitle.js +1 -1
  55. package/ui/components/getStarted/GetStartedFlow.d.ts +8 -0
  56. package/ui/components/getStarted/GetStartedFlow.js +136 -0
  57. package/ui/components/getStarted/reducer.d.ts +59 -0
  58. package/ui/components/getStarted/reducer.js +72 -0
  59. package/ui/components/getStarted/screens/ProjectSetupScreen.d.ts +16 -0
  60. package/ui/components/getStarted/screens/ProjectSetupScreen.js +39 -0
  61. package/ui/components/getStarted/screens/UploadScreen.d.ts +7 -0
  62. package/ui/components/getStarted/screens/UploadScreen.js +43 -0
  63. package/ui/components/getStarted/selectors.d.ts +2 -0
  64. package/ui/components/getStarted/selectors.js +1 -0
  65. package/ui/lib/constants.d.ts +16 -0
  66. package/ui/lib/constants.js +16 -0
  67. package/ui/render.js +6 -0
  68. package/ui/components/GetStartedFlow.d.ts +0 -24
  69. package/ui/components/GetStartedFlow.js +0 -128
@@ -2,7 +2,7 @@ import { GetValidationResultsResponse } from '@hubspot/local-dev-lib/types/Marke
2
2
  export declare function kickOffValidation(accountId: number, assetType: string, src: string): Promise<number>;
3
3
  export declare function pollForValidationFinish(accountId: number, validationId: number): Promise<void>;
4
4
  export declare function fetchValidationResults(accountId: number, validationId: number): Promise<GetValidationResultsResponse>;
5
- export declare function processValidationErrors(invalidPathError: (path: string) => string, validationResults: GetValidationResultsResponse): void;
5
+ export declare function hasProcessValidationErrors(invalidPathError: (path: string) => string, validationResults: GetValidationResultsResponse): boolean;
6
6
  type ResultsCopy = {
7
7
  noErrors: string;
8
8
  required: string;
@@ -1,54 +1,35 @@
1
1
  import chalk from 'chalk';
2
2
  import { requestValidation, getValidationStatus, getValidationResults, } from '@hubspot/local-dev-lib/api/marketplaceValidation';
3
3
  import { uiLogger } from './ui/logger.js';
4
- import { EXIT_CODES } from './enums/exitCodes.js';
5
4
  const SLEEP_TIME = 2000;
6
5
  export async function kickOffValidation(accountId, assetType, src) {
7
6
  const requestGroup = 'EXTERNAL_DEVELOPER';
8
- try {
9
- const { data: requestResult } = await requestValidation(accountId, {
10
- path: src,
11
- assetType,
12
- requestGroup,
13
- });
14
- return requestResult;
15
- }
16
- catch (err) {
17
- uiLogger.debug(err);
18
- process.exit(EXIT_CODES.ERROR);
19
- }
7
+ const { data: requestResult } = await requestValidation(accountId, {
8
+ path: src,
9
+ assetType,
10
+ requestGroup,
11
+ });
12
+ return requestResult;
20
13
  }
21
14
  export async function pollForValidationFinish(accountId, validationId) {
22
- try {
23
- const checkValidationStatus = async () => {
24
- const { data: validationStatus } = await getValidationStatus(accountId, {
25
- validationId,
26
- });
27
- if (validationStatus === 'REQUESTED') {
28
- await new Promise(resolve => setTimeout(resolve, SLEEP_TIME));
29
- await checkValidationStatus();
30
- }
31
- };
32
- await checkValidationStatus();
33
- }
34
- catch (err) {
35
- uiLogger.debug(err);
36
- process.exit(EXIT_CODES.ERROR);
37
- }
38
- }
39
- export async function fetchValidationResults(accountId, validationId) {
40
- try {
41
- const { data: validationResults } = await getValidationResults(accountId, {
15
+ async function checkValidationStatus() {
16
+ const { data: validationStatus } = await getValidationStatus(accountId, {
42
17
  validationId,
43
18
  });
44
- return validationResults;
45
- }
46
- catch (err) {
47
- uiLogger.debug(err);
48
- process.exit(EXIT_CODES.ERROR);
19
+ if (validationStatus === 'REQUESTED') {
20
+ await new Promise(resolve => setTimeout(resolve, SLEEP_TIME));
21
+ await checkValidationStatus();
22
+ }
49
23
  }
24
+ await checkValidationStatus();
25
+ }
26
+ export async function fetchValidationResults(accountId, validationId) {
27
+ const { data: validationResults } = await getValidationResults(accountId, {
28
+ validationId,
29
+ });
30
+ return validationResults;
50
31
  }
51
- export function processValidationErrors(invalidPathError, validationResults) {
32
+ export function hasProcessValidationErrors(invalidPathError, validationResults) {
52
33
  if (validationResults.errors.length) {
53
34
  const { assetPath, errors } = validationResults;
54
35
  errors.forEach(err => {
@@ -56,11 +37,12 @@ export function processValidationErrors(invalidPathError, validationResults) {
56
37
  uiLogger.error(invalidPathError(assetPath));
57
38
  }
58
39
  else {
59
- uiLogger.error(`${err.context}`);
40
+ uiLogger.error(err.context);
60
41
  }
61
42
  });
62
- process.exit(EXIT_CODES.ERROR);
43
+ return true;
63
44
  }
45
+ return false;
64
46
  }
65
47
  function displayFileInfo(file, line, resultsCopy) {
66
48
  if (file) {
@@ -1,10 +1,18 @@
1
- import { AppFunctionComponentMetadata } from '@hubspot/local-dev-lib/types/ComponentStructure';
1
+ import { ProjectConfig } from '../../types/Projects.js';
2
+ type FunctionInfo = {
3
+ componentName: string;
4
+ appId: number;
5
+ endpoint?: {
6
+ path: string;
7
+ };
8
+ };
2
9
  declare class _ProjectLogsManager {
3
10
  projectName: string | undefined;
11
+ projectConfig: ProjectConfig | undefined;
4
12
  projectId: number | undefined;
5
13
  accountId: number | undefined;
6
- functions: AppFunctionComponentMetadata[];
7
- selectedFunction: AppFunctionComponentMetadata | undefined;
14
+ functions: FunctionInfo[];
15
+ selectedFunction: FunctionInfo | undefined;
8
16
  functionName: string | undefined;
9
17
  appId: number | undefined;
10
18
  isPublicFunction: boolean | undefined;
@@ -13,6 +21,7 @@ declare class _ProjectLogsManager {
13
21
  constructor();
14
22
  init(accountId: number): Promise<void>;
15
23
  fetchFunctionDetails(): Promise<void>;
24
+ fetchFunctionDetailsV2(deployedBuildId: number): Promise<void>;
16
25
  getFunctionNames(): string[];
17
26
  setFunction(functionName?: string): void;
18
27
  }
@@ -1,10 +1,15 @@
1
1
  import { getProjectConfig } from './config.js';
2
2
  import { ensureProjectExists } from './ensureProjectExists.js';
3
3
  import { fetchProjectComponentsMetadata } from '@hubspot/local-dev-lib/api/projects';
4
+ import { fetchAppMetadataBySourceId } from '@hubspot/local-dev-lib/api/appsDev';
4
5
  import { uiLogger } from '../ui/logger.js';
5
6
  import { commands } from '../../lang/en.js';
7
+ import { isV2Project } from './platformVersion.js';
8
+ import { getDeployedProjectNodes } from './localDev/helpers/project.js';
9
+ import { debugError } from '../errorHandlers/index.js';
6
10
  class _ProjectLogsManager {
7
11
  projectName;
12
+ projectConfig;
8
13
  projectId;
9
14
  accountId;
10
15
  functions;
@@ -15,6 +20,7 @@ class _ProjectLogsManager {
15
20
  endpointName;
16
21
  reset() {
17
22
  this.projectName = undefined;
23
+ this.projectConfig = undefined;
18
24
  this.projectId = undefined;
19
25
  this.accountId = undefined;
20
26
  this.functions = [];
@@ -32,8 +38,8 @@ class _ProjectLogsManager {
32
38
  if (!projectConfig || !projectConfig.name) {
33
39
  throw new Error(commands.project.logs.errors.noProjectConfig);
34
40
  }
35
- const { name: projectName } = projectConfig;
36
- this.projectName = projectName;
41
+ this.projectConfig = projectConfig;
42
+ this.projectName = projectConfig.name;
37
43
  this.accountId = accountId;
38
44
  this.functions = [];
39
45
  const { project } = await ensureProjectExists(this.accountId, this.projectName, {
@@ -45,7 +51,16 @@ class _ProjectLogsManager {
45
51
  throw new Error(commands.project.logs.errors.failedToFetchProjectDetails);
46
52
  }
47
53
  this.projectId = project.id;
48
- await this.fetchFunctionDetails();
54
+ if (isV2Project(projectConfig.platformVersion)) {
55
+ const deployedBuildId = project.deployedBuild.buildId;
56
+ if (!deployedBuildId) {
57
+ throw new Error(commands.project.logs.errors.noDeployedBuild);
58
+ }
59
+ await this.fetchFunctionDetailsV2(deployedBuildId);
60
+ }
61
+ else {
62
+ await this.fetchFunctionDetails();
63
+ }
49
64
  }
50
65
  async fetchFunctionDetails() {
51
66
  if (!this.projectId) {
@@ -61,14 +76,60 @@ class _ProjectLogsManager {
61
76
  return type && type.name === 'PRIVATE_APP';
62
77
  });
63
78
  apps.forEach(app => {
64
- this.functions.push(
65
- // If component type is APP_FUNCTION, we can safely cast as AppFunctionComponentMetadata
66
- ...app.featureComponents.filter(component => component.type.name === 'APP_FUNCTION'));
79
+ const appFunctions = app.featureComponents.filter(component => component.type.name === 'APP_FUNCTION');
80
+ appFunctions.forEach(fn => {
81
+ if (fn.deployOutput) {
82
+ this.functions.push({
83
+ componentName: fn.componentName,
84
+ appId: fn.deployOutput.appId,
85
+ endpoint: fn.deployOutput.endpoint,
86
+ });
87
+ }
88
+ });
67
89
  });
68
90
  if (this.functions.length === 0) {
69
91
  throw new Error(commands.project.logs.errors.noFunctionsInProject);
70
92
  }
71
93
  }
94
+ async fetchFunctionDetailsV2(deployedBuildId) {
95
+ if (!this.projectId || !this.accountId || !this.projectConfig) {
96
+ uiLogger.debug(commands.project.logs.errors.projectLogsManagerNotInitialized);
97
+ throw new Error(commands.project.logs.errors.generic);
98
+ }
99
+ let deployedNodes;
100
+ try {
101
+ deployedNodes = await getDeployedProjectNodes(this.projectConfig, this.accountId, deployedBuildId);
102
+ }
103
+ catch (err) {
104
+ debugError(err);
105
+ throw new Error(commands.project.logs.errors.failedToFetchProjectDetails);
106
+ }
107
+ const appNode = Object.values(deployedNodes).find(node => node.componentType === 'APPLICATION');
108
+ if (!appNode) {
109
+ throw new Error(commands.project.logs.errors.noFunctionsInProject);
110
+ }
111
+ let appId;
112
+ try {
113
+ const { data: appMetadata } = await fetchAppMetadataBySourceId(this.projectId, appNode.uid, this.accountId);
114
+ appId = appMetadata.id;
115
+ }
116
+ catch (err) {
117
+ debugError(err);
118
+ throw new Error(commands.project.logs.errors.failedToFetchProjectDetails);
119
+ }
120
+ const functionNodes = Object.values(deployedNodes).filter(node => node.componentType === 'APP_FUNCTION');
121
+ for (const fnNode of functionNodes) {
122
+ const config = fnNode.config;
123
+ this.functions.push({
124
+ componentName: fnNode.uid,
125
+ appId,
126
+ endpoint: config.endpoint,
127
+ });
128
+ }
129
+ if (this.functions.length === 0) {
130
+ throw new Error(commands.project.logs.errors.noFunctionsInProject);
131
+ }
132
+ }
72
133
  getFunctionNames() {
73
134
  return this.functions.map(serverlessFunction => serverlessFunction.componentName);
74
135
  }
@@ -81,12 +142,9 @@ class _ProjectLogsManager {
81
142
  throw new Error(commands.project.logs.errors.noFunctionWithName(functionName));
82
143
  }
83
144
  this.functionName = functionName;
84
- if (!this.selectedFunction.deployOutput) {
85
- throw new Error(commands.project.logs.errors.functionNotDeployed(functionName));
86
- }
87
- this.appId = this.selectedFunction.deployOutput.appId;
88
- if (this.selectedFunction.deployOutput.endpoint) {
89
- this.endpointName = this.selectedFunction.deployOutput.endpoint.path;
145
+ this.appId = this.selectedFunction.appId;
146
+ if (this.selectedFunction.endpoint) {
147
+ this.endpointName = this.selectedFunction.endpoint.path;
90
148
  this.isPublicFunction = true;
91
149
  }
92
150
  else {
@@ -2,6 +2,9 @@ import { ProjectLogsManager } from '../ProjectLogsManager.js';
2
2
  import { getProjectConfig } from '../config.js';
3
3
  import { ensureProjectExists } from '../ensureProjectExists.js';
4
4
  import { fetchProjectComponentsMetadata } from '@hubspot/local-dev-lib/api/projects';
5
+ import { fetchAppMetadataBySourceId } from '@hubspot/local-dev-lib/api/appsDev';
6
+ import { getDeployedProjectNodes } from '../localDev/helpers/project.js';
7
+ import { isV2Project } from '../platformVersion.js';
5
8
  const SUBCOMPONENT_TYPES = {
6
9
  APP_ID: 'APP_ID',
7
10
  PACKAGE_LOCK_FILE: 'PACKAGE_LOCK_FILE',
@@ -17,11 +20,20 @@ const SUBCOMPONENT_TYPES = {
17
20
  vi.mock('../../projects/config');
18
21
  vi.mock('../../projects/ensureProjectExists');
19
22
  vi.mock('@hubspot/local-dev-lib/api/projects');
23
+ vi.mock('@hubspot/local-dev-lib/api/appsDev');
24
+ vi.mock('../../projects/localDev/helpers/project');
25
+ vi.mock('../../projects/platformVersion');
20
26
  describe('lib/projects/ProjectLogsManager', () => {
21
27
  const accountId = 12345678;
22
28
  const appId = 999999;
23
29
  const projectName = 'super cool test project';
24
- const projectConfig = { projectConfig: { name: projectName } };
30
+ const projectConfig = {
31
+ projectConfig: {
32
+ name: projectName,
33
+ srcDir: 'src',
34
+ platformVersion: '2024.1',
35
+ },
36
+ };
25
37
  const projectId = 987654321;
26
38
  const projectDetails = {
27
39
  project: {
@@ -33,16 +45,26 @@ describe('lib/projects/ProjectLogsManager', () => {
33
45
  };
34
46
  const function1 = {
35
47
  componentName: 'function1',
36
- type: {
37
- name: SUBCOMPONENT_TYPES.APP_FUNCTION,
38
- },
39
- deployOutput: {
40
- appId,
41
- appFunctionName: 'function1',
42
- },
48
+ appId,
43
49
  };
44
50
  const functions = [
45
51
  function1,
52
+ {
53
+ componentName: 'function2',
54
+ appId,
55
+ },
56
+ ];
57
+ const legacyApiFunctions = [
58
+ {
59
+ componentName: 'function1',
60
+ type: {
61
+ name: SUBCOMPONENT_TYPES.APP_FUNCTION,
62
+ },
63
+ deployOutput: {
64
+ appId,
65
+ appFunctionName: 'function1',
66
+ },
67
+ },
46
68
  {
47
69
  componentName: 'function2',
48
70
  type: {
@@ -56,6 +78,7 @@ describe('lib/projects/ProjectLogsManager', () => {
56
78
  ];
57
79
  beforeEach(() => {
58
80
  ProjectLogsManager.reset();
81
+ isV2Project.mockReturnValue(false);
59
82
  getProjectConfig.mockResolvedValue(projectConfig);
60
83
  ensureProjectExists.mockResolvedValue(projectDetails);
61
84
  fetchProjectComponentsMetadata.mockResolvedValue({
@@ -69,7 +92,7 @@ describe('lib/projects/ProjectLogsManager', () => {
69
92
  appId,
70
93
  },
71
94
  featureComponents: [
72
- ...functions,
95
+ ...legacyApiFunctions,
73
96
  {
74
97
  type: {
75
98
  name: 'NOT_AN_APP_FUNCTION',
@@ -128,6 +151,102 @@ describe('lib/projects/ProjectLogsManager', () => {
128
151
  expect(ProjectLogsManager.functions).toEqual(functions);
129
152
  });
130
153
  });
154
+ describe('v2 project init', () => {
155
+ const v2ProjectConfig = {
156
+ projectConfig: {
157
+ name: projectName,
158
+ srcDir: 'src',
159
+ platformVersion: '2025.2',
160
+ },
161
+ };
162
+ const deployedBuildId = 555;
163
+ const v2ProjectDetails = {
164
+ project: {
165
+ id: projectId,
166
+ deployedBuild: {
167
+ buildId: deployedBuildId,
168
+ subbuildStatuses: {},
169
+ },
170
+ },
171
+ };
172
+ const appUid = 'my-app';
173
+ const fnUid1 = 'my-app/app.functions/function1';
174
+ const fnUid2 = 'my-app/app.functions/function2';
175
+ const deployedNodes = {
176
+ [appUid]: {
177
+ componentType: 'APPLICATION',
178
+ componentDeps: {},
179
+ metaFilePath: 'src/app/app-hsmeta.json',
180
+ uid: appUid,
181
+ config: {},
182
+ files: {},
183
+ },
184
+ [fnUid1]: {
185
+ componentType: 'APP_FUNCTION',
186
+ componentDeps: { app: appUid },
187
+ metaFilePath: 'src/app/app.functions/function1.functions/function-hsmeta.json',
188
+ uid: fnUid1,
189
+ config: { endpoint: { path: '/my-endpoint' } },
190
+ files: {},
191
+ },
192
+ [fnUid2]: {
193
+ componentType: 'APP_FUNCTION',
194
+ componentDeps: { app: appUid },
195
+ metaFilePath: 'src/app/app.functions/function2.functions/function-hsmeta.json',
196
+ uid: fnUid2,
197
+ config: {},
198
+ files: {},
199
+ },
200
+ };
201
+ beforeEach(() => {
202
+ getProjectConfig.mockResolvedValue(v2ProjectConfig);
203
+ ensureProjectExists.mockResolvedValue(v2ProjectDetails);
204
+ isV2Project.mockReturnValue(true);
205
+ getDeployedProjectNodes.mockResolvedValue(deployedNodes);
206
+ fetchAppMetadataBySourceId.mockResolvedValue({
207
+ data: { id: appId },
208
+ });
209
+ });
210
+ it('should populate functions correctly for v2 projects', async () => {
211
+ await ProjectLogsManager.init(accountId);
212
+ expect(getDeployedProjectNodes).toHaveBeenCalledWith(v2ProjectConfig.projectConfig, accountId, deployedBuildId);
213
+ expect(fetchAppMetadataBySourceId).toHaveBeenCalledWith(projectId, appUid, accountId);
214
+ expect(ProjectLogsManager.functions).toEqual([
215
+ {
216
+ componentName: fnUid1,
217
+ appId,
218
+ endpoint: { path: '/my-endpoint' },
219
+ },
220
+ {
221
+ componentName: fnUid2,
222
+ appId,
223
+ endpoint: undefined,
224
+ },
225
+ ]);
226
+ });
227
+ it('should throw noDeployedBuild when buildId is missing', async () => {
228
+ ensureProjectExists.mockResolvedValue({
229
+ project: {
230
+ id: projectId,
231
+ deployedBuild: {
232
+ buildId: undefined,
233
+ subbuildStatuses: {},
234
+ },
235
+ },
236
+ });
237
+ await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow('This project has not been deployed yet. Deploy the project first, then try again.');
238
+ });
239
+ it('should throw noFunctionsInProject when no function nodes exist', async () => {
240
+ getDeployedProjectNodes.mockResolvedValue({
241
+ [appUid]: deployedNodes[appUid],
242
+ });
243
+ await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow(/There aren't any functions in this project/);
244
+ });
245
+ it('should throw a user-friendly error when getDeployedProjectNodes fails', async () => {
246
+ getDeployedProjectNodes.mockRejectedValue(new Error('download failed'));
247
+ await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow(/There was an error fetching project details/);
248
+ });
249
+ });
131
250
  describe('getFunctionNames', () => {
132
251
  it('should return an empty array if functions is empty', async () => {
133
252
  ProjectLogsManager.functions = [];
@@ -154,14 +273,8 @@ describe('lib/projects/ProjectLogsManager', () => {
154
273
  it('should set the data correctly for public functions', async () => {
155
274
  const functionToChoose = {
156
275
  componentName: 'function1',
157
- type: {
158
- name: SUBCOMPONENT_TYPES.APP_FUNCTION,
159
- },
160
- deployOutput: {
161
- appId: 123,
162
- appFunctionName: 'function1',
163
- endpoint: { path: 'yooooooo', methods: ['GET'] },
164
- },
276
+ appId: 123,
277
+ endpoint: { path: 'yooooooo' },
165
278
  };
166
279
  ProjectLogsManager.functions = [functionToChoose];
167
280
  ProjectLogsManager.setFunction('function1');
@@ -170,7 +283,7 @@ describe('lib/projects/ProjectLogsManager', () => {
170
283
  expect(ProjectLogsManager.selectedFunction).toEqual(functionToChoose);
171
284
  expect(ProjectLogsManager.isPublicFunction).toEqual(true);
172
285
  });
173
- it('should set the data correctly for public functions', async () => {
286
+ it('should set the data correctly for private functions', async () => {
174
287
  ProjectLogsManager.functions = functions;
175
288
  ProjectLogsManager.setFunction('function1');
176
289
  expect(ProjectLogsManager.selectedFunction).toEqual(function1);
@@ -1,4 +1,4 @@
1
- import { isV2Project } from '../platformVersion.js';
1
+ import { isV2Project, isUnsupportedPlatformVersion, } from '../platformVersion.js';
2
2
  describe('platformVersion', () => {
3
3
  describe('isV2Project', () => {
4
4
  it('returns true if platform version is UNSTABLE', () => {
@@ -24,4 +24,40 @@ describe('platformVersion', () => {
24
24
  expect(isV2Project('notplaformversion')).toBe(false);
25
25
  });
26
26
  });
27
+ describe('isUnsupportedPlatformVersion', () => {
28
+ it('returns false for platform version 2026.03 (boundary)', () => {
29
+ expect(isUnsupportedPlatformVersion('2026.03')).toBe(false);
30
+ });
31
+ it('returns false for platform versions less than 2026.03', () => {
32
+ expect(isUnsupportedPlatformVersion('2025.2')).toBe(false);
33
+ expect(isUnsupportedPlatformVersion('2026.01')).toBe(false);
34
+ expect(isUnsupportedPlatformVersion('2026.02')).toBe(false);
35
+ });
36
+ it('returns true for platform version 2026.04', () => {
37
+ expect(isUnsupportedPlatformVersion('2026.04')).toBe(true);
38
+ });
39
+ it('returns true for platform versions greater than 2026.03', () => {
40
+ expect(isUnsupportedPlatformVersion('2026.4')).toBe(true);
41
+ expect(isUnsupportedPlatformVersion('2027.01')).toBe(true);
42
+ expect(isUnsupportedPlatformVersion('2027.1')).toBe(true);
43
+ expect(isUnsupportedPlatformVersion('2028.10')).toBe(true);
44
+ });
45
+ it('returns false for UNSTABLE', () => {
46
+ expect(isUnsupportedPlatformVersion('UNSTABLE')).toBe(false);
47
+ expect(isUnsupportedPlatformVersion('unstable')).toBe(false);
48
+ });
49
+ it('returns false for null or undefined', () => {
50
+ expect(isUnsupportedPlatformVersion(null)).toBe(false);
51
+ expect(isUnsupportedPlatformVersion(undefined)).toBe(false);
52
+ });
53
+ it('returns false for invalid platform versions', () => {
54
+ expect(isUnsupportedPlatformVersion('notaversion')).toBe(false);
55
+ expect(isUnsupportedPlatformVersion('abc.def')).toBe(false);
56
+ });
57
+ it('handles beta versions correctly', () => {
58
+ expect(isUnsupportedPlatformVersion('2026.03-beta')).toBe(false);
59
+ expect(isUnsupportedPlatformVersion('2026.04-beta')).toBe(true);
60
+ expect(isUnsupportedPlatformVersion('2027.01-beta')).toBe(true);
61
+ });
62
+ });
27
63
  });
@@ -16,12 +16,16 @@ describe('lib/projects', () => {
16
16
  });
17
17
  it('rejects configuration with missing name', () => {
18
18
  // @ts-ignore Testing invalid input
19
- expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(/.*missing required fields*/);
19
+ expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(/missing required field.*name/);
20
20
  });
21
21
  it('rejects configuration with missing srcDir', () => {
22
22
  expect(() =>
23
23
  // @ts-ignore Testing invalid input
24
- validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(/.*missing required fields.*/);
24
+ validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(/missing required field.*srcDir/);
25
+ });
26
+ it('rejects configuration with both name and srcDir missing', () => {
27
+ // @ts-ignore Testing invalid input
28
+ expect(() => validateProjectConfig({}, projectDir)).toThrow(/missing required fields:.*name.*srcDir/);
25
29
  });
26
30
  describe('rejects configuration with srcDir outside project directory', () => {
27
31
  it('for parent directory', () => {
@@ -1,5 +1,11 @@
1
1
  import { Collision } from '@hubspot/local-dev-lib/types/Archive';
2
2
  import type { ProjectMetadata } from '@hubspot/project-parsing-lib/projects';
3
+ export interface ComponentInfo {
4
+ filename?: string;
5
+ isNew: boolean;
6
+ }
7
+ export type ComponentsByType = Map<string, ComponentInfo[]>;
8
+ export declare function buildProjectTree(projectName: string, uids: string[], componentsByType: ComponentsByType, showOnlyNew: boolean): string;
3
9
  export declare function handleComponentCollision({ dest, src, collisions }: Collision): void;
4
10
  export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[], options?: {
5
11
  currentProjectMetadata?: ProjectMetadata;
@@ -12,7 +12,7 @@ import { renderInline } from '../../ui/render.js';
12
12
  import { getSuccessBox } from '../../ui/components/StatusMessageBoxes.js';
13
13
  // Prefix for the metafile extension
14
14
  const metafileExtensionPrefix = path.parse(metafileExtension).name;
15
- function buildProjectTree(projectName, uids, componentsByType, showOnlyNew) {
15
+ export function buildProjectTree(projectName, uids, componentsByType, showOnlyNew) {
16
16
  const lines = [];
17
17
  lines.push(chalk.bold(projectName));
18
18
  const types = Array.from(componentsByType.keys());
@@ -52,8 +52,15 @@ export function validateProjectConfig(projectConfig, projectDir) {
52
52
  if (!projectConfig || !projectDir) {
53
53
  throw new ProjectValidationError(lib.projects.validateProjectConfig.configNotFound);
54
54
  }
55
- if (!projectConfig.name || !projectConfig.srcDir) {
56
- throw new ProjectValidationError(lib.projects.validateProjectConfig.configMissingFields);
55
+ const missingFields = [];
56
+ if (!projectConfig.name) {
57
+ missingFields.push('name');
58
+ }
59
+ if (!projectConfig.srcDir) {
60
+ missingFields.push('srcDir');
61
+ }
62
+ if (missingFields.length > 0) {
63
+ throw new ProjectValidationError(lib.projects.validateProjectConfig.configMissingFields(missingFields));
57
64
  }
58
65
  const resolvedPath = path.resolve(projectDir, projectConfig.srcDir);
59
66
  if (!resolvedPath.startsWith(projectDir)) {
@@ -1,4 +1,4 @@
1
- import { type IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/translate';
1
+ import { type IntermediateRepresentationNode, type IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/translate';
2
2
  import { Build } from '@hubspot/local-dev-lib/types/Build';
3
3
  import { Project } from '@hubspot/local-dev-lib/types/Project';
4
4
  import { ProjectConfig } from '../../../../types/Projects.js';
@@ -7,6 +7,9 @@ export declare function createInitialBuildForNewProject(projectConfig: ProjectCo
7
7
  export declare function compareLocalProjectToDeployed(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number | undefined, localProjectNodes: {
8
8
  [key: string]: IntermediateRepresentationNodeLocalDev;
9
9
  }, profile?: string): Promise<void>;
10
+ export declare function getDeployedProjectNodes(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, profile?: string): Promise<{
11
+ [key: string]: IntermediateRepresentationNode;
12
+ }>;
10
13
  export declare function isDeployedProjectUpToDateWithLocal(projectConfig: ProjectConfig, accountId: number, deployedBuildId: number, localProjectNodes: {
11
14
  [key: string]: IntermediateRepresentationNodeLocalDev;
12
15
  }, profile?: string): Promise<boolean>;
@@ -149,31 +149,36 @@ export async function compareLocalProjectToDeployed(projectConfig, accountId, de
149
149
  process.exit(EXIT_CODES.SUCCESS);
150
150
  }
151
151
  }
152
- export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes, profile) {
152
+ export async function getDeployedProjectNodes(projectConfig, accountId, deployedBuildId, profile) {
153
153
  let tempDir = null;
154
154
  try {
155
155
  tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hubspot-project-compare-'));
156
156
  const { data: zippedProject } = await downloadProject(accountId, projectConfig.name, deployedBuildId);
157
157
  await extractZipArchive(zippedProject, sanitizeFileName(projectConfig.name), tempDir, { hideLogs: true });
158
158
  const deployedProjectSourceDir = path.join(tempDir, projectConfig.srcDir);
159
- const { intermediateNodesIndexedByUid: deployedProjectNodes } = await translate({
159
+ const { intermediateNodesIndexedByUid } = await translate({
160
160
  projectSourceDir: deployedProjectSourceDir,
161
161
  platformVersion: projectConfig.platformVersion,
162
162
  accountId: accountId,
163
163
  }, { profile });
164
- return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
165
- }
166
- catch (err) {
167
- debugError(err);
168
- return false;
164
+ return intermediateNodesIndexedByUid;
169
165
  }
170
166
  finally {
171
- // Clean up temporary directory
172
167
  if (tempDir && (await fs.pathExists(tempDir))) {
173
168
  await fs.remove(tempDir);
174
169
  }
175
170
  }
176
171
  }
172
+ export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountId, deployedBuildId, localProjectNodes, profile) {
173
+ try {
174
+ const deployedProjectNodes = await getDeployedProjectNodes(projectConfig, accountId, deployedBuildId, profile);
175
+ return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
176
+ }
177
+ catch (err) {
178
+ debugError(err);
179
+ return false;
180
+ }
181
+ }
177
182
  export async function checkAndInstallDependencies() {
178
183
  uiLogger.log('');
179
184
  SpinniesManager.add('checkingDependencies', {
@@ -1 +1,9 @@
1
+ /**
2
+ * Used to surface warnings when users attempt to interact with new platform versions
3
+ * that were released after this version of the CLI was released.
4
+ *
5
+ * We are unable to reliably support versions of projects that are newer than any given CLI release
6
+ * */
7
+ export declare const LATEST_SUPPORTED_PLATFORM_VERSION = "2026.03";
1
8
  export declare function isV2Project(platformVersion?: string | null): boolean;
9
+ export declare function isUnsupportedPlatformVersion(platformVersion?: string | null): boolean;