@hubspot/cli 7.9.0 → 7.10.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/commands/__tests__/project.test.js +2 -0
  2. package/commands/account/__tests__/rename.test.js +35 -0
  3. package/commands/account/createOverride.js +2 -12
  4. package/commands/account/removeOverride.js +2 -10
  5. package/commands/account/rename.d.ts +1 -1
  6. package/commands/account/rename.js +5 -2
  7. package/commands/cms/theme/preview.js +1 -4
  8. package/commands/config/set.js +1 -2
  9. package/commands/getStarted.js +13 -19
  10. package/commands/hubdb.d.ts +1 -1
  11. package/commands/project/__tests__/updateDeps.test.d.ts +1 -0
  12. package/commands/project/__tests__/updateDeps.test.js +142 -0
  13. package/commands/project/create.js +0 -1
  14. package/commands/project/dev/index.js +8 -1
  15. package/commands/project/listBuilds.js +7 -1
  16. package/commands/project/updateDeps.d.ts +6 -0
  17. package/commands/project/updateDeps.js +80 -0
  18. package/commands/project/upload.js +7 -1
  19. package/commands/project/validate.js +7 -1
  20. package/commands/project/watch.js +7 -2
  21. package/commands/project.js +2 -0
  22. package/commands/testAccount/__tests__/create.test.js +68 -0
  23. package/commands/testAccount/create.d.ts +8 -0
  24. package/commands/testAccount/create.js +134 -44
  25. package/commands/testAccount/importData.d.ts +1 -1
  26. package/lang/en.d.ts +3194 -3184
  27. package/lang/en.js +43 -8
  28. package/lib/__tests__/dependencyManagement.test.js +273 -1
  29. package/lib/commonOpts.js +2 -5
  30. package/lib/constants.d.ts +1 -0
  31. package/lib/constants.js +6 -0
  32. package/lib/dependencyManagement.d.ts +8 -2
  33. package/lib/dependencyManagement.js +75 -12
  34. package/lib/mcp/__tests__/setup.test.d.ts +1 -0
  35. package/lib/mcp/__tests__/setup.test.js +127 -0
  36. package/lib/mcp/setup.d.ts +4 -12
  37. package/lib/mcp/setup.js +34 -1
  38. package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
  39. package/lib/middleware/autoUpdateMiddleware.js +1 -0
  40. package/lib/npm.d.ts +3 -0
  41. package/lib/npm.js +6 -0
  42. package/lib/projects/__tests__/components.test.js +148 -24
  43. package/lib/projects/__tests__/platformVersion.test.js +5 -1
  44. package/lib/projects/__tests__/projects.test.js +13 -42
  45. package/lib/projects/components.js +76 -20
  46. package/lib/projects/config.js +5 -9
  47. package/lib/projects/platformVersion.js +1 -1
  48. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
  49. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
  50. package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
  51. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
  52. package/mcp-server/tools/cms/HsCreateFunctionTool.js +6 -0
  53. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
  54. package/mcp-server/tools/cms/HsCreateModuleTool.js +6 -0
  55. package/mcp-server/tools/cms/HsCreateTemplateTool.js +6 -0
  56. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
  57. package/mcp-server/tools/cms/HsFunctionLogsTool.js +4 -0
  58. package/mcp-server/tools/cms/HsListFunctionsTool.js +4 -0
  59. package/mcp-server/tools/cms/HsListTool.js +4 -0
  60. package/mcp-server/tools/index.js +2 -0
  61. package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -0
  62. package/mcp-server/tools/project/CreateProjectTool.js +6 -0
  63. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
  64. package/mcp-server/tools/project/CreateTestAccountTool.js +137 -0
  65. package/mcp-server/tools/project/DeployProjectTool.js +6 -0
  66. package/mcp-server/tools/project/DocFetchTool.js +4 -0
  67. package/mcp-server/tools/project/DocsSearchTool.js +4 -0
  68. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +4 -0
  69. package/mcp-server/tools/project/GetApplicationInfoTool.js +4 -0
  70. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -0
  71. package/mcp-server/tools/project/GuidedWalkthroughTool.js +4 -0
  72. package/mcp-server/tools/project/UploadProjectTools.d.ts +9 -3
  73. package/mcp-server/tools/project/UploadProjectTools.js +50 -4
  74. package/mcp-server/tools/project/ValidateProjectTool.js +4 -0
  75. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
  76. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +231 -0
  77. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
  78. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
  79. package/package.json +2 -2
  80. package/lang/en.lyaml +0 -1508
  81. package/lib/lang.d.ts +0 -8
  82. package/lib/lang.js +0 -72
