@hubspot/cli 7.7.31-experimental.0 → 7.7.33-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.
- package/commands/app.js +1 -6
- package/commands/getStarted.js +5 -4
- package/commands/project/__tests__/add.test.js +3 -5
- package/commands/project/__tests__/deploy.test.js +3 -2
- package/commands/project/add.js +2 -4
- package/commands/project/deploy.js +9 -61
- package/commands/project/dev/index.js +1 -1
- package/commands/project/dev/unifiedFlow.js +3 -0
- package/commands/project/upload.d.ts +2 -2
- package/commands/project/upload.js +3 -3
- package/commands/project/validate.js +1 -1
- package/commands/project/watch.js +2 -2
- package/commands/testAccount/create.js +0 -3
- package/lang/en.d.ts +8 -26
- package/lang/en.js +9 -27
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/__tests__/importData.test.js +1 -1
- package/lib/app/migrate.js +9 -2
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +2 -0
- package/lib/errorHandlers/index.d.ts +4 -0
- package/lib/errorHandlers/index.js +1 -1
- package/lib/hasFeature.js +6 -0
- package/lib/importData.js +1 -1
- package/lib/mcp/setup.js +1 -1
- package/lib/projectProfiles.d.ts +1 -1
- package/lib/projectProfiles.js +2 -10
- package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
- package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
- package/lib/projects/__tests__/deploy.test.js +164 -0
- package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
- package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
- package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
- package/lib/projects/add/legacyAddComponent.d.ts +1 -1
- package/lib/projects/add/legacyAddComponent.js +5 -1
- package/lib/projects/add/v3AddComponent.d.ts +1 -0
- package/lib/projects/add/v3AddComponent.js +2 -2
- package/lib/projects/create/__tests__/v3.test.js +97 -9
- package/lib/projects/create/index.js +2 -2
- package/lib/projects/create/legacy.js +1 -1
- package/lib/projects/create/v3.d.ts +2 -2
- package/lib/projects/create/v3.js +35 -12
- package/lib/projects/deploy.d.ts +13 -0
- package/lib/projects/deploy.js +63 -0
- package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
- package/lib/projects/localDev/AppDevModeInterface.js +65 -36
- package/lib/projects/localDev/DevServerManagerV2.js +1 -0
- package/lib/projects/localDev/LocalDevProcess.js +3 -1
- package/lib/projects/localDev/LocalDevState.d.ts +5 -2
- package/lib/projects/localDev/LocalDevState.js +9 -1
- package/lib/projects/localDev/helpers/project.js +1 -1
- package/lib/projects/platformVersion.d.ts +1 -0
- package/lib/projects/platformVersion.js +10 -0
- package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
- package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
- package/lib/projects/structure.d.ts +2 -2
- package/lib/projects/upload.d.ts +1 -2
- package/lib/projects/upload.js +1 -2
- package/lib/projects/urls.d.ts +1 -0
- package/lib/projects/urls.js +3 -0
- package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
- package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
- package/lib/prompts/importDataFilePathPrompt.js +4 -2
- package/lib/prompts/installAppPrompt.d.ts +6 -1
- package/lib/prompts/installAppPrompt.js +6 -1
- package/lib/prompts/projectAddPrompt.js +1 -1
- package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
- package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
- package/mcp-server/tools/index.js +8 -0
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/GetConfigValuesTool.js +3 -3
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +6 -4
- package/package.json +4 -3
- package/types/LocalDev.d.ts +2 -1
- package/types/Projects.d.ts +1 -0
- package/types/Yargs.d.ts +1 -1
- package/ui/components/BoxWithTitle.d.ts +8 -0
- package/ui/components/BoxWithTitle.js +9 -0
- package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
- package/ui/components/HorizontalSelectPrompt.js +30 -0
- package/ui/components/StatusMessageBoxes.d.ts +12 -0
- package/ui/components/StatusMessageBoxes.js +31 -0
- package/ui/lib/ui-testing-utils.d.ts +9 -0
- package/ui/lib/ui-testing-utils.js +47 -0
- package/ui/lib/useTerminalSize.d.ts +13 -0
- package/ui/lib/useTerminalSize.js +31 -0
- package/ui/styles.d.ts +18 -0
- package/ui/styles.js +18 -0
- package/ui/views/UiSandbox.d.ts +5 -0
- package/ui/views/UiSandbox.js +25 -0
- package/commands/app/__tests__/install.test.js +0 -47
- package/commands/app/install.d.ts +0 -8
- package/commands/app/install.js +0 -122
- /package/{commands/app/__tests__/install.test.d.ts → lib/projects/__tests__/deploy.test.d.ts} +0 -0
- /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → platformVersion.test.d.ts} +0 -0
|
@@ -1,35 +1,173 @@
|
|
|
1
1
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
2
|
-
import {
|
|
2
|
+
import { http } from '@hubspot/local-dev-lib/http';
|
|
3
|
+
import { hasFeature, hasUnfiedAppsAccess } from '../hasFeature.js';
|
|
4
|
+
import { FEATURES } from '../constants.js';
|
|
3
5
|
vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
|
|
6
|
+
vi.mock('@hubspot/local-dev-lib/http');
|
|
4
7
|
const mockedFetchEnabledFeatures = fetchEnabledFeatures;
|
|
8
|
+
const mockedHttp = http;
|
|
5
9
|
describe('lib/hasFeature', () => {
|
|
6
10
|
describe('hasFeature()', () => {
|
|
7
11
|
const accountId = 123;
|
|
8
|
-
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it('should return true if the feature is enabled', async () => {
|
|
9
16
|
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
10
17
|
data: {
|
|
11
18
|
enabledFeatures: {
|
|
12
19
|
'feature-1': true,
|
|
13
|
-
'feature-2': false,
|
|
14
|
-
'feature-3': true,
|
|
15
20
|
},
|
|
16
21
|
},
|
|
17
22
|
});
|
|
18
|
-
});
|
|
19
|
-
it('should return true if the feature is enabled', async () => {
|
|
20
23
|
// @ts-expect-error test data
|
|
21
24
|
const result = await hasFeature(accountId, 'feature-1');
|
|
22
25
|
expect(result).toBe(true);
|
|
23
26
|
});
|
|
24
|
-
it('should return false if the feature is
|
|
27
|
+
it('should return false if the feature is disabled', async () => {
|
|
28
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
29
|
+
data: {
|
|
30
|
+
enabledFeatures: {
|
|
31
|
+
'feature-2': false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
25
35
|
// @ts-expect-error test data
|
|
26
36
|
const result = await hasFeature(accountId, 'feature-2');
|
|
27
37
|
expect(result).toBe(false);
|
|
28
38
|
});
|
|
29
39
|
it('should return false if the feature is not present', async () => {
|
|
40
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
41
|
+
data: {
|
|
42
|
+
enabledFeatures: {},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
30
45
|
// @ts-expect-error test data
|
|
31
46
|
const result = await hasFeature(accountId, 'feature-4');
|
|
32
47
|
expect(result).toBe(false);
|
|
33
48
|
});
|
|
49
|
+
it('should return true for APPS_HOME feature when not present in enabled features (defaults on)', async () => {
|
|
50
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
51
|
+
data: {
|
|
52
|
+
enabledFeatures: {},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const result = await hasFeature(accountId, FEATURES.APPS_HOME);
|
|
56
|
+
expect(result).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('should respect explicit setting for APPS_HOME feature even when it defaults on', async () => {
|
|
59
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
60
|
+
data: {
|
|
61
|
+
enabledFeatures: {
|
|
62
|
+
[FEATURES.APPS_HOME]: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const result = await hasFeature(accountId, FEATURES.APPS_HOME);
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('should handle truthy values correctly', async () => {
|
|
70
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
71
|
+
data: {
|
|
72
|
+
enabledFeatures: {
|
|
73
|
+
'feature-truthy': 'yes',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// @ts-expect-error test data
|
|
78
|
+
const truthyResult = await hasFeature(accountId, 'feature-truthy');
|
|
79
|
+
expect(truthyResult).toBe(true);
|
|
80
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
81
|
+
data: {
|
|
82
|
+
enabledFeatures: {
|
|
83
|
+
'feature-number': 1,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// @ts-expect-error test data
|
|
88
|
+
const numberResult = await hasFeature(accountId, 'feature-number');
|
|
89
|
+
expect(numberResult).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('should handle falsy values correctly', async () => {
|
|
92
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
93
|
+
data: {
|
|
94
|
+
enabledFeatures: {
|
|
95
|
+
'feature-null': null,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
// @ts-expect-error test data
|
|
100
|
+
const nullResult = await hasFeature(accountId, 'feature-null');
|
|
101
|
+
expect(nullResult).toBe(false);
|
|
102
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
103
|
+
data: {
|
|
104
|
+
enabledFeatures: {
|
|
105
|
+
'feature-zero': 0,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
// @ts-expect-error test data
|
|
110
|
+
const zeroResult = await hasFeature(accountId, 'feature-zero');
|
|
111
|
+
expect(zeroResult).toBe(false);
|
|
112
|
+
mockedFetchEnabledFeatures.mockResolvedValueOnce({
|
|
113
|
+
data: {
|
|
114
|
+
enabledFeatures: {
|
|
115
|
+
'feature-empty': '',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// @ts-expect-error test data
|
|
120
|
+
const emptyResult = await hasFeature(accountId, 'feature-empty');
|
|
121
|
+
expect(emptyResult).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('should propagate errors from fetchEnabledFeatures', async () => {
|
|
124
|
+
const error = new Error('API error');
|
|
125
|
+
mockedFetchEnabledFeatures.mockRejectedValueOnce(error);
|
|
126
|
+
await expect(hasFeature(accountId, FEATURES.UNIFIED_APPS)).rejects.toThrow('API error');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('hasUnfiedAppsAccess()', () => {
|
|
130
|
+
const accountId = 123;
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
vi.clearAllMocks();
|
|
133
|
+
});
|
|
134
|
+
it('should return true when API returns true', async () => {
|
|
135
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
136
|
+
mockedHttp.get.mockResolvedValueOnce({ data: true });
|
|
137
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
138
|
+
expect(result).toBe(true);
|
|
139
|
+
expect(mockedHttp.get).toHaveBeenCalledWith(accountId, {
|
|
140
|
+
url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('should return false when API returns false', async () => {
|
|
144
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
145
|
+
mockedHttp.get.mockResolvedValueOnce({ data: false });
|
|
146
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
147
|
+
expect(result).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
it('should handle truthy values correctly', async () => {
|
|
150
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
151
|
+
mockedHttp.get.mockResolvedValueOnce({ data: 'yes' });
|
|
152
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
153
|
+
expect(result).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('should handle falsy values correctly', async () => {
|
|
156
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
157
|
+
mockedHttp.get.mockResolvedValueOnce({ data: null });
|
|
158
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
159
|
+
expect(result).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('should handle undefined response data', async () => {
|
|
162
|
+
// @ts-expect-error Don't want to mock the full response object
|
|
163
|
+
mockedHttp.get.mockResolvedValueOnce({ data: undefined });
|
|
164
|
+
const result = await hasUnfiedAppsAccess(accountId);
|
|
165
|
+
expect(result).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
it('should propagate errors from http.get', async () => {
|
|
168
|
+
const error = new Error('Network error');
|
|
169
|
+
mockedHttp.get.mockRejectedValueOnce(error);
|
|
170
|
+
await expect(hasUnfiedAppsAccess(accountId)).rejects.toThrow('Network error');
|
|
171
|
+
});
|
|
34
172
|
});
|
|
35
173
|
});
|
|
@@ -43,7 +43,7 @@ describe('lib/importData', () => {
|
|
|
43
43
|
data: { id: '123' },
|
|
44
44
|
});
|
|
45
45
|
await handleImportData(targetAccountId, dataFileNames, importRequest);
|
|
46
|
-
expect(mockUiLogger.
|
|
46
|
+
expect(mockUiLogger.info).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
|
|
47
47
|
});
|
|
48
48
|
it('should log the correct error message', async () => {
|
|
49
49
|
mockCreateImport.mockRejectedValue(new Error('test-error'));
|
package/lib/app/migrate.js
CHANGED
|
@@ -22,7 +22,7 @@ import { hasUnfiedAppsAccess } from '../hasFeature.js';
|
|
|
22
22
|
import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
|
|
23
23
|
import { uiLogger } from '../ui/logger.js';
|
|
24
24
|
import { debugError } from '../errorHandlers/index.js';
|
|
25
|
-
import { useV3Api } from '../projects/
|
|
25
|
+
import { useV3Api } from '../projects/platformVersion.js';
|
|
26
26
|
export function getUnmigratableReason(reasonCode, projectName, accountId) {
|
|
27
27
|
switch (reasonCode) {
|
|
28
28
|
case UNMIGRATABLE_REASONS.UP_TO_DATE:
|
|
@@ -236,13 +236,20 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
|
|
|
236
236
|
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
237
237
|
}
|
|
238
238
|
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
239
|
+
let migrated = false;
|
|
240
|
+
let failureReason;
|
|
239
241
|
try {
|
|
240
|
-
await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
242
|
+
const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
243
|
+
migrated = migrationResult.migrated;
|
|
244
|
+
failureReason = migrationResult.failureReason;
|
|
241
245
|
}
|
|
242
246
|
catch (error) {
|
|
243
247
|
debugError(error);
|
|
244
248
|
throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
|
|
245
249
|
}
|
|
250
|
+
if (!migrated) {
|
|
251
|
+
throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
|
|
252
|
+
}
|
|
246
253
|
const newProjectConfig = { ...projectConfig.projectConfig };
|
|
247
254
|
newProjectConfig.platformVersion = platformVersion;
|
|
248
255
|
const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
|
package/lib/constants.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare const FEATURES: {
|
|
|
80
80
|
readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
|
|
81
81
|
readonly SANDBOXES_V2: "sandboxes:v2:enabled";
|
|
82
82
|
readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
|
|
83
|
+
readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
|
|
84
|
+
readonly APPS_HOME: "UIE:AppHome";
|
|
83
85
|
};
|
|
84
86
|
export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
85
87
|
UPLOAD_SUCCESS: string;
|
package/lib/constants.js
CHANGED
|
@@ -72,6 +72,8 @@ export const FEATURES = {
|
|
|
72
72
|
UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
|
|
73
73
|
SANDBOXES_V2: 'sandboxes:v2:enabled',
|
|
74
74
|
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
75
|
+
APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
|
|
76
|
+
APPS_HOME: 'UIE:AppHome',
|
|
75
77
|
};
|
|
76
78
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
77
79
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
|
@@ -85,7 +85,7 @@ export class ApiErrorContext {
|
|
|
85
85
|
this.projectName = props.projectName || '';
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
function isErrorWithMessageOrReason(error) {
|
|
88
|
+
export function isErrorWithMessageOrReason(error) {
|
|
89
89
|
return (typeof error === 'object' &&
|
|
90
90
|
error !== null &&
|
|
91
91
|
('message' in error || 'reason' in error));
|
package/lib/hasFeature.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { http } from '@hubspot/local-dev-lib/http';
|
|
2
2
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
3
|
+
import { FEATURES } from './constants.js';
|
|
4
|
+
const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
|
|
3
5
|
export async function hasFeature(accountId, feature) {
|
|
4
6
|
const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
|
|
7
|
+
if (enabledFeatures[feature] === undefined &&
|
|
8
|
+
FEATURES_THAT_DEFAULT_ON.includes(feature)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
5
11
|
return Boolean(enabledFeatures[feature]);
|
|
6
12
|
}
|
|
7
13
|
export async function hasUnfiedAppsAccess(accountId) {
|
package/lib/importData.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function handleImportData(targetAccountId, dataFileNames, importReq
|
|
|
10
10
|
const baseUrl = getHubSpotWebsiteOrigin(getEnv());
|
|
11
11
|
const response = await createImport(targetAccountId, importRequest, dataFileNames);
|
|
12
12
|
const importId = response.data.id;
|
|
13
|
-
uiLogger.
|
|
13
|
+
uiLogger.info(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
16
16
|
uiLogger.error(lib.importData.errors.failedToImportData);
|
package/lib/mcp/setup.js
CHANGED
|
@@ -182,7 +182,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
182
182
|
});
|
|
183
183
|
await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
|
|
184
184
|
}
|
|
185
|
-
await execAsync(`claude mcp add-json "${mcpServerName}"
|
|
185
|
+
await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
|
|
186
186
|
SpinniesManager.succeed('claudeCode', {
|
|
187
187
|
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
188
188
|
});
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
|
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
5
|
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
|
|
6
6
|
export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
|
package/lib/projectProfiles.js
CHANGED
|
@@ -38,12 +38,7 @@ export function loadProfile(projectConfig, projectDir, profileName) {
|
|
|
38
38
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
42
|
-
...profile,
|
|
43
|
-
accountId: process.env.HUBSPOT_ACCOUNT_ID
|
|
44
|
-
? Number(process.env.HUBSPOT_ACCOUNT_ID)
|
|
45
|
-
: profile.accountId,
|
|
46
|
-
};
|
|
41
|
+
return profile;
|
|
47
42
|
}
|
|
48
43
|
catch (e) {
|
|
49
44
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
|
|
@@ -59,7 +54,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
|
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
|
-
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile
|
|
57
|
+
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
|
|
63
58
|
if (argsProfile) {
|
|
64
59
|
logProfileHeader(argsProfile);
|
|
65
60
|
const profile = loadProfile(projectConfig, projectDir, argsProfile);
|
|
@@ -68,9 +63,6 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
|
|
|
68
63
|
process.exit(EXIT_CODES.ERROR);
|
|
69
64
|
}
|
|
70
65
|
logProfileFooter(profile, true);
|
|
71
|
-
if (useEnv) {
|
|
72
|
-
return Number(process.env.HUBSPOT_ACCOUNT_ID);
|
|
73
|
-
}
|
|
74
66
|
return profile.accountId;
|
|
75
67
|
}
|
|
76
68
|
else {
|
|
@@ -240,16 +240,21 @@ describe('AppDevModeInterface', () => {
|
|
|
240
240
|
await newAppDevModeInterface.setup({});
|
|
241
241
|
expect(process.exit).toHaveBeenCalledWith(0);
|
|
242
242
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
243
|
+
// @TODO: Restore test account auto install functionality
|
|
244
|
+
// it('should auto-install static auth app on test account', async () => {
|
|
245
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
246
|
+
// data: {
|
|
247
|
+
// isInstalledWithScopeGroups: false,
|
|
248
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
249
|
+
// },
|
|
250
|
+
// });
|
|
251
|
+
// await appDevModeInterface.setup({});
|
|
252
|
+
// expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
|
|
253
|
+
// 123,
|
|
254
|
+
// 67890,
|
|
255
|
+
// [1, 2, 3]
|
|
256
|
+
// );
|
|
257
|
+
// });
|
|
253
258
|
it('should open browser for OAuth app installation', async () => {
|
|
254
259
|
const oauthAppNode = {
|
|
255
260
|
...mockAppNode,
|
|
@@ -288,7 +293,12 @@ describe('AppDevModeInterface', () => {
|
|
|
288
293
|
},
|
|
289
294
|
});
|
|
290
295
|
await appDevModeInterface.setup({});
|
|
291
|
-
expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true
|
|
296
|
+
expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
|
|
297
|
+
appUid: 'test-app-uid',
|
|
298
|
+
projectAccountId: 12345,
|
|
299
|
+
projectName: 'test-project',
|
|
300
|
+
testingAccountId: 67890,
|
|
301
|
+
});
|
|
292
302
|
});
|
|
293
303
|
it('should handle errors during setup', async () => {
|
|
294
304
|
const error = new Error('Setup failed');
|
|
@@ -318,39 +328,46 @@ describe('AppDevModeInterface', () => {
|
|
|
318
328
|
await appDevModeInterface.setup({});
|
|
319
329
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
320
330
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
331
|
+
// @TODO: Restore test account auto install functionality
|
|
332
|
+
// it('should exit if user declines auto-install', async () => {
|
|
333
|
+
// // Set up conditions for automatic installation
|
|
334
|
+
// (getAccountConfig as Mock).mockReturnValue({
|
|
335
|
+
// parentAccountId: 12345, // matches targetProjectAccountId
|
|
336
|
+
// });
|
|
337
|
+
// (isDeveloperTestAccount as Mock).mockReturnValue(true);
|
|
338
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
339
|
+
// data: {
|
|
340
|
+
// isInstalledWithScopeGroups: false,
|
|
341
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
342
|
+
// },
|
|
343
|
+
// });
|
|
344
|
+
// (installAppAutoPrompt as Mock).mockResolvedValue(false);
|
|
345
|
+
// // Create a new instance to trigger the exit during setup
|
|
346
|
+
// const newAppDevModeInterface = new AppDevModeInterface({
|
|
347
|
+
// localDevState: mockLocalDevState,
|
|
348
|
+
// localDevLogger: mockLocalDevLogger,
|
|
349
|
+
// });
|
|
350
|
+
// // The setup method catches the error, so we check that process.exit was called
|
|
351
|
+
// await newAppDevModeInterface.setup({});
|
|
352
|
+
// expect(process.exit).toHaveBeenCalledWith(0);
|
|
353
|
+
// });
|
|
354
|
+
// @TODO: Restore test account auto install functionality
|
|
355
|
+
// it('should fallback to browser install if auto-install fails', async () => {
|
|
356
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
357
|
+
// data: {
|
|
358
|
+
// isInstalledWithScopeGroups: false,
|
|
359
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
360
|
+
// },
|
|
361
|
+
// });
|
|
362
|
+
// (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
|
|
363
|
+
// new Error('Install failed')
|
|
364
|
+
// );
|
|
365
|
+
// await appDevModeInterface.setup({});
|
|
366
|
+
// expect(installAppBrowserPrompt).toHaveBeenCalledWith(
|
|
367
|
+
// 'http://static-install-url',
|
|
368
|
+
// false
|
|
369
|
+
// );
|
|
370
|
+
// });
|
|
354
371
|
});
|
|
355
372
|
describe('start()', () => {
|
|
356
373
|
it('should return early if no app node exists', async () => {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { validateBuildIdForDeploy, logDeployErrors, handleProjectDeploy, } from '../deploy.js';
|
|
3
|
+
import { uiLogger } from '../../ui/logger.js';
|
|
4
|
+
import { commands } from '../../../lang/en.js';
|
|
5
|
+
import { PROJECT_ERROR_TYPES } from '../../constants.js';
|
|
6
|
+
import { deployProject } from '@hubspot/local-dev-lib/api/projects';
|
|
7
|
+
import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
|
|
8
|
+
// Mock external dependencies
|
|
9
|
+
vi.mock('../../ui/logger.js');
|
|
10
|
+
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
11
|
+
vi.mock('../pollProjectBuildAndDeploy.js');
|
|
12
|
+
const mockUiLogger = vi.mocked(uiLogger);
|
|
13
|
+
const mockDeployProject = vi.mocked(deployProject);
|
|
14
|
+
const mockPollDeployStatus = vi.mocked(pollDeployStatus);
|
|
15
|
+
describe('lib/projects/deploy', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.resetAllMocks();
|
|
18
|
+
});
|
|
19
|
+
describe('validateBuildIdForDeploy()', () => {
|
|
20
|
+
const accountId = 12345;
|
|
21
|
+
const projectName = 'test-project';
|
|
22
|
+
it('returns true when build ID is valid for deployment', () => {
|
|
23
|
+
const buildId = 5;
|
|
24
|
+
const deployedBuildId = 3;
|
|
25
|
+
const latestBuildId = 10;
|
|
26
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
27
|
+
expect(result).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('returns error message when build ID does not exist', () => {
|
|
30
|
+
const buildId = 15;
|
|
31
|
+
const deployedBuildId = 3;
|
|
32
|
+
const latestBuildId = 10;
|
|
33
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
34
|
+
expect(result).toBe(commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName));
|
|
35
|
+
});
|
|
36
|
+
it('returns error message when build is already deployed', () => {
|
|
37
|
+
const buildId = 3;
|
|
38
|
+
const deployedBuildId = 3;
|
|
39
|
+
const latestBuildId = 10;
|
|
40
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
41
|
+
expect(result).toBe(commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName));
|
|
42
|
+
});
|
|
43
|
+
it('handles edge case when deployedBuildId is undefined', () => {
|
|
44
|
+
const buildId = 5;
|
|
45
|
+
const deployedBuildId = undefined;
|
|
46
|
+
const latestBuildId = 10;
|
|
47
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('logDeployErrors()', () => {
|
|
52
|
+
it('logs main error message and individual error messages', () => {
|
|
53
|
+
const errorData = {
|
|
54
|
+
message: 'Deploy failed with errors',
|
|
55
|
+
errors: [
|
|
56
|
+
{
|
|
57
|
+
message: 'Component error 1',
|
|
58
|
+
subCategory: 'SOME_ERROR',
|
|
59
|
+
context: { COMPONENT_NAME: 'test-component' },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
message: 'Component error 2',
|
|
63
|
+
subCategory: 'ANOTHER_ERROR',
|
|
64
|
+
context: { COMPONENT_NAME: 'another-component' },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
logDeployErrors(errorData);
|
|
69
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy failed with errors');
|
|
70
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 1');
|
|
71
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 2');
|
|
72
|
+
});
|
|
73
|
+
it('handles DEPLOY_CONTAINS_REMOVALS error type specially', () => {
|
|
74
|
+
const errorData = {
|
|
75
|
+
message: 'Deploy contains removals',
|
|
76
|
+
errors: [
|
|
77
|
+
{
|
|
78
|
+
message: 'Component will be removed',
|
|
79
|
+
subCategory: PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS,
|
|
80
|
+
context: { COMPONENT_NAME: 'removed-component' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
logDeployErrors(errorData);
|
|
85
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy contains removals');
|
|
86
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployContainsRemovals('removed-component'));
|
|
87
|
+
});
|
|
88
|
+
it('handles empty errors array', () => {
|
|
89
|
+
const errorData = {
|
|
90
|
+
message: 'No specific errors',
|
|
91
|
+
errors: [],
|
|
92
|
+
};
|
|
93
|
+
logDeployErrors(errorData);
|
|
94
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('No specific errors');
|
|
95
|
+
expect(mockUiLogger.log).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('handleProjectDeploy()', () => {
|
|
99
|
+
const targetAccountId = 12345;
|
|
100
|
+
const projectName = 'test-project';
|
|
101
|
+
const buildId = 5;
|
|
102
|
+
const useV3Api = true;
|
|
103
|
+
const force = false;
|
|
104
|
+
it('successfully deploys and returns deploy result', async () => {
|
|
105
|
+
const mockDeployResponseData = {
|
|
106
|
+
id: 'deploy-123',
|
|
107
|
+
buildResultType: 'DEPLOY_QUEUED',
|
|
108
|
+
links: {
|
|
109
|
+
status: 'http://status-url',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const mockDeployResult = {
|
|
113
|
+
deployId: 123,
|
|
114
|
+
buildId: 5,
|
|
115
|
+
status: 'SUCCESS',
|
|
116
|
+
enqueuedAt: '2023-01-01T00:00:00Z',
|
|
117
|
+
startedAt: '2023-01-01T00:01:00Z',
|
|
118
|
+
finishedAt: '2023-01-01T00:05:00Z',
|
|
119
|
+
portalId: targetAccountId,
|
|
120
|
+
projectName: 'test-project',
|
|
121
|
+
userId: 456,
|
|
122
|
+
source: 'HUBSPOT_USER',
|
|
123
|
+
subdeployStatuses: [],
|
|
124
|
+
};
|
|
125
|
+
mockDeployProject.mockResolvedValue({
|
|
126
|
+
data: mockDeployResponseData,
|
|
127
|
+
});
|
|
128
|
+
mockPollDeployStatus.mockResolvedValue(mockDeployResult);
|
|
129
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
130
|
+
expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
|
|
131
|
+
expect(result).toEqual(mockDeployResult);
|
|
132
|
+
});
|
|
133
|
+
it('handles blocked deploy with warnings', async () => {
|
|
134
|
+
const mockBlockedResponse = {
|
|
135
|
+
buildResultType: 'DEPLOY_BLOCKED',
|
|
136
|
+
issues: [
|
|
137
|
+
{
|
|
138
|
+
uid: 'component-1',
|
|
139
|
+
componentTypeName: 'module',
|
|
140
|
+
errorMessages: [],
|
|
141
|
+
blockingMessages: [
|
|
142
|
+
{
|
|
143
|
+
message: 'This is a warning',
|
|
144
|
+
isWarning: true,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
mockDeployProject.mockResolvedValue({
|
|
151
|
+
data: mockBlockedResponse,
|
|
152
|
+
});
|
|
153
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
154
|
+
expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
|
|
155
|
+
expect(result).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
it('handles general deploy failure', async () => {
|
|
158
|
+
mockDeployProject.mockResolvedValue({ data: null });
|
|
159
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
160
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
161
|
+
expect(result).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useV3Api } from '../
|
|
2
|
-
describe('
|
|
1
|
+
import { useV3Api } from '../platformVersion.js';
|
|
2
|
+
describe('platformVersion', () => {
|
|
3
3
|
describe('useV3Api', () => {
|
|
4
4
|
it('returns true if platform version is UNSTABLE', () => {
|
|
5
5
|
expect(useV3Api('UNSTABLE')).toBe(true);
|