@hubspot/cli 7.9.0-beta.1 → 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.
Files changed (105) hide show
  1. package/commands/__tests__/project.test.js +2 -0
  2. package/commands/account/createOverride.js +2 -12
  3. package/commands/account/removeOverride.js +2 -10
  4. package/commands/cms/theme/preview.js +1 -4
  5. package/commands/getStarted.js +7 -19
  6. package/commands/project/__tests__/deploy.test.js +4 -3
  7. package/commands/project/__tests__/updateDeps.test.d.ts +1 -0
  8. package/commands/project/__tests__/updateDeps.test.js +142 -0
  9. package/commands/project/create.js +0 -1
  10. package/commands/project/updateDeps.d.ts +6 -0
  11. package/commands/project/updateDeps.js +80 -0
  12. package/commands/project.js +2 -0
  13. package/commands/testAccount/create.js +1 -1
  14. package/lang/en.d.ts +30 -15
  15. package/lang/en.js +32 -18
  16. package/lib/__tests__/dependencyManagement.test.js +273 -1
  17. package/lib/__tests__/npm.test.js +1 -1
  18. package/lib/__tests__/sandboxSync.test.js +1 -1
  19. package/lib/__tests__/usageTracking.test.js +2 -2
  20. package/lib/commonOpts.js +2 -5
  21. package/lib/configMigrate.js +3 -3
  22. package/lib/dependencyManagement.d.ts +8 -2
  23. package/lib/dependencyManagement.js +75 -12
  24. package/lib/doctor/DiagnosticInfoBuilder.js +1 -1
  25. package/lib/doctor/Doctor.js +1 -1
  26. package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +4 -2
  27. package/lib/doctor/__tests__/Doctor.test.js +1 -1
  28. package/lib/jsonLoader.d.ts +14 -0
  29. package/lib/jsonLoader.js +60 -0
  30. package/lib/middleware/__test__/requestMiddleware.test.js +1 -1
  31. package/lib/middleware/autoUpdateMiddleware.js +1 -1
  32. package/lib/middleware/commandTargetingUtils.js +1 -0
  33. package/lib/middleware/fireAlarmMiddleware.js +1 -1
  34. package/lib/middleware/notificationsMiddleware.js +1 -1
  35. package/lib/middleware/requestMiddleware.js +1 -1
  36. package/lib/npm.d.ts +3 -0
  37. package/lib/npm.js +7 -1
  38. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -0
  39. package/lib/projects/__tests__/platformVersion.test.js +5 -1
  40. package/lib/projects/create/__tests__/v2.test.js +20 -14
  41. package/lib/projects/create/v2.js +8 -13
  42. package/lib/projects/localDev/LocalDevLogger.js +2 -2
  43. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
  44. package/lib/projects/localDev/LocalDevWebsocketServer.js +1 -1
  45. package/lib/projects/platformVersion.js +1 -1
  46. package/lib/prompts/promptUtils.d.ts +8 -0
  47. package/lib/prompts/promptUtils.js +7 -1
  48. package/lib/prompts/selectProjectTemplatePrompt.js +4 -0
  49. package/lib/sandboxSync.js +1 -1
  50. package/lib/usageTracking.js +2 -2
  51. package/mcp-server/tools/cms/HsCreateFunctionTool.js +2 -2
  52. package/mcp-server/tools/cms/HsCreateModuleTool.js +2 -2
  53. package/mcp-server/tools/cms/HsCreateTemplateTool.js +2 -2
  54. package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -9
  55. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  56. package/mcp-server/tools/cms/HsListTool.js +1 -1
  57. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +7 -4
  58. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +7 -3
  59. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +7 -4
  60. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +5 -1
  61. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +8 -3
  62. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +8 -3
  63. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +4 -1
  64. package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -5
  65. package/mcp-server/tools/project/CreateProjectTool.js +2 -2
  66. package/mcp-server/tools/project/DeployProjectTool.d.ts +4 -1
  67. package/mcp-server/tools/project/DeployProjectTool.js +4 -3
  68. package/mcp-server/tools/project/DocFetchTool.d.ts +4 -1
  69. package/mcp-server/tools/project/DocFetchTool.js +7 -6
  70. package/mcp-server/tools/project/DocsSearchTool.js +5 -5
  71. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +4 -1
  72. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +7 -5
  73. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +8 -2
  74. package/mcp-server/tools/project/GetApplicationInfoTool.js +7 -6
  75. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -4
  76. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +4 -1
  77. package/mcp-server/tools/project/GuidedWalkthroughTool.js +6 -14
  78. package/mcp-server/tools/project/UploadProjectTools.d.ts +4 -1
  79. package/mcp-server/tools/project/UploadProjectTools.js +4 -3
  80. package/mcp-server/tools/project/ValidateProjectTool.d.ts +4 -1
  81. package/mcp-server/tools/project/ValidateProjectTool.js +5 -4
  82. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +6 -1
  83. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +7 -3
  84. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +7 -2
  85. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +8 -3
  86. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +6 -2
  87. package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +8 -3
  88. package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +9 -5
  89. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +6 -2
  90. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +43 -13
  91. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +8 -2
  92. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +8 -2
  93. package/mcp-server/utils/__tests__/content.test.d.ts +1 -0
  94. package/mcp-server/utils/__tests__/content.test.js +166 -0
  95. package/mcp-server/utils/__tests__/feedbackTracking.test.d.ts +1 -0
  96. package/mcp-server/utils/__tests__/feedbackTracking.test.js +121 -0
  97. package/mcp-server/utils/content.d.ts +1 -1
  98. package/mcp-server/utils/content.js +8 -1
  99. package/mcp-server/utils/feedbackTracking.d.ts +1 -0
  100. package/mcp-server/utils/feedbackTracking.js +41 -0
  101. package/package.json +2 -2
  102. package/commands/project/__tests__/fixtures/exampleProject.json +0 -33
  103. package/lang/en.lyaml +0 -1508
  104. package/lib/lang.d.ts +0 -8
  105. package/lib/lang.js +0 -72