package/lang/en.js CHANGED
@@ -51,12 +51,11 @@ export const commands = {
51
51
  uploadProject: (accountName) => `Would you like to upload this project to account "${accountName}" now?`,
52
52
  projectCreated: {
53
53
  title: chalk.bold('Next steps:'),
54
- description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project install-deps')} to ${chalk.bold('install dependencies')} and ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
54
+ description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
55
55
  },
56
56
  },
57
57
  logs: {
58
58
  appSelected: `We'll create a new project with a sample app for you.\nProjects are what you can use to create apps with HubSpot.\nUsually you'll use the ${uiCommandReference('hs project create')} command, but we'll go ahead and make one now.`,
59
- dependenciesInstalled: 'Dependencies installed successfully.',
60
59
  uploadingProject: 'Uploading your project to HubSpot...',
61
60
  uploadSuccess: 'Project uploaded successfully!',
62
61
  developerOverviewLink: 'Open this link to navigate to your HubSpot developer portal',
@@ -64,7 +63,6 @@ export const commands = {
64
63
  errors: {
65
64
  uploadFailed: 'Failed to upload project to HubSpot.',
66
65
  configFileNotFound: 'Could not find project configuration for upload.',
67
- installDepsFailed: 'Failed to install dependencies.',
68
66
  },
69
67
  },
70
68
  completion: {
@@ -120,7 +118,7 @@ export const commands = {
120
118
  },
121
119
  },
122
120
  success: {
123
- renamed: (name, newName) => `Account "${name}" renamed to "${newName}"`,
121
+ renamed: (name, newName, nameWasSanitized) => `Account "${chalk.bold(name)}" successfully renamed to "${chalk.bold(newName)}"${nameWasSanitized ? ' (Sanitized to remove invalid characters)' : ''}.`,
124
122
  },
125
123
  },
