@hubspot/cli 7.9.0-beta.2 → 7.9.0-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.
@@ -1,5 +1,5 @@
1
1
  import util from 'util';
2
- import { installPackages, getProjectPackageJsonLocations, } from '../dependencyManagement.js';
2
+ import { installPackages, updatePackages, getProjectPackageJsonLocations, isPackageInstalled, } from '../dependencyManagement.js';
3
3
  import { walk } from '@hubspot/local-dev-lib/fs';
4
4
  import path from 'path';
5
5
  import { getProjectConfig } from '../projects/config.js';
@@ -105,6 +105,54 @@ describe('lib/dependencyManagement', () => {
105
105
  cwd: extensionsDir,
106
106
  });
107
107
  });
108
+ it('should install packages as dev dependencies when dev flag is true', async () => {
109
+ const packages = ['eslint', 'prettier'];
110
+ await installPackages({ packages, installLocations, dev: true });
111
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
112
+ for (const location of installLocations) {
113
+ expect(execMock).toHaveBeenCalledWith(`npm install --save-dev eslint prettier`, {
114
+ cwd: location,
115
+ });
116
+ }
117
+ });
118
+ it('should install packages as regular dependencies when dev flag is false', async () => {
119
+ const packages = ['react', 'react-dom'];
120
+ await installPackages({ packages, installLocations, dev: false });
121
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
122
+ for (const location of installLocations) {
123
+ expect(execMock).toHaveBeenCalledWith(`npm install react react-dom`, {
124
+ cwd: location,
125
+ });
126
+ }
127
+ });
128
+ it('should install packages as regular dependencies when dev flag is not provided', async () => {
129
+ const packages = ['axios'];
130
+ await installPackages({ packages, installLocations });
131
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
132
+ for (const location of installLocations) {
133
+ expect(execMock).toHaveBeenCalledWith(`npm install axios`, {
134
+ cwd: location,
135
+ });
136
+ }
137
+ });
138
+ it('should not use --save-dev flag when dev is true but no packages are provided', async () => {
139
+ await installPackages({ installLocations, dev: true });
140
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
141
+ for (const location of installLocations) {
142
+ expect(execMock).toHaveBeenCalledWith(`npm install `, {
143
+ cwd: location,
144
+ });
145
+ }
146
+ });
147
+ it('should not use --save-dev flag when dev is true but packages array is empty', async () => {
148
+ await installPackages({ packages: [], installLocations, dev: true });
149
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
150
+ for (const location of installLocations) {
151
+ expect(execMock).toHaveBeenCalledWith(`npm install `, {
152
+ cwd: location,
153
+ });
154
+ }
155
+ });
108
156
  it('should throw an error when installing the dependencies fails', async () => {
109
157
  execMock = vi.fn().mockImplementation(command => {
110
158
  if (command === 'npm --version') {
@@ -133,6 +181,92 @@ describe('lib/dependencyManagement', () => {
133
181
  });
134
182
  });
135
183
  });
184
+ describe('updatePackages()', () => {
185
+ it('should setup a loading spinner', async () => {
186
+ const packages = ['package1', 'package2'];
187
+ await updatePackages({ packages, installLocations });
188
+ expect(SpinniesManager.init).toHaveBeenCalledTimes(installLocations.length);
189
+ expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
190
+ expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
191
+ });
192
+ it('should update the provided packages in all the provided install locations', async () => {
193
+ const packages = ['package1', 'package2'];
194
+ await updatePackages({ packages, installLocations });
195
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
196
+ expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
197
+ expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
198
+ for (const location of installLocations) {
199
+ expect(execMock).toHaveBeenCalledWith(`npm update package1 package2`, {
200
+ cwd: location,
201
+ });
202
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
203
+ text: `Updating [package1, package2] in ${location}`,
204
+ });
205
+ expect(SpinniesManager.succeed).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
206
+ text: `Updated dependencies in ${location}`,
207
+ });
208
+ }
209
+ });
210
+ it('should use the provided install locations', async () => {
211
+ await updatePackages({ installLocations });
212
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
213
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
214
+ cwd: appFunctionsDir,
215
+ });
216
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
217
+ cwd: extensionsDir,
218
+ });
219
+ });
220
+ it('should locate the projects package.json files when install locations is not provided', async () => {
221
+ const installLocations = [
222
+ path.join(appFunctionsDir, 'package.json'),
223
+ path.join(extensionsDir, 'package.json'),
224
+ ];
225
+ mockedWalk.mockResolvedValue(installLocations);
226
+ mockedGetProjectConfig.mockResolvedValue({
227
+ projectDir,
228
+ projectConfig: {
229
+ srcDir,
230
+ },
231
+ });
232
+ await updatePackages({});
233
+ // It's called once per each install location, plus once to check if npm installed
234
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length + 1);
235
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
236
+ cwd: appFunctionsDir,
237
+ });
238
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
239
+ cwd: extensionsDir,
240
+ });
241
+ });
242
+ it('should throw an error when updating the dependencies fails', async () => {
243
+ execMock = vi.fn().mockImplementation(command => {
244
+ if (command === 'npm --version') {
245
+ return;
246
+ }
247
+ throw new Error('OH NO');
248
+ });
249
+ util.promisify = mockedPromisify(execMock);
250
+ // Mock walk to return the directory paths instead of package.json paths
251
+ mockedWalk.mockResolvedValue([appFunctionsDir, extensionsDir]);
252
+ mockedFs.existsSync.mockImplementation(filePath => {
253
+ const pathStr = filePath.toString();
254
+ if (pathStr === projectDir ||
255
+ pathStr === path.join(projectDir, srcDir)) {
256
+ return true;
257
+ }
258
+ return false;
259
+ });
260
+ await expect(() => updatePackages({ installLocations: [appFunctionsDir, extensionsDir] })).rejects.toThrowError(`Updating dependencies for ${appFunctionsDir} failed`);
261
+ expect(SpinniesManager.fail).toHaveBeenCalledTimes(installLocations.length);
262
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${appFunctionsDir}`, {
263
+ text: `Updating dependencies for ${appFunctionsDir} failed`,
264
+ });
265
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${extensionsDir}`, {
266
+ text: `Updating dependencies for ${extensionsDir} failed`,
267
+ });
268
+ });
269
+ });
136
270
  describe('getProjectPackageJsonFiles()', () => {
137
271
  it('should throw an error when ran outside the boundary of a project', async () => {
138
272
  mockedGetProjectConfig.mockResolvedValue({});
@@ -149,6 +283,30 @@ describe('lib/dependencyManagement', () => {
149
283
  mockedFs.existsSync.mockReturnValueOnce(false);
150
284
  await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
151
285
  });
286
+ it('should throw "install" error message when isUpdate=false and no package.json files found', async () => {
287
+ mockedWalk.mockResolvedValue([]);
288
+ mockedFs.existsSync.mockImplementation(filePath => {
289
+ const pathStr = filePath.toString();
290
+ if (pathStr === projectDir ||
291
+ pathStr === path.join(projectDir, srcDir)) {
292
+ return true;
293
+ }
294
+ return false;
295
+ });
296
+ await expect(() => getProjectPackageJsonLocations(undefined, false)).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
297
+ });
298
+ it('should throw "update" error message when isUpdate=true and no package.json files found', async () => {
299
+ mockedWalk.mockResolvedValue([]);
300
+ mockedFs.existsSync.mockImplementation(filePath => {
301
+ const pathStr = filePath.toString();
302
+ if (pathStr === projectDir ||
303
+ pathStr === path.join(projectDir, srcDir)) {
304
+ return true;
305
+ }
306
+ return false;
307
+ });
308
+ await expect(() => getProjectPackageJsonLocations(undefined, true)).rejects.toThrowError(new RegExp(`No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files.`));
309
+ });
152
310
  it('should ignore package.json files in certain directories', async () => {
153
311
  const nodeModulesDir = path.join(appDir, 'node_modules');
154
312
  const viteDir = path.join(appDir, '.vite');
@@ -172,4 +330,118 @@ describe('lib/dependencyManagement', () => {
172
330
  expect(actual).toEqual([appFunctionsDir, extensionsDir]);
173
331
  });
174
332
  });
333
+ describe('isPackageInstalled()', () => {
334
+ const testDir = '/test/directory';
335
+ const readFileSyncSpy = vi.spyOn(fs, 'readFileSync');
336
+ const existsSyncSpy = vi.spyOn(fs, 'existsSync');
337
+ function mockNodeModulesExists(packageName, exists = true) {
338
+ existsSyncSpy.mockImplementation(filePath => {
339
+ const pathStr = filePath.toString();
340
+ return (exists && pathStr === path.join(testDir, 'node_modules', packageName));
341
+ });
342
+ }
343
+ beforeEach(() => {
344
+ vi.clearAllMocks();
345
+ readFileSyncSpy.mockReset();
346
+ existsSyncSpy.mockReset();
347
+ });
348
+ it('should return true if package is in dependencies and in node_modules', () => {
349
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
350
+ dependencies: {
351
+ eslint: '^9.0.0',
352
+ },
353
+ }));
354
+ mockNodeModulesExists('eslint', true);
355
+ const result = isPackageInstalled(testDir, 'eslint');
356
+ expect(result).toBe(true);
357
+ expect(readFileSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'package.json'), 'utf-8');
358
+ expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', 'eslint'));
359
+ });
360
+ it('should return true if package is in devDependencies and in node_modules', () => {
361
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
362
+ devDependencies: {
363
+ prettier: '^3.0.0',
364
+ },
365
+ }));
366
+ mockNodeModulesExists('prettier', true);
367
+ const result = isPackageInstalled(testDir, 'prettier');
368
+ expect(result).toBe(true);
369
+ });
370
+ it('should return false if package is in package.json but not in node_modules', () => {
371
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
372
+ dependencies: {
373
+ react: '^18.0.0',
374
+ },
375
+ }));
376
+ mockNodeModulesExists('react', false);
377
+ const result = isPackageInstalled(testDir, 'react');
378
+ expect(result).toBe(false);
379
+ });
380
+ it('should return false if package is not in package.json but is in node_modules', () => {
381
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
382
+ dependencies: {
383
+ typescript: '^5.0.0',
384
+ },
385
+ }));
386
+ mockNodeModulesExists('lodash', true);
387
+ const result = isPackageInstalled(testDir, 'lodash');
388
+ expect(result).toBe(false);
389
+ });
390
+ it('should return false if package is not in package.json and not in node_modules', () => {
391
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
392
+ dependencies: {},
393
+ }));
394
+ mockNodeModulesExists('nonexistent-package', false);
395
+ const result = isPackageInstalled(testDir, 'nonexistent-package');
396
+ expect(result).toBe(false);
397
+ });
398
+ it('should return false if package.json cannot be read', () => {
399
+ readFileSyncSpy.mockImplementationOnce(() => {
400
+ throw new Error('File not found');
401
+ });
402
+ const result = isPackageInstalled(testDir, 'eslint');
403
+ expect(result).toBe(false);
404
+ });
405
+ it('should return false if package.json has invalid JSON', () => {
406
+ readFileSyncSpy.mockReturnValueOnce('invalid json{');
407
+ const result = isPackageInstalled(testDir, 'eslint');
408
+ expect(result).toBe(false);
409
+ });
410
+ it('should return false if checking node_modules throws an error', () => {
411
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
412
+ dependencies: {
413
+ eslint: '^9.0.0',
414
+ },
415
+ }));
416
+ existsSyncSpy.mockImplementation(() => {
417
+ throw new Error('Permission denied');
418
+ });
419
+ const result = isPackageInstalled(testDir, 'eslint');
420
+ expect(result).toBe(false);
421
+ });
422
+ it('should handle scoped packages correctly', () => {
423
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
424
+ dependencies: {
425
+ '@typescript-eslint/parser': '^8.0.0',
426
+ },
427
+ }));
428
+ mockNodeModulesExists('@typescript-eslint/parser', true);
429
+ const result = isPackageInstalled(testDir, '@typescript-eslint/parser');
430
+ expect(result).toBe(true);
431
+ expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', '@typescript-eslint/parser'));
432
+ });
433
+ it('should check both dependencies and devDependencies', () => {
434
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
435
+ dependencies: {
436
+ react: '^18.0.0',
437
+ },
438
+ devDependencies: {
439
+ eslint: '^9.0.0',
440
+ },
441
+ }));
442
+ mockNodeModulesExists('eslint', true);
443
+ const result = isPackageInstalled(testDir, 'eslint');
444
+ expect(result).toBe(true);
445
+ });
446
+ });
175
447
  });