@@ -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`, {
@@ -1,7 +1,7 @@
1
1
  import { getProjectConfig } from '../projects/config.js';
2
2
  import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
3
3
  import path from 'path';
4
- import pkg from '../../package.json' with { type: 'json' };
4
+ import { pkg } from '../jsonLoader.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
6
  import { getAccountId, getDefaultAccountOverrideFilePath, isConfigFlagEnabled, } from '@hubspot/local-dev-lib/config';
7
7
  import { getAccountConfig, getConfigPath } from '@hubspot/local-dev-lib/config';
@@ -13,7 +13,7 @@ import { PORT_MANAGER_SERVER_PORT } from '@hubspot/local-dev-lib/constants/ports
13
13
  import { accessTokenForPersonalAccessKey, authorizedScopesForPortalAndUser, scopesOnAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
14
14
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
15
15
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
16
- import pkg from '../../package.json' with { type: 'json' };
16
+ import { pkg } from '../jsonLoader.js';
17
17
  import { lib } from '../../lang/en.js';
18
18
  import { uiLink } from '../ui/index.js';
19
19
  const minMajorNodeVersion = 18;
@@ -6,11 +6,13 @@ vi.mock('@hubspot/local-dev-lib/personalAccessKey');
6
6
  vi.mock('../../projects/config');
7
7
  vi.mock('@hubspot/local-dev-lib/api/projects');
8
8
  vi.mock('util');
9
- vi.mock('../../../package.json', () => {
9
+ vi.mock('../../jsonLoader.js', () => {
10
10
  return {
11
- default: {
11
+ pkg: {
12
+ name: '@hubspot/cli',
12
13
  version: '1.0.0',
13
14
  },
15
+ loadJson: vi.fn(),
14
16
  };
15
17
  });
16
18
  import { DiagnosticInfoBuilder, } from '../DiagnosticInfoBuilder.js';
@@ -359,7 +359,7 @@ describe('lib/doctor/Doctor', () => {
359
359
  expect(doctor.diagnosis?.addProjectSection).toHaveBeenCalledWith({
360
360
  type: 'warning',
361
361
  message: 'Port 8080 is in use',
362
- secondaryMessaging: expect.stringMatching(/Make sure it is available before running `hs project dev`/),
362
+ secondaryMessaging: expect.stringMatching(/Make sure it is available before running/),
363
363
  });
364
364
  });
365
365
  it('should add success section if port is available', async () => {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generic JSON loader that works in both test (lib/) and production (dist/lib/) environments.
3
+ * Automatically resolves paths relative to the caller's location.
4
+ *
5
+ * @param importMetaUrl - Pass import.meta.url from the calling file
6
+ * @param relativePath - Path to JSON file relative to the caller (e.g., '../package.json', './fixtures/data.json')
7
+ * @returns The loaded JSON object
8
+ */
9
+ export declare function loadJson<T = unknown>(importMetaUrl: string, relativePath: string): T;
10
+ export declare const pkg: {
11
+ [key: string]: unknown;
12
+ name: string;
13
+ version: string;
14
+ };
@@ -0,0 +1,60 @@
1
+ // NOTE: Can be switched back to standard import with min node version 23
2
+ import { createRequire } from 'module';
3
+ import { fileURLToPath } from 'url';
4
+ import { existsSync } from 'fs';
5
+ import path from 'path';
6
+ /**
7
+ * Generic JSON loader that works in both test (lib/) and production (dist/lib/) environments.
8
+ * Automatically resolves paths relative to the caller's location.
9
+ *
10
+ * @param importMetaUrl - Pass import.meta.url from the calling file
11
+ * @param relativePath - Path to JSON file relative to the caller (e.g., '../package.json', './fixtures/data.json')
12
+ * @returns The loaded JSON object
13
+ */
14
+ export function loadJson(importMetaUrl, relativePath) {
15
+ const callerDir = path.dirname(fileURLToPath(importMetaUrl));
16
+ const resolvedPath = path.resolve(callerDir, relativePath);
17
+ // If the resolved path exists, use it directly
18
+ if (existsSync(resolvedPath)) {
19
+ return createRequire(importMetaUrl)(resolvedPath);
20
+ }
21
+ // If not found, try adjusting for dist/ directory
22
+ // This handles the case where we're in dist/lib/ but the JSON is at project root
23
+ const pathParts = resolvedPath.split(path.sep);
24
+ const distIndex = pathParts.indexOf('dist');
25
+ if (distIndex !== -1) {
26
+ // Remove 'dist' from the path and try again
27
+ pathParts.splice(distIndex, 1);
28
+ const adjustedPath = pathParts.join(path.sep);
29
+ if (existsSync(adjustedPath)) {
30
+ return createRequire(importMetaUrl)(adjustedPath);
31
+ }
32
+ }
33
+ throw new Error(`JSON file not found: ${relativePath} (resolved to ${resolvedPath})`);
34
+ }
35
+ /**
36
+ * Helper to find package.json by walking up the directory tree.
37
+ * Works regardless of whether we're in lib/ or dist/lib/.
38
+ */
39
+ function findPackageJsonPath(startDir) {
40
+ let currentDir = startDir;
41
+ while (true) {
42
+ const pkgPath = path.join(currentDir, 'package.json');
43
+ if (existsSync(pkgPath)) {
44
+ return pkgPath;
45
+ }
46
+ const parentDir = path.dirname(currentDir);
47
+ if (parentDir === currentDir) {
48
+ // Reached root without finding package.json (e.g., in test environments)
49
+ return null;
50
+ }
51
+ currentDir = parentDir;
52
+ }
53
+ }
54
+ // Load package.json once when this module is imported for convenience
55
+ // In test environments where this can't be found, tests should mock this module
56
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
57
+ const pkgPath = findPackageJsonPath(__dirname);
58
+ export const pkg = pkgPath
59
+ ? createRequire(import.meta.url)(pkgPath)
60
+ : { name: 'unknown', version: 'unknown' };
@@ -1,6 +1,6 @@
1
1
  import { addUserAgentHeader } from '@hubspot/local-dev-lib/http';
2
2
  import { setRequestHeaders } from '../requestMiddleware.js';
3
- import pkg from '../../../package.json' with { type: 'json' };
3
+ import { pkg } from '../../jsonLoader.js';
4
4
  vi.mock('@hubspot/local-dev-lib/http', () => ({
5
5
  addUserAgentHeader: vi.fn(),
6
6
  }));
@@ -1,6 +1,6 @@
1
1
  import updateNotifier from 'update-notifier';
2
2
  import { isConfigFlagEnabled } from '@hubspot/local-dev-lib/config';
3
- import pkg from '../../package.json' with { type: 'json' };
3
+ import { pkg } from '../jsonLoader.js';
4
4
  import { UI_COLORS } from '../ui/index.js';
5
5
  import SpinniesManager from '../ui/SpinniesManager.js';
6
6
  import { lib } from '../../lang/en.js';
@@ -35,6 +35,7 @@ export function shouldLoadConfigForCommand(commandParts) {
35
35
  }
36
36
  const SKIP_CONFIG_VALIDATION_COMMANDS = {
37
37
  auth: true,
38
+ account: { auth: true },
38
39
  mcp: {
39
40
  setup: true,
40
41
  start: true,
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
3
3
  import { debugError } from '../errorHandlers/index.js';
4
- import pkg from '../../package.json' with { type: 'json' };
4
+ import { pkg } from '../jsonLoader.js';
5
5
  import { logInBox } from '../ui/boxen.js';
6
6
  import { renderInline } from '../../ui/index.js';
7
7
  import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
@@ -1,5 +1,5 @@
1
1
  import updateNotifier from 'update-notifier';
2
- import pkg from '../../package.json' with { type: 'json' };
2
+ import { pkg } from '../jsonLoader.js';
3
3
  import { UI_COLORS } from '../ui/index.js';
4
4
  import { lib } from '../../lang/en.js';
5
5
  const notifier = updateNotifier({
@@ -1,5 +1,5 @@
1
1
  import { addUserAgentHeader } from '@hubspot/local-dev-lib/http';
2
- import pkg from '../../package.json' with { type: 'json' };
2
+ import { pkg } from '../jsonLoader.js';
3
3
  export function setRequestHeaders() {
4
4
  addUserAgentHeader('HubSpot CLI', pkg.version);
5
5
  }
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
@@ -1,7 +1,7 @@
1
1
  import { exec as execAsync } from 'node:child_process';
2
2
  import util from 'util';
3
3
  import { uiLogger } from './ui/logger.js';
4
- import pkg from '../package.json' with { type: 'json' };
4
+ import { pkg } from './jsonLoader.js';
5
5
  export const DEFAULT_PACKAGE_MANAGER = 'npm';
6
6
  export async function isGloballyInstalled(packageName) {
7
7
  const exec = util.promisify(execAsync);
@@ -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
+ }
@@ -21,6 +21,7 @@ import { confirmPrompt } from '../../prompts/promptUtils.js';
21
21
  import { getOauthAppInstallUrl, getStaticAuthAppInstallUrl, } from '../../app/urls.js';
22
22
  import { isDeveloperTestAccount, isSandbox } from '../../accountTypes.js';
23
23
  import { logError } from '../../errorHandlers/index.js';
24
+ import { isServerRunningAtUrl } from '../../http.js';
24
25
  import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, APP_INSTALLATION_STATES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
25
26
  import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
26
27
  vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
@@ -36,6 +37,7 @@ vi.mock('../../errorHandlers/index');
36
37
  vi.mock('../localDev/LocalDevState');
37
38
  vi.mock('../localDev/LocalDevLogger');
38
39
  vi.mock('../../ui/SpinniesManager');
40
+ vi.mock('../../http');
39
41
  describe('AppDevModeInterface', () => {
40
42
  let appDevModeInterface;
41
43
  let mockLocalDevState;
@@ -139,6 +141,7 @@ describe('AppDevModeInterface', () => {
139
141
  });
140
142
  confirmPrompt.mockResolvedValue(true);
141
143
  installStaticAuthAppOnTestAccount.mockResolvedValue(undefined);
144
+ isServerRunningAtUrl.mockResolvedValue(true);
142
145
  // Mock process.exit
143
146
  vi.spyOn(global.process, 'exit').mockImplementation((code) => {
144
147
  throw new Error(`Process.exit called with code ${code}`);
@@ -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);
@@ -37,12 +37,12 @@ describe('lib/projects/create/v2', () => {
37
37
  };
38
38
  it('returns enabled components when they meet all requirements', async () => {
39
39
  const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', 123, mockProjectMetadataForChoices);
40
- expect(choices).toHaveLength(4); // includes separator
40
+ expect(choices).toHaveLength(2);
41
41
  expect(choices[0]).toEqual({
42
42
  name: 'Module Component [module]',
43
43
  value: mockComponents[0],
44
44
  });
45
- expect(choices[2]).toEqual({
45
+ expect(choices[1]).toEqual({
46
46
  name: expect.stringContaining('Card Component'),
47
47
  value: mockComponents[1],
48
48
  disabled: expect.stringContaining('maximum'),
@@ -50,20 +50,20 @@ describe('lib/projects/create/v2', () => {
50
50
  });
51
51
  it('disables components when auth type is not supported', async () => {
52
52
  const choices = await calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', 123, mockProjectMetadataForChoices);
53
- // All components should be disabled, so they come after the separator
54
- expect(choices[1]).toEqual({
53
+ // All components should be disabled
54
+ expect(choices[0]).toEqual({
55
55
  name: expect.stringContaining('Module Component'),
56
56
  value: mockComponents[0],
57
- disabled: expect.stringContaining('privatekey'),
57
+ disabled: expect.stringContaining('oauth'),
58
58
  });
59
59
  });
60
60
  it('disables components when distribution is not supported', async () => {
61
61
  const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', 123, mockProjectMetadataForChoices);
62
- // All components should be disabled, so they come after the separator
63
- expect(choices[1]).toEqual({
62
+ // All components should be disabled
63
+ expect(choices[0]).toEqual({
64
64
  name: expect.stringContaining('Module Component'),
65
65
  value: mockComponents[0],
66
- disabled: expect.stringContaining('enterprise'),
66
+ disabled: expect.stringContaining('private'),
67
67
  });
68
68
  });
69
69
  it('handles components without auth type or distribution restrictions', async () => {
@@ -125,8 +125,8 @@ describe('lib/projects/create/v2', () => {
125
125
  },
126
126
  };
127
127
  const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, projectMetadataAtMaxWorkflowAction);
128
- expect(choices).toHaveLength(3); // includes separators
129
- expect(choices[1]).toEqual({
128
+ expect(choices).toHaveLength(1);
129
+ expect(choices[0]).toEqual({
130
130
  name: expect.stringContaining('Workflow Action Tool'),
131
131
  value: componentWithCliSelector[0],
132
132
  disabled: expect.stringContaining('maximum'),
@@ -183,12 +183,18 @@ describe('lib/projects/create/v2', () => {
183
183
  supportedDistributions: ['private'],
184
184
  },
185
185
  ];
186
- const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, mockProjectMetadataForChoices);
187
- expect(choices).toHaveLength(3); // includes separators
188
- expect(choices[1]).toEqual({
186
+ const projectMetadataWithWorkflowAction = {
187
+ hsMetaFiles: [],
188
+ components: {
189
+ 'workflow-action': { count: 0, maxCount: 3, hsMetaFiles: [] },
190
+ },
191
+ };
192
+ const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, projectMetadataWithWorkflowAction);
193
+ expect(choices).toHaveLength(1);
194
+ expect(choices[0]).toEqual({
189
195
  name: expect.stringContaining('Workflow Action Tool'),
190
196
  value: gatedComponent[0],
191
- disabled: expect.stringContaining('does not have access to this feature'),
197
+ disabled: expect.stringContaining("doesn't have access to this feature"),
192
198
  });
193
199
  expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
194
200
  });
@@ -1,8 +1,6 @@
1
- import { Separator } from '@inquirer/prompts';
2
1
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_PROJECT, PROJECT_WITH_APP, FEATURES, } from '../../constants.js';
3
2
  import { commands, lib } from '../../../lang/en.js';
4
3
  import { listPrompt } from '../../prompts/promptUtils.js';
5
- import chalk from 'chalk';
6
4
  import { isV2Project } from '../platformVersion.js';
7
5
  import path from 'path';
8
6
  import { getConfigForPlatformVersion } from './legacy.js';
@@ -75,25 +73,27 @@ export async function calculateComponentTemplateChoices(components, authType, di
75
73
  if (Array.isArray(supportedAuthTypes) &&
76
74
  authType &&
77
75
  !supportedAuthTypes.includes(authType.toLowerCase())) {
78
- disabledReasons.push(commands.project.add.error.authTypeNotAllowed(authType));
76
+ const supportedAuthTypesString = supportedAuthTypes.join(', ');
77
+ disabledReasons.push(commands.project.add.error.authTypeNotAllowed(supportedAuthTypesString));
79
78
  }
80
79
  if (Array.isArray(supportedDistributions) &&
81
80
  distribution &&
82
81
  !supportedDistributions.includes(distribution.toLowerCase())) {
83
- disabledReasons.push(commands.project.add.error.distributionNotAllowed(distribution));
82
+ const supportedDistributionsString = supportedDistributions.join(', ');
83
+ disabledReasons.push(commands.project.add.error.distributionNotAllowed(supportedDistributionsString));
84
84
  }
85
85
  const templateGate = componentTypeToGateMap[template.cliSelector || template.type];
86
86
  if (templateGate) {
87
87
  const isUngated = await hasFeature(accountId, templateGate);
88
88
  if (!isUngated) {
89
- disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature(accountId));
89
+ disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature());
90
90
  }
91
91
  }
92
92
  if (disabledReasons.length > 0) {
93
93
  disabledComponents.push({
94
- name: `[${chalk.yellow('DISABLED')}] ${template.label} -`,
94
+ name: `${template.label} [${template.cliSelector || template.type}]`,
95
95
  value: template,
96
- disabled: disabledReasons.join(' '),
96
+ disabled: `– ${disabledReasons.join(' ')}`,
97
97
  });
98
98
  }
99
99
  else {
@@ -104,12 +104,7 @@ export async function calculateComponentTemplateChoices(components, authType, di
104
104
  }
105
105
  }
106
106
  return disabledComponents.length
107
- ? [
108
- ...enabledComponents,
109
- new Separator(),
110
- ...disabledComponents,
111
- new Separator(),
112
- ]
107
+ ? [...enabledComponents, ...disabledComponents]
113
108
  : [...enabledComponents];
114
109
  }
115
110
  export async function v2ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, accountId) {
@@ -1,7 +1,7 @@
1
1
  import { getAccountId, hasLocalStateFlag } from '@hubspot/local-dev-lib/config';
2
2
  import { getConfigDefaultAccount } from '@hubspot/local-dev-lib/config';
3
3
  import { uiLogger } from '../../ui/logger.js';
4
- import { uiBetaTag, uiLine, uiAccountDescription, uiCommandReference, } from '../../ui/index.js';
4
+ import { uiLine, uiAccountDescription, uiCommandReference, } from '../../ui/index.js';
5
5
  import { lib } from '../../../lang/en.js';
6
6
  import SpinniesManager from '../../ui/SpinniesManager.js';
7
7
  import { logError } from '../../errorHandlers/index.js';
@@ -75,7 +75,7 @@ class LocalDevLogger {
75
75
  if (!this.state.debug) {
76
76
  console.clear();
77
77
  }
78
- uiBetaTag(lib.LocalDevManager.betaMessage);
78
+ uiLogger.log(lib.LocalDevManager.headerMessage);
79
79
  uiLogger.log(lib.LocalDevManager.learnMoreLocalDevServer);
80
80
  uiLogger.log('');
81
81
  uiLogger.log(lib.LocalDevManager.running(this.state.projectConfig.name, uiAccountDescription(this.state.targetProjectAccountId)));
@@ -11,7 +11,7 @@ import { EXIT_CODES } from '../../enums/exitCodes.js';
11
11
  import { getAccountHomeUrl } from '../urls.js';
12
12
  import { componentIsApp, componentIsPublicApp, CONFIG_FILES, getAppCardConfigs, getComponentUid, } from '../structure.js';
13
13
  import { ComponentTypes, } from '../../../types/Projects.js';
14
- import { UI_COLORS, uiCommandReference, uiAccountDescription, uiBetaTag, uiLink, uiLine, } from '../../ui/index.js';
14
+ import { UI_COLORS, uiCommandReference, uiAccountDescription, uiLink, uiLine, } from '../../ui/index.js';
15
15
  import { logError } from '../../errorHandlers/index.js';
16
16
  import { installAppBrowserPrompt } from '../../prompts/installAppPrompt.js';
17
17
  import { confirmPrompt } from '../../prompts/promptUtils.js';
@@ -131,8 +131,8 @@ class LocalDevManager {
131
131
  else if (!this.debug) {
132
132
  console.clear();
133
133
  }
134
- uiBetaTag(lib.LocalDevManager.betaMessage);
135
- uiLogger.log(uiLink(lib.LocalDevManager.learnMoreLocalDevServer, 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'));
134
+ uiLogger.log(lib.LocalDevManager.headerMessage);
135
+ uiLogger.log(uiLink(lib.LocalDevManager.learnMoreLocalDevServer, 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands'));
136
136
  uiLogger.log('');
137
137
  uiLogger.log(chalk.hex(UI_COLORS.SORBET)(lib.LocalDevManager.running(this.projectConfig.name, uiAccountDescription(this.targetAccountId))));
138
138
  uiLogger.log(lib.LocalDevManager.viewProjectLink(this.projectConfig.name, this.targetProjectAccountId));
@@ -6,7 +6,7 @@ import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, CONFIG
6
6
  import { lib } from '../../../lang/en.js';
7
7
  import { removeAnsiCodes } from '../../ui/removeAnsiCodes.js';
8
8
  import { isDeployWebsocketMessage, isViewedWelcomeScreenWebsocketMessage, isUploadWebsocketMessage, isAppInstallFailureWebsocketMessage, isAppInstallSuccessWebsocketMessage, isAppInstallInitiatedWebsocketMessage, } from './localDevWebsocketServerUtils.js';
9
- import pkg from '../../../package.json' with { type: 'json' };
9
+ import { pkg } from '../../jsonLoader.js';
10
10
  const LOCAL_DEV_WEBSOCKET_SERVER_VERSION = 2;
11
11
  const LOG_PREFIX = '[LocalDevWebsocketServer]';
12
12
  const DOMAINS = ['hubspot.com', 'hubspotqa.com'];
@@ -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
  }
@@ -6,6 +6,14 @@ export declare const PROMPT_THEME: {
6
6
  idle: string;
7
7
  };
8
8
  };
9
+ export declare const CHECKBOX_PROMPT_THEME: {
10
+ prefix: {
11
+ idle: string;
12
+ };
13
+ style: {
14
+ disabledChoice: (text: string) => string;
15
+ };
16
+ };
9
17
  export declare function promptUser<T extends GenericPromptResponse>(config: PromptConfig<T> | PromptConfig<T>[]): Promise<T>;
10
18
  export declare function confirmPrompt(message: string, options?: {
11
19
  defaultAnswer?: boolean;
@@ -5,6 +5,12 @@ import { lib } from '../../lang/en.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
6
  export const Separator = new _Separator();
7
7
  export const PROMPT_THEME = { prefix: { idle: chalk.green('?') } };
8
+ export const CHECKBOX_PROMPT_THEME = {
9
+ prefix: { idle: chalk.green('?') },
10
+ style: {
11
+ disabledChoice: (text) => chalk.dim(` ◯ ${text}`),
12
+ },
13
+ };
8
14
  function isUserCancellationError(error) {
9
15
  return error instanceof Error && error.name === 'ExitPromptError';
10
16
  }
@@ -147,7 +153,7 @@ function handleCheckboxPrompt(config) {
147
153
  pageSize: config.pageSize,
148
154
  validate: config.validate,
149
155
  loop: config.loop,
150
- theme: PROMPT_THEME,
156
+ theme: CHECKBOX_PROMPT_THEME,
151
157
  shortcuts: {
152
158
  invert: null,
153
159
  },
@@ -1,6 +1,7 @@
1
1
  import { Separator } from '@inquirer/prompts';
2
2
  import { promptUser } from './promptUtils.js';
3
3
  import { lib } from '../../lang/en.js';
4
+ import { uiLogger } from '../ui/logger.js';
4
5
  function findTemplateByNameOrLabel(projectTemplates, templateNameOrLabel) {
5
6
  return projectTemplates.find(t => t.name === templateNameOrLabel || t.label === templateNameOrLabel);
6
7
  }
@@ -55,6 +56,9 @@ export async function selectProjectTemplatePrompt(promptOptions, projectTemplate
55
56
  pageSize: componentTemplates?.length,
56
57
  },
57
58
  ]);
59
+ if (result.componentTemplates?.length === 0) {
60
+ uiLogger.log(lib.projects.add.nothingAdded);
61
+ }
58
62
  if (!result.componentTemplates) {
59
63
  result.componentTemplates = selectedComponents;
60
64
  }