126
124
  use: {
@@ -1198,6 +1196,7 @@ export const commands = {
1198
1196
  setup: {
1199
1197
  describe: 'Setup the HubSpot development MCP servers.',
1200
1198
  installingDocSearch: 'Adding the docs-search mcp server',
1199
+ codex: 'Codex CLI',
1201
1200
  claudeCode: 'Claude Code',
1202
1201
  cursor: 'Cursor',
1203
1202
  windsurf: 'Windsurf',
@@ -1219,7 +1218,11 @@ export const commands = {
1219
1218
  configuredClaudeCode: 'Configured Claude Code',
1220
1219
  claudeCodeNotFound: 'Claude Code not found - skipping configuration',
1221
1220
  claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
1222
- failedToConfigureClaudeDesktop: 'Failed to configure Claude Desktop',
1221
+ // Codex
1222
+ configuringCodex: 'Configuring Codex...',
1223
+ configuredCodex: 'Configured Codex',
1224
+ codexNotFound: 'Codex command not found - skipping configuration',
1225
+ codexInstallFailed: 'Failed to configure Codex',
1223
1226
  // Cursor
1224
1227
  configuringCursor: 'Configuring Cursor...',
1225
1228
  failedToConfigureCursor: 'Failed to configure Cursor',
@@ -1765,6 +1768,22 @@ export const commands = {
1765
1768
  noPackageJsonInProject: (projectName) => `No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
1766
1769
  packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
1767
1770
  },
1771
+ updateDeps: {
1772
+ help: {
1773
+ describe: 'Update the npm dependencies for your project, or update specific dependencies in a subcomponent of a project.',
1774
+ updateAppDepsExample: 'Update the dependencies for the project',
1775
+ updateDepToSubComponentExample: 'Update the npm dependencies in one or more project subcomponents',
1776
+ },
1777
+ installLocationPrompt: 'Choose which project components you would like to update the dependencies for:',
1778
+ installLocationPromptRequired: 'You must choose at least one subcomponent',
1779
+ updatingDependencies: (directory) => `Updating dependencies in ${directory}`,
1780
+ updateSuccessful: (directory) => `Updated dependencies in ${directory}`,
1781
+ updatingDependenciesToLocation: (dependencies, directory) => `Updating ${dependencies} in ${directory}`,
1782
+ updatingDependenciesFailed: (directory) => `Updating dependencies for ${directory} failed`,
1783
+ noProjectConfig: 'No project detected. Run this command from a project directory.',
1784
+ noPackageJsonInProject: (projectName) => `No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
1785
+ packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
1786
+ },
1768
1787
  validate: {
1769
1788
  describe: 'Validate the project before uploading',
1770
1789
  mustBeRanWithinAProject: 'This command must be run from within a project directory.',
@@ -2082,7 +2101,7 @@ export const commands = {
2082
2101
  },
2083
2102
  },
2084
2103
  create: {
2085
- describe: 'Create a test account from a config file',
2104
+ describe: `Create a test account from scratch or from a config file. Use ${uiCommandReference('hs test-account create-config')} to generate a config file. \n${uiLink('Learn more', 'https://developers.hubspot.com/docs/developer-tooling/local-development/configurable-test-accounts')}`,
2086
2105
  configPathPrompt: '[--config-path] Enter the path to the test account config: ',
2087
2106
  createTestAccountFromConfigPrompt: 'How would you like to create your test account?',
2088
2107
  createFromConfigOption: 'Create test account from config file',
@@ -2099,9 +2118,21 @@ export const commands = {
2099
2118
  createFailure: 'Failed to create test account.',
2100
2119
  },
2101
2120
  options: {
2102
- configPath: 'The path to the test account config',
2121
+ configPath: 'Path to config file (mutually exclusive with other flags)',
2122
+ accountName: 'Name for the test account',
2123
+ description: 'Description for the test account',
2124
+ marketingLevel: 'Marketing Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
2125
+ opsLevel: 'Operations Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
2126
+ serviceLevel: 'Service Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
2127
+ salesLevel: 'Sales Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
2128
+ contentLevel: 'CMS Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
2103
2129
  },
2104
2130
  example: (configPath) => `Create a test account from the config file at ${configPath}`,
2131
+ examples: {
2132
+ withAllHubsEnterprise: 'Create a test account with all hubs at ENTERPRISE level',
2133
+ withSpecificHubLevels: 'Create a test account with specific hub levels',
2134
+ },
2135
+ savedAccountNameDiffers: (originalName, savedName) => `Account name "${chalk.bold(originalName)}" was saved as "${chalk.bold(savedName)}" in config.`,
2105
2136
  },
2106
2137
  createConfig: {
2107
2138
  describe: 'Create a test account config file.',
@@ -2964,12 +2995,16 @@ export const lib = {
2964
2995
  },
2965
2996
  },
2966
2997
  add: {
2967
- nothingAdded: 'No features added.',
2998
+ nothingAdded: 'No features were added to the project. Use the space bar to select features from the list.',
2968
2999
  },
2969
3000
  updateHsMetaFilesWithAutoGeneratedFields: {
2970
3001
  header: 'Created the following components and features:',
2971
3002
  applicationLog: (componentType, uid, name) => ` - Created ${chalk.bold(componentType)} with uid ${chalk.bold(uid)} and name ${chalk.bold(name)}`,
2972
3003
  componentLog: (componentType, uid) => ` - Created ${chalk.bold(componentType)} feature with uid ${chalk.bold(uid)}`,
3004
+ failedToUpdate: (hsMetaFile) => `Failed to update the uid in ${chalk.bold(hsMetaFile)}`,
3005
+ },
3006
+ generateSafeFilenameDifferentiator: {
3007
+ failedToCheckFiles: 'Failed to check files for filename differentiator. Falling back to timestamp.',
2973
3008
  },
2974
3009
  validateProjectConfig: {
2975
3010
  configNotFound: `Unable to locate a project configuration file. Try running again from a project directory, or run ${uiCommandReference('hs project create')} to create a new project.`,
@@ -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]) || '';
@@ -139,3 +139,4 @@ export declare const LEGACY_PRIVATE_APP_FILE = "app.json";
139
139
  export declare const THEME_FILE = "theme.json";
140
140
  export declare const CMS_ASSETS_FILE = "cms-assets.json";
141
141
  export declare const LEGACY_CONFIG_FILES: string[];
142
+ export declare const ACCOUNT_LEVEL_CHOICES: readonly ["FREE", "STARTER", "PROFESSIONAL", "ENTERPRISE"];
package/lib/constants.js CHANGED
@@ -135,3 +135,9 @@ export const LEGACY_CONFIG_FILES = [
135
135
  LEGACY_PRIVATE_APP_FILE,
136
136
  LEGACY_PUBLIC_APP_FILE,
137
137
  ];
138
+ export const ACCOUNT_LEVEL_CHOICES = [
139
+ 'FREE',
140
+ 'STARTER',
141
+ 'PROFESSIONAL',
142
+ 'ENTERPRISE',
143
+ ];
@@ -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`, {
@@ -0,0 +1 @@
1
+ export {};