@hubspot/cli 7.10.0-experimental.0 → 7.10.1-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/project/__tests__/validate.test.js +285 -27
- package/commands/project/deploy.js +14 -6
- package/commands/project/dev/index.js +13 -4
- package/commands/project/migrate.js +4 -13
- package/commands/project/upload.js +8 -2
- package/commands/project/validate.js +72 -12
- package/lang/en.d.ts +13 -10
- package/lang/en.js +14 -11
- package/lib/__tests__/projectProfiles.test.js +273 -32
- package/lib/middleware/autoUpdateMiddleware.js +25 -22
- package/lib/middleware/fireAlarmMiddleware.js +4 -15
- package/lib/projectProfiles.d.ts +4 -3
- package/lib/projectProfiles.js +78 -32
- package/lib/projects/localDev/LocalDevLogger.d.ts +0 -3
- package/lib/projects/localDev/LocalDevLogger.js +0 -9
- package/lib/projects/localDev/LocalDevProcess.d.ts +0 -1
- package/lib/projects/localDev/LocalDevProcess.js +1 -12
- package/lib/projects/localDev/LocalDevState.d.ts +0 -3
- package/lib/projects/localDev/LocalDevState.js +0 -9
- package/mcp-server/utils/config.js +1 -1
- package/package.json +3 -4
- package/types/LocalDev.d.ts +0 -1
- package/ui/components/BoxWithTitle.d.ts +2 -1
- package/ui/components/BoxWithTitle.js +2 -2
- package/ui/components/StatusMessageBoxes.d.ts +5 -4
- package/ui/components/StatusMessageBoxes.js +8 -8
- package/lib/projects/localDev/DevSessionManager.d.ts +0 -17
- package/lib/projects/localDev/DevSessionManager.js +0 -56
- package/lib/projects/localDev/helpers/devSessionsApi.d.ts +0 -9
- package/lib/projects/localDev/helpers/devSessionsApi.js +0 -19
- package/lib/ui/boxen.d.ts +0 -5
- package/lib/ui/boxen.js +0 -26
package/lang/en.js
CHANGED
|
@@ -1824,10 +1824,19 @@ export const commands = {
|
|
|
1824
1824
|
default: 'Validate the project before uploading',
|
|
1825
1825
|
},
|
|
1826
1826
|
success: (projectName) => `Project ${projectName} is valid and ready to upload`,
|
|
1827
|
-
failure: (projectName) => `Project ${projectName} is invalid`,
|
|
1827
|
+
failure: (projectName, profileName) => `Project ${projectName} is invalid${profileName ? `with profile ${profileName} applied` : ''}`,
|
|
1828
|
+
spinners: {
|
|
1829
|
+
validatingProfile: (profileName) => `Validating project with profile "${profileName}"`,
|
|
1830
|
+
profileValidationFailed: (profileName) => `Profile "${profileName}" failed validation`,
|
|
1831
|
+
profileValidationSucceeded: (profileName) => `Project valid with profile "${profileName}" applied`,
|
|
1832
|
+
invalidWithProfile: (profileName, projectName) => `Project is invalid with profile "${profileName}" applied \n ${commands.project.validate.failure(projectName)}`,
|
|
1833
|
+
validatingAllProfiles: 'Validating the project with all profiles',
|
|
1834
|
+
allProfilesValidationSucceeded: 'Project profile validation succeeded',
|
|
1835
|
+
allProfilesValidationFailed: 'Project profile validation failed',
|
|
1836
|
+
},
|
|
1828
1837
|
options: {
|
|
1829
1838
|
profile: {
|
|
1830
|
-
describe: 'The profile to target for this validation',
|
|
1839
|
+
describe: 'The profile to target for this validation. If no profile is provided, all profiles will be validated.',
|
|
1831
1840
|
},
|
|
1832
1841
|
},
|
|
1833
1842
|
},
|
|
@@ -2866,11 +2875,6 @@ export const lib = {
|
|
|
2866
2875
|
startError: (message) => `Failed to start local dev server: ${message}`,
|
|
2867
2876
|
fileChangeError: (message) => `Failed to notify local dev server of file change: ${message}`,
|
|
2868
2877
|
},
|
|
2869
|
-
devSession: {
|
|
2870
|
-
registrationError: (message) => `Failed to register dev session: ${message}`,
|
|
2871
|
-
heartbeatError: (message) => `Failed to send dev session heartbeat: ${message}`,
|
|
2872
|
-
deletionError: (message) => `Failed to delete dev session: ${message}`,
|
|
2873
|
-
},
|
|
2874
2878
|
},
|
|
2875
2879
|
AppDevModeInterface: {
|
|
2876
2880
|
autoInstallStaticAuthApp: {
|
|
@@ -2983,7 +2987,7 @@ export const lib = {
|
|
|
2983
2987
|
updateNotification: {
|
|
2984
2988
|
notifyTitle: chalk.bold('CLI update available'),
|
|
2985
2989
|
cmsUpdateNotification: (packageName) => `${chalk.bold('The CMS CLI is now the HubSpot CLI')}\n\nTo upgrade, uninstall ${chalk.bold(packageName)}\nand then run ${uiCommandReference('{updateCommand}')}`,
|
|
2986
|
-
cliUpdateNotification: `HubSpot CLI version ${chalk.cyan(chalk.bold(
|
|
2990
|
+
cliUpdateNotification: (currentVersion, updateCommand, latestVersion) => `HubSpot CLI version ${chalk.cyan(chalk.bold(`${currentVersion}`))} is outdated.\nRun ${uiCommandReference(`${updateCommand}`)} to upgrade to version ${chalk.cyan(chalk.bold(`${latestVersion}`))}`,
|
|
2987
2991
|
},
|
|
2988
2992
|
autoUpdateCLI: {
|
|
2989
2993
|
updateAvailable: (latestVersion) => `There's a new HubSpot CLI version available! Updating to version ${chalk.bold(latestVersion)}`,
|
|
@@ -3008,7 +3012,9 @@ export const lib = {
|
|
|
3008
3012
|
noProjectConfig: 'No project config found. Please run this command from a project directory.',
|
|
3009
3013
|
profileNotFound: (profileName) => `Profile ${chalk.bold(profileName)} not found.`,
|
|
3010
3014
|
missingAccountId: (profileName) => `Profile ${chalk.bold(profileName)} is missing an account id.`,
|
|
3015
|
+
listedAccountNotFound: (accountId, profileName) => `The account ${uiAccountDescription(accountId)} is defined in your profile ${chalk.bold(profileName)}, but is missing in your config file`,
|
|
3011
3016
|
failedToLoadProfile: (profileName) => `Failed to load profile ${chalk.bold(profileName)}.`,
|
|
3017
|
+
profileNotValid: (profileName, errors) => `Profile "${profileName}" is not valid:\n\t- ${errors.join('\n\t- ')}`,
|
|
3012
3018
|
},
|
|
3013
3019
|
},
|
|
3014
3020
|
},
|
|
@@ -3099,9 +3105,6 @@ export const lib = {
|
|
|
3099
3105
|
legacyFileDetected: (filename, platformVersion) => `The ${chalk.bold(filename)} file is not supported on platform version ${chalk.bold(platformVersion)} and will be ignored.`,
|
|
3100
3106
|
},
|
|
3101
3107
|
},
|
|
3102
|
-
boxen: {
|
|
3103
|
-
failedToLoad: 'Failed to load boxen util.',
|
|
3104
|
-
},
|
|
3105
3108
|
importData: {
|
|
3106
3109
|
errors: {
|
|
3107
3110
|
incorrectAccountType: (derivedAccountId) => `The account ${uiAccountDescription(derivedAccountId)} is not a standard account, developer test account, or app developer account.`,
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, } from '@hubspot/project-parsing-lib';
|
|
2
|
+
import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, validateProfileVariables, } from '@hubspot/project-parsing-lib';
|
|
3
3
|
import { lib } from '../../lang/en.js';
|
|
4
4
|
import { uiBetaTag, uiLine } from '../ui/index.js';
|
|
5
5
|
import { uiLogger } from '../ui/logger.js';
|
|
6
|
-
import {
|
|
7
|
-
import { logProfileHeader, logProfileFooter, loadProfile,
|
|
6
|
+
import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
|
|
7
|
+
import { logProfileHeader, logProfileFooter, loadProfile, enforceProfileUsage, loadAndValidateProfile, validateProjectForProfile, } from '../projectProfiles.js';
|
|
8
|
+
import { handleTranslate } from '../projects/upload.js';
|
|
9
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
10
|
+
import { commands } from '../../lang/en.js';
|
|
8
11
|
// Mock dependencies
|
|
9
12
|
vi.mock('@hubspot/project-parsing-lib');
|
|
13
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
10
14
|
vi.mock('../ui');
|
|
11
15
|
vi.mock('../ui/logger');
|
|
12
16
|
vi.mock('../../lang/en');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error(`Process.exit called with code ${code}`);
|
|
16
|
-
});
|
|
17
|
+
vi.mock('../projects/upload');
|
|
18
|
+
vi.mock('../ui/SpinniesManager');
|
|
17
19
|
const mockedLoadHsProfileFile = loadHsProfileFile;
|
|
18
20
|
const mockedGetHsProfileFilename = getHsProfileFilename;
|
|
19
21
|
const mockedGetAllHsProfiles = getAllHsProfiles;
|
|
22
|
+
const mockedValidateProfileVariables = validateProfileVariables;
|
|
23
|
+
const mockedGetConfigAccountById = getConfigAccountById;
|
|
20
24
|
const mockedUiBetaTag = uiBetaTag;
|
|
21
25
|
const mockedUiLine = uiLine;
|
|
22
26
|
const mockedUiLogger = uiLogger;
|
|
@@ -68,62 +72,299 @@ describe('lib/projectProfiles', () => {
|
|
|
68
72
|
const mockProfile = {
|
|
69
73
|
accountId: 123,
|
|
70
74
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
it('should throw error when project config is missing', () => {
|
|
79
|
+
expect(() => loadProfile(null, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
|
|
75
80
|
});
|
|
76
|
-
it('should
|
|
81
|
+
it('should throw error when project dir is missing', () => {
|
|
82
|
+
expect(() => loadProfile(mockProjectConfig, null, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
|
|
83
|
+
});
|
|
84
|
+
it('should throw error when profile is not found', () => {
|
|
77
85
|
mockedLoadHsProfileFile.mockReturnValue(null);
|
|
78
86
|
const filename = 'test-profile.hsprofile';
|
|
79
87
|
mockedGetHsProfileFilename.mockReturnValue(filename);
|
|
80
|
-
|
|
81
|
-
expect(result).toBeUndefined();
|
|
82
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
|
|
88
|
+
expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
|
|
83
89
|
});
|
|
84
|
-
it('should
|
|
90
|
+
it('should throw error when profile has no account ID', () => {
|
|
85
91
|
mockedLoadHsProfileFile.mockReturnValue({});
|
|
86
92
|
const filename = 'test-profile.hsprofile';
|
|
87
93
|
mockedGetHsProfileFilename.mockReturnValue(filename);
|
|
88
|
-
|
|
89
|
-
expect(result).toBeUndefined();
|
|
90
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
|
|
94
|
+
expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
|
|
91
95
|
});
|
|
92
|
-
it('should
|
|
96
|
+
it('should throw error when profile loading fails', () => {
|
|
93
97
|
mockedLoadHsProfileFile.mockImplementation(() => {
|
|
94
98
|
throw new Error('Load failed');
|
|
95
99
|
});
|
|
96
100
|
const filename = 'test-profile.hsprofile';
|
|
97
101
|
mockedGetHsProfileFilename.mockReturnValue(filename);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(filename));
|
|
103
|
+
});
|
|
104
|
+
it('should throw error when account is not found in config', () => {
|
|
105
|
+
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
106
|
+
mockedGetConfigAccountById.mockImplementation(() => {
|
|
107
|
+
throw new Error('Account not found');
|
|
108
|
+
});
|
|
109
|
+
const filename = 'test-profile.hsprofile';
|
|
110
|
+
mockedGetHsProfileFilename.mockReturnValue(filename);
|
|
111
|
+
expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.listedAccountNotFound(mockProfile.accountId, filename));
|
|
101
112
|
});
|
|
102
113
|
it('should return profile when loading succeeds', () => {
|
|
103
114
|
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
115
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
116
|
+
accountId: mockProfile.accountId,
|
|
117
|
+
});
|
|
104
118
|
const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
|
|
105
119
|
expect(result).toEqual(mockProfile);
|
|
106
120
|
expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
|
|
121
|
+
expect(mockedGetConfigAccountById).toHaveBeenCalledWith(mockProfile.accountId);
|
|
107
122
|
});
|
|
108
123
|
});
|
|
109
|
-
describe('
|
|
124
|
+
describe('enforceProfileUsage()', () => {
|
|
110
125
|
const mockProjectConfig = {
|
|
111
126
|
srcDir: 'src',
|
|
112
127
|
name: 'test-project',
|
|
113
128
|
platformVersion: '1.0.0',
|
|
114
129
|
};
|
|
115
130
|
const mockProjectDir = '/test/project';
|
|
116
|
-
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
vi.clearAllMocks();
|
|
133
|
+
});
|
|
134
|
+
it('should not throw when no profiles exist', async () => {
|
|
117
135
|
mockedGetAllHsProfiles.mockResolvedValue([]);
|
|
118
|
-
await
|
|
119
|
-
expect(mockedUiLogger.error).not.toHaveBeenCalled();
|
|
120
|
-
expect(mockExit).not.toHaveBeenCalled();
|
|
136
|
+
await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).resolves.toBeUndefined();
|
|
121
137
|
});
|
|
122
|
-
it('should
|
|
138
|
+
it('should throw error when profiles exist', async () => {
|
|
123
139
|
mockedGetAllHsProfiles.mockResolvedValue(['profile1', 'profile2']);
|
|
124
|
-
await expect(
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
|
|
141
|
+
});
|
|
142
|
+
it('should not throw when project config is null', async () => {
|
|
143
|
+
await expect(enforceProfileUsage(null, mockProjectDir)).resolves.toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
it('should not throw when project dir is null', async () => {
|
|
146
|
+
await expect(enforceProfileUsage(mockProjectConfig, null)).resolves.toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('loadAndValidateProfile()', () => {
|
|
150
|
+
const mockProjectConfig = {
|
|
151
|
+
srcDir: 'src',
|
|
152
|
+
name: 'test-project',
|
|
153
|
+
platformVersion: '1.0.0',
|
|
154
|
+
};
|
|
155
|
+
const mockProjectDir = '/test/project';
|
|
156
|
+
const mockProfileName = 'test-profile';
|
|
157
|
+
const mockProfile = {
|
|
158
|
+
accountId: 123,
|
|
159
|
+
variables: {
|
|
160
|
+
key1: 'value1',
|
|
161
|
+
key2: 'value2',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
vi.clearAllMocks();
|
|
166
|
+
});
|
|
167
|
+
it('should enforce profile usage when no profile name provided', async () => {
|
|
168
|
+
mockedGetAllHsProfiles.mockResolvedValue([]);
|
|
169
|
+
const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined);
|
|
170
|
+
expect(result).toBeUndefined();
|
|
171
|
+
expect(mockedGetAllHsProfiles).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir));
|
|
172
|
+
});
|
|
173
|
+
it('should throw when profiles exist but no profile name provided', async () => {
|
|
174
|
+
mockedGetAllHsProfiles.mockResolvedValue(['profile1']);
|
|
175
|
+
await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
|
|
176
|
+
});
|
|
177
|
+
it('should load and return account ID when profile is valid', async () => {
|
|
178
|
+
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
179
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
180
|
+
accountId: mockProfile.accountId,
|
|
181
|
+
});
|
|
182
|
+
mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
|
|
183
|
+
mockedValidateProfileVariables.mockReturnValue({ success: true });
|
|
184
|
+
const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
|
|
185
|
+
expect(result).toBe(mockProfile.accountId);
|
|
186
|
+
expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
|
|
187
|
+
expect(mockedValidateProfileVariables).toHaveBeenCalledWith(mockProfile.variables, mockProfileName);
|
|
188
|
+
});
|
|
189
|
+
it('should log profile header and footer when not silent', async () => {
|
|
190
|
+
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
191
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
192
|
+
accountId: mockProfile.accountId,
|
|
193
|
+
});
|
|
194
|
+
mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
|
|
195
|
+
mockedValidateProfileVariables.mockReturnValue({ success: true });
|
|
196
|
+
await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, false);
|
|
197
|
+
expect(mockedUiBetaTag).toHaveBeenCalled();
|
|
198
|
+
expect(mockedUiLine).toHaveBeenCalled();
|
|
199
|
+
expect(mockedUiLogger.log).toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
it('should not log when silent is true', async () => {
|
|
202
|
+
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
203
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
204
|
+
accountId: mockProfile.accountId,
|
|
205
|
+
});
|
|
206
|
+
mockedValidateProfileVariables.mockReturnValue({ success: true });
|
|
207
|
+
await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, true);
|
|
208
|
+
expect(mockedUiBetaTag).not.toHaveBeenCalled();
|
|
209
|
+
expect(mockedUiLine).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
it('should throw error when profile variables are invalid', async () => {
|
|
212
|
+
const invalidProfile = {
|
|
213
|
+
accountId: 123,
|
|
214
|
+
variables: {
|
|
215
|
+
invalid: 'value',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
const validationErrors = ['Variable "invalid" is not allowed'];
|
|
219
|
+
mockedLoadHsProfileFile.mockReturnValue(invalidProfile);
|
|
220
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
221
|
+
accountId: invalidProfile.accountId,
|
|
222
|
+
});
|
|
223
|
+
mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
|
|
224
|
+
mockedValidateProfileVariables.mockReturnValue({
|
|
225
|
+
success: false,
|
|
226
|
+
errors: validationErrors,
|
|
227
|
+
});
|
|
228
|
+
await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName)).rejects.toThrow(lib.projectProfiles.loadProfile.errors.profileNotValid('test-profile.hsprofile', validationErrors));
|
|
229
|
+
});
|
|
230
|
+
it('should not validate when profile has no variables', async () => {
|
|
231
|
+
const profileWithoutVars = {
|
|
232
|
+
accountId: 123,
|
|
233
|
+
};
|
|
234
|
+
mockedLoadHsProfileFile.mockReturnValue(profileWithoutVars);
|
|
235
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
236
|
+
accountId: profileWithoutVars.accountId,
|
|
237
|
+
});
|
|
238
|
+
mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
|
|
239
|
+
const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
|
|
240
|
+
expect(result).toBe(profileWithoutVars.accountId);
|
|
241
|
+
expect(mockedValidateProfileVariables).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('validateProjectForProfile()', () => {
|
|
245
|
+
const mockProjectConfig = {
|
|
246
|
+
srcDir: 'src',
|
|
247
|
+
name: 'test-project',
|
|
248
|
+
platformVersion: '2025.2',
|
|
249
|
+
};
|
|
250
|
+
const mockProjectDir = '/test/project';
|
|
251
|
+
const mockProfileName = 'test-profile';
|
|
252
|
+
const mockDerivedAccountId = 123;
|
|
253
|
+
const mockProfileFilename = 'test-profile.hsprofile';
|
|
254
|
+
const mockProfile = {
|
|
255
|
+
accountId: mockDerivedAccountId,
|
|
256
|
+
};
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
vi.clearAllMocks();
|
|
259
|
+
mockedGetHsProfileFilename.mockReturnValue(mockProfileFilename);
|
|
260
|
+
vi.mocked(SpinniesManager.init);
|
|
261
|
+
vi.mocked(SpinniesManager.add);
|
|
262
|
+
vi.mocked(SpinniesManager.succeed);
|
|
263
|
+
vi.mocked(SpinniesManager.fail);
|
|
264
|
+
// Mock dependencies for loadAndValidateProfile
|
|
265
|
+
mockedGetAllHsProfiles.mockResolvedValue([]);
|
|
266
|
+
mockedLoadHsProfileFile.mockReturnValue(mockProfile);
|
|
267
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
268
|
+
accountId: mockDerivedAccountId,
|
|
269
|
+
});
|
|
270
|
+
mockedValidateProfileVariables.mockReturnValue({ success: true });
|
|
271
|
+
vi.mocked(handleTranslate).mockResolvedValue(undefined);
|
|
272
|
+
});
|
|
273
|
+
it('should return empty array when validation succeeds', async () => {
|
|
274
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
275
|
+
expect(result).toEqual([]);
|
|
276
|
+
expect(SpinniesManager.init).toHaveBeenCalled();
|
|
277
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
278
|
+
text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
|
|
279
|
+
indent: 0,
|
|
280
|
+
});
|
|
281
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
282
|
+
text: commands.project.validate.spinners.profileValidationSucceeded(mockProfileFilename),
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
it('should call handleTranslate with profile account ID from profile', async () => {
|
|
286
|
+
await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
287
|
+
expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, mockDerivedAccountId, false, mockProfileName);
|
|
288
|
+
});
|
|
289
|
+
it('should call handleTranslate with different profile account ID when profile has different ID', async () => {
|
|
290
|
+
const profileAccountId = 456;
|
|
291
|
+
const profileWithDifferentId = {
|
|
292
|
+
accountId: profileAccountId,
|
|
293
|
+
};
|
|
294
|
+
mockedLoadHsProfileFile.mockReturnValue(profileWithDifferentId);
|
|
295
|
+
mockedGetConfigAccountById.mockReturnValue({
|
|
296
|
+
accountId: profileAccountId,
|
|
297
|
+
});
|
|
298
|
+
await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
299
|
+
expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, profileAccountId, false, mockProfileName);
|
|
300
|
+
});
|
|
301
|
+
it('should return error when profile has no accountId', async () => {
|
|
302
|
+
// @ts-expect-error causing an error on purpose
|
|
303
|
+
const profileWithoutId = {};
|
|
304
|
+
mockedLoadHsProfileFile.mockReturnValue(profileWithoutId);
|
|
305
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
306
|
+
expect(result.length).toBeGreaterThan(0);
|
|
307
|
+
expect(SpinniesManager.fail).toHaveBeenCalled();
|
|
308
|
+
expect(handleTranslate).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
it('should indent spinners when indentSpinners is true', async () => {
|
|
311
|
+
await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, true);
|
|
312
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
313
|
+
text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
|
|
314
|
+
indent: 4,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
it('should not indent spinners when indentSpinners is false', async () => {
|
|
318
|
+
await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, false);
|
|
319
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
320
|
+
text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
|
|
321
|
+
indent: 0,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
it('should return error array when profile loading fails', async () => {
|
|
325
|
+
mockedLoadHsProfileFile.mockReturnValue(null);
|
|
326
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
327
|
+
expect(result.length).toBeGreaterThan(0);
|
|
328
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
329
|
+
text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
|
|
330
|
+
});
|
|
331
|
+
expect(handleTranslate).not.toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
it('should return error when profile file loading throws', async () => {
|
|
334
|
+
mockedLoadHsProfileFile.mockImplementation(() => {
|
|
335
|
+
throw new Error('Failed to load profile file');
|
|
336
|
+
});
|
|
337
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
338
|
+
expect(result.length).toBeGreaterThan(0);
|
|
339
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
340
|
+
text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
|
|
341
|
+
});
|
|
342
|
+
expect(handleTranslate).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
it('should return error array when translation fails', async () => {
|
|
345
|
+
const error = new Error('Translation failed');
|
|
346
|
+
vi.mocked(handleTranslate).mockRejectedValue(error);
|
|
347
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
348
|
+
expect(result).toHaveLength(2);
|
|
349
|
+
expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
|
|
350
|
+
expect(result[1]).toBe(error);
|
|
351
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
|
|
352
|
+
text: commands.project.validate.spinners.invalidWithProfile(mockProfileFilename, mockProjectConfig.name),
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
it('should return string error when translation fails with non-Error', async () => {
|
|
356
|
+
const error = 'Translation error';
|
|
357
|
+
vi.mocked(handleTranslate).mockRejectedValue(error);
|
|
358
|
+
const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
|
|
359
|
+
expect(result).toHaveLength(2);
|
|
360
|
+
expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
|
|
361
|
+
expect(result[1]).toBe(error);
|
|
362
|
+
});
|
|
363
|
+
it('should use correct spinner name based on profile name', async () => {
|
|
364
|
+
const customProfileName = 'custom-profile';
|
|
365
|
+
await validateProjectForProfile(mockProjectConfig, mockProjectDir, customProfileName, mockDerivedAccountId);
|
|
366
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
|
|
367
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
|
|
127
368
|
});
|
|
128
369
|
});
|
|
129
370
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import updateNotifier from 'update-notifier';
|
|
2
2
|
import { getConfig } from '@hubspot/local-dev-lib/config';
|
|
3
3
|
import { pkg } from '../jsonLoader.js';
|
|
4
|
-
import { UI_COLORS } from '../ui/index.js';
|
|
5
4
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
6
5
|
import { lib } from '../../lang/en.js';
|
|
7
6
|
import { DEFAULT_PACKAGE_MANAGER, isGloballyInstalled, executeInstall, } from '../npm.js';
|
|
8
7
|
import { debugError } from '../errorHandlers/index.js';
|
|
9
8
|
import { uiLogger } from '../ui/logger.js';
|
|
10
9
|
import { isTargetedCommand } from './commandTargetingUtils.js';
|
|
10
|
+
import { renderInline } from '../../ui/index.js';
|
|
11
|
+
import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
11
12
|
// Default behavior is to check for notifications at most once per day
|
|
12
13
|
// update-notifier stores the last checked date in the user's home directory
|
|
13
14
|
const notifier = updateNotifier({
|
|
@@ -16,24 +17,16 @@ const notifier = updateNotifier({
|
|
|
16
17
|
shouldNotifyInNpmScript: true,
|
|
17
18
|
});
|
|
18
19
|
const CMS_CLI_PACKAGE_NAME = '@hubspot/cms-cli';
|
|
19
|
-
async function updateNotification() {
|
|
20
|
-
|
|
20
|
+
async function updateNotification(currentVersion, latestVersion, updateCommand) {
|
|
21
|
+
await renderInline(getWarningBox({
|
|
22
|
+
title: pkg.name === CMS_CLI_PACKAGE_NAME
|
|
23
|
+
? ''
|
|
24
|
+
: lib.middleware.updateNotification.notifyTitle,
|
|
21
25
|
message: pkg.name === CMS_CLI_PACKAGE_NAME
|
|
22
26
|
? lib.middleware.updateNotification.cmsUpdateNotification(CMS_CLI_PACKAGE_NAME)
|
|
23
|
-
: lib.middleware.updateNotification.cliUpdateNotification,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
borderColor: UI_COLORS.MARIGOLD_DARK,
|
|
27
|
-
margin: 1,
|
|
28
|
-
padding: 1,
|
|
29
|
-
textAlignment: 'center',
|
|
30
|
-
borderStyle: 'round',
|
|
31
|
-
title: pkg.name === CMS_CLI_PACKAGE_NAME
|
|
32
|
-
? undefined
|
|
33
|
-
: lib.middleware.updateNotification.notifyTitle,
|
|
34
|
-
},
|
|
35
|
-
isGlobal: await isGloballyInstalled('hs'),
|
|
36
|
-
});
|
|
27
|
+
: lib.middleware.updateNotification.cliUpdateNotification(currentVersion, updateCommand, latestVersion),
|
|
28
|
+
textCentered: true,
|
|
29
|
+
}));
|
|
37
30
|
}
|
|
38
31
|
const SKIP_AUTO_UPDATE_COMMANDS = {
|
|
39
32
|
config: {
|
|
@@ -44,8 +37,16 @@ const preventAutoUpdateForCommand = (commandParts) => {
|
|
|
44
37
|
return isTargetedCommand(commandParts, SKIP_AUTO_UPDATE_COMMANDS);
|
|
45
38
|
};
|
|
46
39
|
export async function autoUpdateCLI(argv) {
|
|
47
|
-
// This lets us back to default update-notifier behavior
|
|
48
40
|
let showManualInstallHelp = true;
|
|
41
|
+
let isGlobalInstall = null;
|
|
42
|
+
const checkGlobalInstall = async () => {
|
|
43
|
+
if (isGlobalInstall === null) {
|
|
44
|
+
isGlobalInstall =
|
|
45
|
+
(await isGloballyInstalled(DEFAULT_PACKAGE_MANAGER)) &&
|
|
46
|
+
(await isGloballyInstalled('hs'));
|
|
47
|
+
}
|
|
48
|
+
return isGlobalInstall;
|
|
49
|
+
};
|
|
49
50
|
let config;
|
|
50
51
|
try {
|
|
51
52
|
config = getConfig();
|
|
@@ -71,8 +72,7 @@ export async function autoUpdateCLI(argv) {
|
|
|
71
72
|
text: lib.middleware.autoUpdateCLI.updateAvailable(notifier.update.latest),
|
|
72
73
|
});
|
|
73
74
|
try {
|
|
74
|
-
if (
|
|
75
|
-
(await isGloballyInstalled('hs'))) {
|
|
75
|
+
if (await checkGlobalInstall()) {
|
|
76
76
|
await executeInstall(['@hubspot/cli@latest'], '-g');
|
|
77
77
|
showManualInstallHelp = false;
|
|
78
78
|
SpinniesManager.succeed('cliAutoUpdate', {
|
|
@@ -95,7 +95,10 @@ export async function autoUpdateCLI(argv) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
if (showManualInstallHelp
|
|
99
|
-
|
|
98
|
+
if (showManualInstallHelp &&
|
|
99
|
+
notifier.update &&
|
|
100
|
+
process.stdout.isTTY &&
|
|
101
|
+
!notifier.update.current.includes('-')) {
|
|
102
|
+
await updateNotification(notifier.update.current, notifier.update.latest, `npm i ${(await checkGlobalInstall()) ? '-g' : ''} @hubspot/cli`);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
@@ -2,7 +2,6 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
|
|
3
3
|
import { debugError } from '../errorHandlers/index.js';
|
|
4
4
|
import { pkg } from '../jsonLoader.js';
|
|
5
|
-
import { logInBox } from '../ui/boxen.js';
|
|
6
5
|
import { renderInline } from '../../ui/index.js';
|
|
7
6
|
import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
8
7
|
/*
|
|
@@ -100,20 +99,10 @@ async function logFireAlarms(accountId, command, version) {
|
|
|
100
99
|
}
|
|
101
100
|
return acc;
|
|
102
101
|
}, '');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
title: 'Notifications',
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
await renderInline(getWarningBox({
|
|
113
|
-
title: 'Notifications',
|
|
114
|
-
message: notifications,
|
|
115
|
-
}));
|
|
116
|
-
}
|
|
102
|
+
await renderInline(getWarningBox({
|
|
103
|
+
title: 'Notifications',
|
|
104
|
+
message: notifications,
|
|
105
|
+
}));
|
|
117
106
|
}
|
|
118
107
|
}
|
|
119
108
|
export async function checkFireAlarms(argv) {
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { HsProfileFile } from '@hubspot/project-parsing-lib/src/lib/types.js';
|
|
|
2
2
|
import { ProjectConfig } from '../types/Projects.js';
|
|
3
3
|
export declare function logProfileHeader(profileName: string): void;
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
|
-
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile |
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null,
|
|
5
|
+
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | never;
|
|
6
|
+
export declare function enforceProfileUsage(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string | undefined, silent?: boolean): Promise<number | undefined>;
|
|
8
|
+
export declare function validateProjectForProfile(projectConfig: ProjectConfig, projectDir: string, profileName: string, derivedAccountId: number, indentSpinners?: boolean): Promise<(string | Error)[]>;
|