package/lib/commonOpts.js CHANGED
@@ -149,16 +149,13 @@ export function setCLILogLevel(options) {
149
149
  setLogLevel(LOG_LEVEL.ERROR);
150
150
  SpinniesManager.setDisableOutput(true);
151
151
  }
152
- else if (debug) {
152
+ else if (debug || networkDebug) {
153
153
  setLogLevel(LOG_LEVEL.DEBUG);
154
+ process.env.HUBSPOT_NETWORK_LOGGING = 'true';
154
155
  }
155
156
  else {
156
157
  setLogLevel(LOG_LEVEL.LOG);
157
158
  }
158
- if (networkDebug) {
159
- process.env.HUBSPOT_NETWORK_LOGGING = 'true';
160
- setLogLevel(LOG_LEVEL.DEBUG);
161
- }
162
159
  }
163
160
  export function getCommandName(argv) {
164
161
  return String(argv && argv._ && argv._[0]) || '';
@@ -1,6 +1,12 @@
1
- export declare function installPackages({ packages, installLocations, }: {
1
+ export declare function installPackages({ packages, installLocations, dev, }: {
2
2
  packages?: string[];
3
3
  installLocations?: string[];
4
+ dev?: boolean;
4
5
  }): Promise<void>;
5
- export declare function getProjectPackageJsonLocations(dir?: string): Promise<string[]>;
6
+ export declare function updatePackages({ packages, installLocations, }: {
7
+ packages?: string[];
8
+ installLocations?: string[];
9
+ }): Promise<void>;
10
+ export declare function getProjectPackageJsonLocations(dir?: string, isUpdate?: boolean): Promise<string[]>;
11
+ export declare function isPackageInstalled(directory: string, packageName: string): boolean;
6
12
  export declare function hasMissingPackages(directory: string): Promise<boolean>;
@@ -6,19 +6,21 @@ import { walk } from '@hubspot/local-dev-lib/fs';
6
6
  import { getProjectConfig } from './projects/config.js';
7
7
  import { commands } from '../lang/en.js';
8
8
  import SpinniesManager from './ui/SpinniesManager.js';
9
- import { isGloballyInstalled, executeInstall, DEFAULT_PACKAGE_MANAGER, } from './npm.js';
9
+ import { isGloballyInstalled, executeInstall, executeUpdate, DEFAULT_PACKAGE_MANAGER, } from './npm.js';
10
10
  class NoPackageJsonFilesError extends Error {
11
- constructor(projectName) {
12
- super(commands.project.installDeps.noPackageJsonInProject(projectName));
11
+ constructor(projectName, isUpdate = false) {
12
+ super(isUpdate
13
+ ? commands.project.updateDeps.noPackageJsonInProject(projectName)
14
+ : commands.project.installDeps.noPackageJsonInProject(projectName));
13
15
  }
14
16
  }
15
- export async function installPackages({ packages, installLocations, }) {
17
+ export async function installPackages({ packages, installLocations, dev = false, }) {
16
18
  const installDirs = installLocations || (await getProjectPackageJsonLocations());
17
19
  await Promise.all(installDirs.map(async (dir) => {
18
- await installPackagesInDirectory(dir, packages);
20
+ await installPackagesInDirectory(dir, packages, dev);
19
21
  }));
20
22
  }
21
- async function installPackagesInDirectory(directory, packages) {
23
+ async function installPackagesInDirectory(directory, packages, dev = false) {
22
24
  const spinner = `installingDependencies-${directory}`;
23
25
  const relativeDir = path.relative(process.cwd(), directory);
24
26
  SpinniesManager.init();
@@ -28,7 +30,8 @@ async function installPackagesInDirectory(directory, packages) {
28
30
  : commands.project.installDeps.installingDependencies(relativeDir),
29
31
  });
30
32
  try {
31
- await executeInstall(packages, null, { cwd: directory });
33
+ const flags = dev && packages && packages.length > 0 ? '--save-dev' : null;
34
+ await executeInstall(packages, flags, { cwd: directory });
32
35
  SpinniesManager.succeed(spinner, {
33
36
  text: commands.project.installDeps.installationSuccessful(relativeDir),
34
37
  });
@@ -42,26 +45,60 @@ async function installPackagesInDirectory(directory, packages) {
42
45
  });
43
46
  }
44
47
  }
45
- export async function getProjectPackageJsonLocations(dir) {
48
+ export async function updatePackages({ packages, installLocations, }) {
49
+ const installDirs = installLocations || (await getProjectPackageJsonLocations(undefined, true));
50
+ await Promise.all(installDirs.map(async (dir) => {
51
+ await updatePackagesInDirectory(dir, packages);
52
+ }));
53
+ }
54
+ async function updatePackagesInDirectory(directory, packages) {
55
+ const spinner = `updatingDependencies-${directory}`;
56
+ const relativeDir = path.relative(process.cwd(), directory);
57
+ SpinniesManager.init();
58
+ SpinniesManager.add(spinner, {
59
+ text: packages && packages.length
60
+ ? commands.project.updateDeps.updatingDependenciesToLocation(`[${packages.join(', ')}]`, relativeDir)
61
+ : commands.project.updateDeps.updatingDependencies(relativeDir),
62
+ });
63
+ try {
64
+ await executeUpdate(packages, null, { cwd: directory });
65
+ SpinniesManager.succeed(spinner, {
66
+ text: commands.project.updateDeps.updateSuccessful(relativeDir),
67
+ });
68
+ }
69
+ catch (e) {
70
+ SpinniesManager.fail(spinner, {
71
+ text: commands.project.updateDeps.updatingDependenciesFailed(relativeDir),
72
+ });
73
+ throw new Error(commands.project.updateDeps.updatingDependenciesFailed(relativeDir), {
74
+ cause: e,
75
+ });
76
+ }
77
+ }
78
+ export async function getProjectPackageJsonLocations(dir, isUpdate = false) {
46
79
  const projectConfig = await getProjectConfig(dir);
47
80
  if (!projectConfig ||
48
81
  !projectConfig.projectDir ||
49
82
  !projectConfig.projectConfig) {
50
- throw new Error(commands.project.installDeps.noProjectConfig);
83
+ throw new Error(isUpdate
84
+ ? commands.project.updateDeps.noProjectConfig
85
+ : commands.project.installDeps.noProjectConfig);
51
86
  }
52
87
  const { projectDir, projectConfig: { srcDir, name }, } = projectConfig;
53
88
  if (!(await isGloballyInstalled(DEFAULT_PACKAGE_MANAGER))) {
54
- throw new Error(commands.project.installDeps.packageManagerNotInstalled(DEFAULT_PACKAGE_MANAGER));
89
+ throw new Error(isUpdate
90
+ ? commands.project.updateDeps.packageManagerNotInstalled(DEFAULT_PACKAGE_MANAGER)
91
+ : commands.project.installDeps.packageManagerNotInstalled(DEFAULT_PACKAGE_MANAGER));
55
92
  }
56
93
  if (!fs.existsSync(projectConfig.projectDir) ||
57
94
  !fs.existsSync(path.join(projectDir, srcDir))) {
58
- throw new NoPackageJsonFilesError(name);
95
+ throw new NoPackageJsonFilesError(name, isUpdate);
59
96
  }
60
97
  const packageJsonFiles = (await walk(path.join(projectDir, srcDir))).filter(file => file.includes('package.json') &&
61
98
  !file.includes('node_modules') &&
62
99
  !file.includes('.vite'));
63
100
  if (packageJsonFiles.length === 0) {
64
- throw new NoPackageJsonFilesError(name);
101
+ throw new NoPackageJsonFilesError(name, isUpdate);
65
102
  }
66
103
  const packageParentDirs = [];
67
104
  packageJsonFiles.forEach(packageJsonFile => {
@@ -70,6 +107,32 @@ export async function getProjectPackageJsonLocations(dir) {
70
107
  });
71
108
  return packageParentDirs;
72
109
  }
110
+ function isPackageInPackageJson(directory, packageName) {
111
+ const packageJsonPath = path.join(directory, 'package.json');
112
+ try {
113
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
114
+ const packageJson = JSON.parse(packageJsonContent);
115
+ return !!((packageJson.dependencies && packageJson.dependencies[packageName]) ||
116
+ (packageJson.devDependencies && packageJson.devDependencies[packageName]));
117
+ }
118
+ catch (error) {
119
+ return false;
120
+ }
121
+ }
122
+ function isPackageInNodeModules(directory, packageName) {
123
+ const packagePath = path.join(directory, 'node_modules', packageName);
124
+ try {
125
+ return fs.existsSync(packagePath);
126
+ }
127
+ catch (error) {
128
+ return false;
129
+ }
130
+ }
131
+ export function isPackageInstalled(directory, packageName) {
132
+ const inPackageJson = isPackageInPackageJson(directory, packageName);
133
+ const actuallyInstalled = isPackageInNodeModules(directory, packageName);
134
+ return inPackageJson && actuallyInstalled;
135
+ }
73
136
  export async function hasMissingPackages(directory) {
74
137
  const exec = util.promisify(execAsync);
75
138
  const { stdout } = await exec(`npm install --ignore-scripts --dry-run`, {
package/lib/npm.d.ts CHANGED
@@ -7,3 +7,6 @@ export declare function getLatestCliVersion(): Promise<{
7
7
  export declare function executeInstall(packages?: string[], flags?: string | null, options?: {
8
8
  cwd?: string;
9
9
  }): Promise<void>;
10
+ export declare function executeUpdate(packages?: string[], flags?: string | null, options?: {
11
+ cwd?: string;
12
+ }): Promise<void>;
package/lib/npm.js CHANGED
@@ -25,3 +25,9 @@ export async function executeInstall(packages = [], flags, options) {
25
25
  const exec = util.promisify(execAsync);
26
26
  await exec(installCommand, options);
27
27
  }
28
+ export async function executeUpdate(packages = [], flags, options) {
29
+ const updateCommand = `${DEFAULT_PACKAGE_MANAGER} update${flags ? ` ${flags}` : ''} ${packages.join(' ')}`;
30
+ uiLogger.debug('Running', updateCommand);
31
+ const exec = util.promisify(execAsync);
32
+ await exec(updateCommand, options);
33
+ }
@@ -8,10 +8,14 @@ describe('platformVersion', () => {
8
8
  expect(isV2Project('2025.2')).toBe(true);
9
9
  });
10
10
  it('returns true if platform version is greater than the minimum', () => {
11
- expect(isV2Project('2026.2')).toBe(true);
11
+ expect(isV2Project('2025.3')).toBe(true);
12
+ expect(isV2Project('2026.03')).toBe(true);
13
+ expect(isV2Project('2026.03-beta')).toBe(true);
12
14
  });
13
15
  it('returns false if platform version is less than the minimum', () => {
14
16
  expect(isV2Project('2025.0')).toBe(false);
17
+ expect(isV2Project('2025.01')).toBe(false);
18
+ expect(isV2Project('2025.01-beta')).toBe(false);
15
19
  });
16
20
  it('returns false if platform version is invalid', () => {
17
21
  expect(isV2Project(null)).toBe(false);
@@ -5,6 +5,6 @@ export function isV2Project(platformVersion) {
5
5
  if (platformVersion.toLowerCase() === 'unstable') {
6
6
  return true;
7
7
  }
8
- const [year, minor] = platformVersion.split('.');
8
+ const [year, minor] = platformVersion.split(/[.-]/);
9
9
  return Number(year) >= 2025 && Number(minor) >= 2;
10
10
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.9.0-beta.2",
3
+ "version": "7.9.0-experimental.0",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "@hubspot/local-dev-lib": "3.21.1",
9
+ "@hubspot/local-dev-lib": "3.21.2",
10
10
  "@hubspot/project-parsing-lib": "0.10.1",
11
11
  "@hubspot/serverless-dev-runtime": "7.0.6",
12
12
  "@hubspot/theme-preview-dev-server": "0.0.10",