@hubspot/cli 8.5.0 → 8.6.0-beta.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 (64) hide show
  1. package/commands/account/clean.js +2 -0
  2. package/commands/account/createOverride.js +3 -0
  3. package/commands/account/info.js +34 -16
  4. package/commands/account/link.d.ts +4 -0
  5. package/commands/account/link.js +89 -0
  6. package/commands/account/list.js +29 -71
  7. package/commands/account/remove.js +2 -0
  8. package/commands/account/removeOverride.js +3 -0
  9. package/commands/account/unlink.d.ts +4 -0
  10. package/commands/account/unlink.js +70 -0
  11. package/commands/account/use.js +71 -1
  12. package/commands/account.js +4 -0
  13. package/commands/project/appInstallStatus.d.ts +4 -0
  14. package/commands/project/appInstallStatus.js +132 -0
  15. package/commands/project/create.js +8 -0
  16. package/commands/project/dev/deprecatedFlow.js +20 -2
  17. package/commands/project/dev/index.js +6 -0
  18. package/commands/project/dev/unifiedFlow.js +20 -3
  19. package/commands/project/lint.js +20 -2
  20. package/commands/project.js +2 -0
  21. package/lang/en.d.ts +102 -0
  22. package/lang/en.js +116 -8
  23. package/lib/app/migrate.js +2 -1
  24. package/lib/constants.d.ts +1 -0
  25. package/lib/constants.js +3 -0
  26. package/lib/doctor/Doctor.js +5 -5
  27. package/lib/link/accountTableUtils.d.ts +10 -0
  28. package/lib/link/accountTableUtils.js +39 -0
  29. package/lib/link/index.d.ts +18 -0
  30. package/lib/link/index.js +185 -0
  31. package/lib/link/linkUtils.d.ts +5 -0
  32. package/lib/link/linkUtils.js +49 -0
  33. package/lib/link/prompts.d.ts +7 -0
  34. package/lib/link/prompts.js +126 -0
  35. package/lib/link/renderLinkedAccountsTable.d.ts +2 -0
  36. package/lib/link/renderLinkedAccountsTable.js +14 -0
  37. package/lib/link/warnIfLinkedDirectory.d.ts +1 -0
  38. package/lib/link/warnIfLinkedDirectory.js +9 -0
  39. package/lib/projects/localDev/DevServerManager_DEPRECATED.d.ts +2 -1
  40. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +2 -2
  41. package/lib/projects/localDev/LocalDevManager_DEPRECATED.d.ts +2 -0
  42. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -0
  43. package/lib/projects/uieLinting.d.ts +17 -3
  44. package/lib/projects/uieLinting.js +93 -28
  45. package/lib/prompts/promptUtils.js +1 -0
  46. package/lib/ui/accountTable.d.ts +8 -0
  47. package/lib/ui/accountTable.js +67 -0
  48. package/mcp-server/server.js +39 -1
  49. package/mcp-server/tools/index.js +2 -0
  50. package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
  51. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  52. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  53. package/mcp-server/tools/project/FindProjectsTool.d.ts +15 -0
  54. package/mcp-server/tools/project/FindProjectsTool.js +60 -0
  55. package/mcp-server/tools/project/GetBuildLogsTool.js +1 -1
  56. package/mcp-server/tools/project/GetBuildStatusTool.js +1 -1
  57. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  58. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  59. package/package.json +2 -2
  60. package/types/Link.d.ts +32 -0
  61. package/types/Link.js +5 -0
  62. package/types/PackageJson.d.ts +1 -0
  63. package/types/Prompts.d.ts +1 -0
  64. package/types/Yargs.d.ts +1 -0
@@ -0,0 +1,126 @@
1
+ import { promptUser } from '../prompts/promptUtils.js';
2
+ import { uiAccountDescription } from '../ui/index.js';
3
+ import { uiLogger } from '../ui/logger.js';
4
+ import { commands } from '../../lang/en.js';
5
+ import { buildAccountRow, getNameColumnWidth, buildAccountHeader, sortDefaultFirst, } from './accountTableUtils.js';
6
+ import { Separator } from '@inquirer/prompts';
7
+ function buildColumnarChoices(accounts, localDefaultAccount) {
8
+ const rows = accounts.map(a => ({
9
+ ...buildAccountRow(a.accountId, a.accountId === localDefaultAccount),
10
+ disabled: a.disabled,
11
+ checked: a.checked,
12
+ hint: a.hint,
13
+ }));
14
+ const nameWidth = getNameColumnWidth(rows);
15
+ const header = buildAccountHeader(nameWidth);
16
+ return [
17
+ new Separator(header),
18
+ ...rows.map(row => {
19
+ const label = `${row.name.padEnd(nameWidth)} ${row.accountId}`;
20
+ return {
21
+ name: row.hint ? `${label} ${row.hint}` : label,
22
+ short: uiAccountDescription(Number(row.accountId), false),
23
+ value: Number(row.accountId),
24
+ disabled: row.disabled,
25
+ checked: row.checked,
26
+ };
27
+ }),
28
+ ];
29
+ }
30
+ function mapLinkAccountChoices(eligibleAccounts, inEligibleAccounts, accountOverrideId, localDefaultAccount, preselectedAccountId) {
31
+ const sortedIneligible = sortDefaultFirst(inEligibleAccounts, localDefaultAccount);
32
+ const accounts = [
33
+ ...eligibleAccounts.map(a => ({
34
+ accountId: a.accountId,
35
+ disabled: false,
36
+ checked: a.accountId === accountOverrideId ||
37
+ a.accountId === preselectedAccountId,
38
+ hint: a.accountId === accountOverrideId
39
+ ? commands.account.subcommands.link.prompts.fromHsAccount
40
+ : a.accountId === preselectedAccountId
41
+ ? commands.account.subcommands.link.prompts.newlyAuthenticated
42
+ : undefined,
43
+ })),
44
+ ...sortedIneligible.map(a => ({
45
+ accountId: a.accountId,
46
+ disabled: commands.account.subcommands.link.prompts.alreadyLinked,
47
+ checked: false,
48
+ })),
49
+ ];
50
+ return buildColumnarChoices(accounts, localDefaultAccount);
51
+ }
52
+ export async function promptForAction(state) {
53
+ const isSettingsEmpty = state.accounts.length === 0 && state.localDefaultAccount === undefined;
54
+ const { accountEditOption } = await promptUser({
55
+ type: 'list',
56
+ name: 'accountEditOption',
57
+ message: isSettingsEmpty
58
+ ? commands.account.subcommands.link.prompts.howToProceed
59
+ : commands.account.subcommands.link.prompts.whatToDo,
60
+ choices: [
61
+ {
62
+ name: commands.account.subcommands.link.prompts.linkExisting,
63
+ value: 'link',
64
+ },
65
+ {
66
+ name: commands.account.subcommands.link.prompts.authenticateNew,
67
+ value: 'authenticate',
68
+ },
69
+ {
70
+ name: commands.account.subcommands.link.prompts.cancel,
71
+ value: 'cancel',
72
+ },
73
+ ],
74
+ });
75
+ uiLogger.log('');
76
+ return accountEditOption;
77
+ }
78
+ export async function promptForDefaultAccount(accounts, currentDefaultAccount, prompt = '') {
79
+ const choiceAccounts = accounts.map(accountId => ({
80
+ accountId,
81
+ }));
82
+ const choices = buildColumnarChoices(choiceAccounts, currentDefaultAccount);
83
+ const { defaultAccount } = await promptUser({
84
+ type: 'list',
85
+ name: 'defaultAccount',
86
+ pageSize: 20,
87
+ message: prompt || commands.account.subcommands.link.prompts.selectDefault,
88
+ choices,
89
+ default: currentDefaultAccount ?? undefined,
90
+ });
91
+ uiLogger.log('');
92
+ return defaultAccount;
93
+ }
94
+ export async function promptForAccountsToLink(context, eligibleAccounts, inEligibleAccounts, localDefaultAccount) {
95
+ const { accountsToAdd } = await promptUser({
96
+ type: 'checkbox',
97
+ name: 'accountsToAdd',
98
+ pageSize: 20,
99
+ message: commands.account.subcommands.link.prompts.selectToLink,
100
+ choices: mapLinkAccountChoices(eligibleAccounts, inEligibleAccounts, context.accountOverrideId, localDefaultAccount, context.preselectedAccountId),
101
+ validate: (answer) => {
102
+ if (answer.length === 0) {
103
+ return commands.account.subcommands.link.prompts.mustSelectOne;
104
+ }
105
+ return true;
106
+ },
107
+ });
108
+ uiLogger.log('');
109
+ return accountsToAdd;
110
+ }
111
+ export async function promptForAccountsToUnlink(accounts, localDefaultAccount) {
112
+ const sortedAccounts = sortDefaultFirst(accounts, localDefaultAccount);
113
+ const choiceAccounts = sortedAccounts.map(accountId => ({
114
+ accountId,
115
+ }));
116
+ const choices = buildColumnarChoices(choiceAccounts, localDefaultAccount);
117
+ const { accountsToRemove } = await promptUser({
118
+ type: 'checkbox',
119
+ name: 'accountsToRemove',
120
+ pageSize: 20,
121
+ message: commands.account.subcommands.link.prompts.selectToUnlink,
122
+ choices,
123
+ });
124
+ uiLogger.log('');
125
+ return accountsToRemove;
126
+ }
@@ -0,0 +1,2 @@
1
+ import { HsSettingsFile } from '@hubspot/local-dev-lib/types/HsSettings';
2
+ export declare function renderLinkedAccountsTable(settings: HsSettingsFile): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { commands } from '../../lang/en.js';
2
+ import { renderTable } from '../../ui/render.js';
3
+ import { buildAccountRow, sortDefaultFirst } from './accountTableUtils.js';
4
+ export async function renderLinkedAccountsTable(settings) {
5
+ const labels = commands.account.subcommands.list.labels;
6
+ const tableHeader = [labels.name, labels.accountId];
7
+ const sortedAccounts = sortDefaultFirst(settings.accounts, settings.localDefaultAccount);
8
+ const tableData = sortedAccounts.map(accountId => {
9
+ const isDefault = accountId === settings.localDefaultAccount;
10
+ const row = buildAccountRow(accountId, isDefault);
11
+ return [row.name, row.accountId];
12
+ });
13
+ await renderTable(tableHeader, tableData, true);
14
+ }
@@ -0,0 +1 @@
1
+ export declare function warnIfLinkedDirectory(args: (string | number)[]): void;
@@ -0,0 +1,9 @@
1
+ import { getHsSettingsFilePath } from '@hubspot/local-dev-lib/config/hsSettings';
2
+ import { uiLogger } from '../ui/logger.js';
3
+ import { lib } from '../../lang/en.js';
4
+ export function warnIfLinkedDirectory(args) {
5
+ if (getHsSettingsFilePath() === null) {
6
+ return;
7
+ }
8
+ uiLogger.warn(lib.linkedDirectory.warning(`hs ${args.join(' ')}`, getHsSettingsFilePath()));
9
+ }
@@ -21,12 +21,13 @@ declare class DevServerManager_DEPRECATED {
21
21
  [key: string]: Component;
22
22
  }) => Promise<void>): Promise<void>;
23
23
  arrangeComponentsByType(components: Component[]): ComponentsByType;
24
- setup({ components, onUploadRequired, accountId, setActiveApp, exit, }: {
24
+ setup({ components, onUploadRequired, accountId, setActiveApp, exit, port, }: {
25
25
  components: Component[];
26
26
  onUploadRequired: () => void;
27
27
  accountId: number;
28
28
  setActiveApp: (appUid: string | undefined) => Promise<void>;
29
29
  exit: ExitFunction;
30
+ port?: number;
30
31
  }): Promise<void>;
31
32
  start({ accountId, projectConfig, }: {
32
33
  accountId: number;
@@ -57,7 +57,7 @@ class DevServerManager_DEPRECATED {
57
57
  return acc;
58
58
  }, {});
59
59
  }
60
- async setup({ components, onUploadRequired, accountId, setActiveApp, exit, }) {
60
+ async setup({ components, onUploadRequired, accountId, setActiveApp, exit, port, }) {
61
61
  this.componentsByType = this.arrangeComponentsByType(components);
62
62
  let env;
63
63
  const accountConfig = getConfigAccountById(accountId);
@@ -65,7 +65,7 @@ class DevServerManager_DEPRECATED {
65
65
  env = accountConfig.env;
66
66
  }
67
67
  try {
68
- await startPortManagerServer();
68
+ await startPortManagerServer(port);
69
69
  }
70
70
  catch (e) {
71
71
  logError(e);
@@ -16,6 +16,7 @@ type LocalDevManagerConstructorOptions = {
16
16
  runnableComponents: Component[];
17
17
  env: Environment;
18
18
  exit: ExitFunction;
19
+ port?: number;
19
20
  };
20
21
  declare class LocalDevManager_DEPRECATED {
21
22
  targetAccountId: number;
@@ -39,6 +40,7 @@ declare class LocalDevManager_DEPRECATED {
39
40
  mostRecentUploadWarning: string | null;
40
41
  private devSessionManager;
41
42
  private exit;
43
+ private port?;
42
44
  constructor(options: LocalDevManagerConstructorOptions);
43
45
  setActiveApp(appUid?: string): Promise<void>;
44
46
  setActivePublicAppData(): Promise<void>;
@@ -48,6 +48,7 @@ class LocalDevManager_DEPRECATED {
48
48
  mostRecentUploadWarning;
49
49
  devSessionManager;
50
50
  exit;
51
+ port;
51
52
  constructor(options) {
52
53
  this.targetAccountId = options.targetAccountId;
53
54
  // The account that the project exists in. This is not always the targetAccountId
@@ -67,6 +68,7 @@ class LocalDevManager_DEPRECATED {
67
68
  this.publicAppActiveInstalls = null;
68
69
  this.mostRecentUploadWarning = null;
69
70
  this.exit = options.exit;
71
+ this.port = options.port;
70
72
  this.projectSourceDir = path.join(this.projectDir, this.projectConfig.srcDir);
71
73
  if (!this.targetAccountId || !this.projectConfig || !this.projectDir) {
72
74
  uiLogger.error(lib.LocalDevManager.failedToInitialize);
@@ -337,6 +339,7 @@ class LocalDevManager_DEPRECATED {
337
339
  accountId: this.targetAccountId,
338
340
  setActiveApp: this.setActiveApp.bind(this),
339
341
  exit: this.exit,
342
+ port: this.port,
340
343
  });
341
344
  return true;
342
345
  }
@@ -1,10 +1,19 @@
1
1
  export declare const REQUIRED_PACKAGES_AND_MIN_VERSIONS: {
2
2
  readonly eslint: "9.0.0";
3
- readonly '@typescript-eslint/eslint-plugin': "8.46.4";
4
- readonly '@typescript-eslint/parser': "8.46.4";
3
+ readonly '@eslint/js': "9.0.0";
5
4
  readonly 'typescript-eslint': "8.46.4";
5
+ readonly '@hubspot/eslint-config-ui-extensions': "1.0.0";
6
+ readonly 'eslint-config-prettier': "10.0.0";
7
+ readonly 'eslint-plugin-react': "7.0.0";
8
+ readonly 'eslint-plugin-react-hooks': "7.0.0";
9
+ readonly 'eslint-plugin-unused-imports': "4.0.0";
10
+ readonly prettier: "3.0.0";
6
11
  readonly jiti: "2.6.1";
7
12
  };
13
+ export declare const LINT_SCRIPTS: {
14
+ readonly lint: "eslint .";
15
+ readonly 'lint:fix': "eslint . --fix";
16
+ };
8
17
  export declare function isEslintInstalled(directory: string): boolean;
9
18
  export declare function areAllLintPackagesInstalled(directory: string): boolean;
10
19
  export declare function getMissingLintPackages(directory: string): {
@@ -13,7 +22,7 @@ export declare function getMissingLintPackages(directory: string): {
13
22
  export declare function hasEslintConfig(directory: string): boolean;
14
23
  export declare function hasDeprecatedEslintConfig(directory: string): boolean;
15
24
  export declare function getDeprecatedEslintConfigFiles(directory: string): string[];
16
- export declare function createEslintConfig(directory: string): string;
25
+ export declare function createEslintConfig(directory: string, platformVersion?: string | null): Promise<string>;
17
26
  export declare function lintPackagesInDirectory(directory: string, projectDir?: string): Promise<{
18
27
  success: boolean;
19
28
  output: string;
@@ -31,3 +40,8 @@ export declare function displayLintResults(results: Array<{
31
40
  success: boolean;
32
41
  output: string;
33
42
  }>): void;
43
+ export declare function getMissingLintScripts(directory: string): string[];
44
+ export declare function addLintScriptsToPackageJson(directory: string): {
45
+ added: string[];
46
+ relativePath: string;
47
+ };
@@ -3,17 +3,27 @@ import path from 'path';
3
3
  import util from 'util';
4
4
  import semver from 'semver';
5
5
  import { exec as execAsync } from 'node:child_process';
6
+ import { fetchRepoFile } from '@hubspot/local-dev-lib/api/github';
6
7
  import { getProjectPackageJsonLocations, isPackageInstalled, } from '../dependencyManagement.js';
7
8
  import { commands } from '../../lang/en.js';
8
9
  import { uiLogger } from '../ui/logger.js';
9
- import { safeGetPackageJsonCached } from '../npm/packageJson.js';
10
+ import { clearPackageJsonCache, safeGetPackageJsonCached, } from '../npm/packageJson.js';
11
+ import { debugError } from '../errorHandlers/index.js';
12
+ import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
+ import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../constants.js';
10
14
  export const REQUIRED_PACKAGES_AND_MIN_VERSIONS = {
11
15
  eslint: '9.0.0',
12
- '@typescript-eslint/eslint-plugin': '8.46.4',
13
- '@typescript-eslint/parser': '8.46.4',
16
+ '@eslint/js': '9.0.0',
14
17
  'typescript-eslint': '8.46.4',
18
+ '@hubspot/eslint-config-ui-extensions': '1.0.0',
19
+ 'eslint-config-prettier': '10.0.0',
20
+ 'eslint-plugin-react': '7.0.0',
21
+ 'eslint-plugin-react-hooks': '7.0.0',
22
+ 'eslint-plugin-unused-imports': '4.0.0',
23
+ prettier: '3.0.0',
15
24
  jiti: '2.6.1',
16
25
  };
26
+ const UIE_ESLINT_CONFIG_PATH_IN_REPO = 'components/cards/src/app/cards/eslint.config.js';
17
27
  const ESLINT_CONFIG_FILES = [
18
28
  'eslint.config.mts',
19
29
  'eslint.config.ts',
@@ -30,28 +40,10 @@ const DEPRECATED_ESLINT_CONFIG_FILES = [
30
40
  '.eslintrc.json',
31
41
  '.eslintrc',
32
42
  ];
33
- const ESLINT_CONFIG_TEMPLATE = `import { defineConfig } from "eslint/config";
34
- import tsParser from "@typescript-eslint/parser";
35
-
36
- export default defineConfig([
37
- {
38
- files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
39
- languageOptions: {
40
- parser: tsParser,
41
- parserOptions: {
42
- ecmaVersion: "latest",
43
- sourceType: "module",
44
- ecmaFeatures: {
45
- jsx: true
46
- }
47
- }
48
- },
49
- rules: {
50
- "no-console": ["warn", { allow: ["warn", "error"] }]
51
- }
52
- },
53
- ]);
54
- `;
43
+ export const LINT_SCRIPTS = {
44
+ lint: 'eslint .',
45
+ 'lint:fix': 'eslint . --fix',
46
+ };
55
47
  function getPackageVersionFromPackageJson(directory, packageName) {
56
48
  const packageJsonPath = path.join(directory, 'package.json');
57
49
  const packageJson = safeGetPackageJsonCached(packageJsonPath);
@@ -127,10 +119,43 @@ export function getDeprecatedEslintConfigFiles(directory) {
127
119
  return fs.existsSync(configPath);
128
120
  });
129
121
  }
130
- export function createEslintConfig(directory) {
131
- const configPath = path.join(directory, 'eslint.config.mts');
122
+ function repoFileDataToString(data) {
123
+ if (typeof data === 'string') {
124
+ return data;
125
+ }
126
+ if (Buffer.isBuffer(data)) {
127
+ return data.toString('utf-8');
128
+ }
129
+ return String(data);
130
+ }
131
+ export async function createEslintConfig(directory, platformVersion) {
132
+ const versionForRemote = platformVersion && !isLegacyProject(platformVersion)
133
+ ? platformVersion
134
+ : null;
135
+ if (versionForRemote === null) {
136
+ const message = commands.project.lint.createEslintConfigRequiresV2Platform(platformVersion);
137
+ uiLogger.error(message);
138
+ throw new Error(message);
139
+ }
140
+ let fetchedContent = null;
132
141
  try {
133
- fs.writeFileSync(configPath, ESLINT_CONFIG_TEMPLATE, 'utf-8');
142
+ const { data } = await fetchRepoFile(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, `${versionForRemote}/${UIE_ESLINT_CONFIG_PATH_IN_REPO}`, DEFAULT_PROJECT_TEMPLATE_BRANCH);
143
+ const content = repoFileDataToString(data);
144
+ if (content.trim().length > 0) {
145
+ fetchedContent = content;
146
+ }
147
+ }
148
+ catch (error) {
149
+ debugError(error);
150
+ }
151
+ if (fetchedContent === null) {
152
+ const message = commands.project.lint.failedToFetchRemoteEslintConfig(versionForRemote);
153
+ uiLogger.error(message);
154
+ throw new Error(message);
155
+ }
156
+ const configPath = path.join(directory, 'eslint.config.js');
157
+ try {
158
+ fs.writeFileSync(configPath, fetchedContent, 'utf-8');
134
159
  return path.relative(process.cwd(), configPath);
135
160
  }
136
161
  catch (error) {
@@ -218,3 +243,43 @@ export function displayLintResults(results) {
218
243
  });
219
244
  }
220
245
  }
246
+ export function getMissingLintScripts(directory) {
247
+ const packageJsonPath = path.join(directory, 'package.json');
248
+ const packageJson = safeGetPackageJsonCached(packageJsonPath);
249
+ if (!packageJson) {
250
+ return [];
251
+ }
252
+ return Object.keys(LINT_SCRIPTS).filter(scriptName => !packageJson.scripts?.[scriptName]);
253
+ }
254
+ export function addLintScriptsToPackageJson(directory) {
255
+ const packageJsonPath = path.join(directory, 'package.json');
256
+ try {
257
+ const rawContent = fs.readFileSync(packageJsonPath, 'utf-8');
258
+ const packageJson = JSON.parse(rawContent);
259
+ if (!packageJson.scripts) {
260
+ packageJson.scripts = {};
261
+ }
262
+ const added = [];
263
+ for (const [scriptName, scriptValue] of Object.entries(LINT_SCRIPTS)) {
264
+ if (!packageJson.scripts[scriptName]) {
265
+ packageJson.scripts[scriptName] = scriptValue;
266
+ added.push(scriptName);
267
+ }
268
+ }
269
+ if (added.length > 0) {
270
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
271
+ clearPackageJsonCache();
272
+ }
273
+ return {
274
+ added,
275
+ relativePath: path.relative(process.cwd(), packageJsonPath),
276
+ };
277
+ }
278
+ catch {
279
+ uiLogger.warn(commands.project.lint.failedToAddLintScripts(packageJsonPath));
280
+ return {
281
+ added: [],
282
+ relativePath: path.relative(process.cwd(), packageJsonPath),
283
+ };
284
+ }
285
+ }
@@ -42,6 +42,7 @@ function mapPromptChoicesToChoices(choices) {
42
42
  return {
43
43
  value: choice.value,
44
44
  name: choice.name,
45
+ short: choice.short,
45
46
  disabled: choice.disabled,
46
47
  checked: choice.checked,
47
48
  };
@@ -0,0 +1,8 @@
1
+ import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ export declare function sortAndMapAccounts(accounts: HubSpotConfigAccount[]): {
3
+ [key: string]: HubSpotConfigAccount[];
4
+ };
5
+ export declare function getAccountData(mappedAccountData: {
6
+ [key: string]: HubSpotConfigAccount[];
7
+ }): string[][];
8
+ export declare function renderAccountTable(showAllLabel?: boolean): void;
@@ -0,0 +1,67 @@
1
+ import { HUBSPOT_ACCOUNT_TYPES, HUBSPOT_ACCOUNT_TYPE_STRINGS, } from '@hubspot/local-dev-lib/constants/config';
2
+ import { isSandbox, isDeveloperTestAccount } from '../accountTypes.js';
3
+ import { getAllConfigAccounts } from '@hubspot/local-dev-lib/config';
4
+ import { commands } from '../../lang/en.js';
5
+ import { renderTable } from '../../ui/render.js';
6
+ import { uiLogger } from './logger.js';
7
+ export function sortAndMapAccounts(accounts) {
8
+ const mappedAccountData = {};
9
+ // Standard and app developer accounts
10
+ accounts
11
+ .filter(p => p.accountType &&
12
+ (p.accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD ||
13
+ p.accountType === HUBSPOT_ACCOUNT_TYPES.APP_DEVELOPER))
14
+ .forEach(account => {
15
+ mappedAccountData[account.accountId] = [account];
16
+ });
17
+ // Non-standard accounts (sandbox, developer test account)
18
+ accounts
19
+ .filter(p => p.accountType && (isSandbox(p) || isDeveloperTestAccount(p)))
20
+ .forEach(p => {
21
+ if (p.parentAccountId) {
22
+ mappedAccountData[p.parentAccountId] = [
23
+ ...(mappedAccountData[p.parentAccountId] || []),
24
+ p,
25
+ ];
26
+ }
27
+ else {
28
+ mappedAccountData[p.accountId] = [p];
29
+ }
30
+ });
31
+ return mappedAccountData;
32
+ }
33
+ export function getAccountData(mappedAccountData) {
34
+ const accountData = [];
35
+ Object.entries(mappedAccountData).forEach(([key, set]) => {
36
+ const hasParentAccount = set.filter(p => p.accountId === parseInt(key, 10))[0];
37
+ set.forEach(account => {
38
+ let name = `${account.name} [${HUBSPOT_ACCOUNT_TYPE_STRINGS[account.accountType]}]`;
39
+ if (isSandbox(account)) {
40
+ if (hasParentAccount && set.length > 1) {
41
+ name = `↳ ${name}`;
42
+ }
43
+ }
44
+ else if (isDeveloperTestAccount(account)) {
45
+ if (hasParentAccount && set.length > 1) {
46
+ name = `↳ ${name}`;
47
+ }
48
+ }
49
+ accountData.push([name, String(account.accountId), account.authType]);
50
+ });
51
+ });
52
+ return accountData;
53
+ }
54
+ export function renderAccountTable(showAllLabel = false) {
55
+ const accountsList = getAllConfigAccounts();
56
+ const mappedAccountData = sortAndMapAccounts(accountsList);
57
+ const accountData = getAccountData(mappedAccountData);
58
+ const tableHeader = [
59
+ commands.account.subcommands.list.labels.name,
60
+ commands.account.subcommands.list.labels.accountId,
61
+ commands.account.subcommands.list.labels.authType,
62
+ ];
63
+ uiLogger.log(showAllLabel
64
+ ? commands.account.subcommands.list.allAccounts
65
+ : commands.account.subcommands.list.accounts);
66
+ renderTable(tableHeader, accountData, true);
67
+ }
@@ -2,11 +2,49 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { registerProjectTools, registerCmsTools } from './tools/index.js';
4
4
  import { McpLogger } from './utils/logger.js';
5
+ const instructions = `
6
+ This server exposes the HubSpot CLI (\`hs\`) for local development of HubSpot
7
+ projects, apps, and CMS assets. Prefer these tools over running \`hs\`,
8
+ \`npx hs\`, or HubSpot HTTP APIs directly via shell — they handle config
9
+ loading, auth, platform-version flags, and structured output for you.
10
+
11
+ WHEN TO USE THIS SERVER
12
+ - The user is working in a HubSpot project directory (has an hsproject.json
13
+ or *-hsmeta.json files), or wants to scaffold one.
14
+ - The user asks about HubSpot apps, CMS modules/templates/serverless
15
+ functions, project builds, deploys, or developer test accounts.
16
+ - The user asks a HubSpot platform/API question — answer it from the docs
17
+ via \`search-docs\` + \`fetch-doc\` rather than from prior knowledge.
18
+
19
+ REQUIRED WORKFLOWS
20
+ 1. Documentation lookup: always call \`search-docs\` first, then
21
+ \`fetch-doc\` on the most relevant result(s) before planning, writing
22
+ code, or answering platform/API questions. Do not answer from memory.
23
+ 2. Locating a HubSpot project: when the current working directory is not
24
+ a HubSpot project (no \`hsproject.json\`) or you need to determine
25
+ whether a directory contains one, call \`find-projects\` before
26
+ running any tool that requires a project path.
27
+ 3. Editing \`*-hsmeta.json\`: call \`get-feature-config-schema\` for that
28
+ feature type first to learn the allowed fields and values.
29
+ 4. Debugging a failed build: start with \`get-build-status\` to surface
30
+ error messages, and only reach for \`get-build-logs\` for deeper
31
+ troubleshooting or warnings.
32
+ 5. Reading serverless function logs: call \`list-cms-serverless-functions\`
33
+ first to discover the endpoint path, then
34
+ \`get-cms-serverless-function-logs\`.
35
+ 6. App analytics: call \`get-apps-info\` to discover \`appId\` values
36
+ before \`get-api-usage-patterns-by-app-id\`.
37
+
38
+ OUTPUT
39
+ Tool results contain the relevant \`hs\` stdout/stderr or structured data.
40
+ Surface error text from results to the user verbatim when troubleshooting,
41
+ rather than paraphrasing.
42
+ `.trim();
5
43
  const server = new McpServer({
6
44
  name: 'HubSpot CLI MCP Server',
7
45
  version: '0.0.1',
8
46
  description: 'Helps perform tasks for local development of HubSpot projects.',
9
- }, { capabilities: { logging: {} } });
47
+ }, { capabilities: { logging: {} }, instructions });
10
48
  const logger = new McpLogger(server);
11
49
  registerProjectTools(server, logger);
12
50
  registerCmsTools(server, logger);
@@ -18,6 +18,7 @@ import { HsCreateFunctionTool } from './cms/HsCreateFunctionTool.js';
18
18
  import { HsListFunctionsTool } from './cms/HsListFunctionsTool.js';
19
19
  import { HsFunctionLogsTool } from './cms/HsFunctionLogsTool.js';
20
20
  import { CreateTestAccountTool } from './project/CreateTestAccountTool.js';
21
+ import { FindProjectsTool } from './project/FindProjectsTool.js';
21
22
  export function registerProjectTools(mcpServer, logger) {
22
23
  return [
23
24
  new UploadProjectTools(mcpServer, logger).register(),
@@ -34,6 +35,7 @@ export function registerProjectTools(mcpServer, logger) {
34
35
  new GetApplicationInfoTool(mcpServer, logger).register(),
35
36
  new GetBuildLogsTool(mcpServer, logger).register(),
36
37
  new GetBuildStatusTool(mcpServer, logger).register(),
38
+ new FindProjectsTool(mcpServer, logger).register(),
37
39
  ];
38
40
  }
39
41
  export function registerCmsTools(mcpServer, logger) {
@@ -71,7 +71,7 @@ export class AddFeatureToProjectTool extends Tool {
71
71
  return this.mcpServer.registerTool(toolName, {
72
72
  title: 'Add feature to HubSpot Project',
73
73
  description: `Adds a feature to an existing HubSpot project.
74
- Only works for projects with platformVersion '2025.2' and beyond`,
74
+ Only works for projects with platformVersion '2025.2' and beyond. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.`,
75
75
  inputSchema,
76
76
  annotations: {
77
77
  readOnlyHint: false,
@@ -57,7 +57,7 @@ const inputSchema = {
57
57
  contentLevel: z
58
58
  .enum(ACCOUNT_LEVEL_CHOICES)
59
59
  .optional()
60
- .describe(`CMS Hub tier level. Options: ${ACCOUNT_LEVEL_CHOICES.join(', ')}. Defaults to ENTERPRISE if not specified.`),
60
+ .describe(`Content Hub tier level. Options: ${ACCOUNT_LEVEL_CHOICES.join(', ')}. Defaults to ENTERPRISE if not specified.`),
61
61
  commerceLevel: z
62
62
  .enum(ACCOUNT_LEVEL_CHOICES_WITHOUT_STARTER)
63
63
  .optional()
@@ -43,7 +43,7 @@ export class DeployProjectTool extends Tool {
43
43
  register() {
44
44
  return this.mcpServer.registerTool(toolName, {
45
45
  title: 'Deploy a build of HubSpot Project',
46
- description: 'Takes a build number and a project name and deploys that build of the project. DO NOT run this tool unless the user specifies they would like to deploy the project.',
46
+ description: 'Takes a build number and a project name and deploys that build of the project. DO NOT run this tool unless the user specifies they would like to deploy the project. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.',
47
47
  inputSchema,
48
48
  annotations: {
49
49
  readOnlyHint: false,
@@ -0,0 +1,15 @@
1
+ import { TextContentResponse } from '../../types.js';
2
+ import { Tool } from '../../Tool.js';
3
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { McpLogger } from '../../utils/logger.js';
5
+ import { z } from 'zod';
6
+ declare const inputSchemaZodObject: z.ZodObject<{
7
+ absoluteDirectory: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export type FindProjectsInputSchema = z.infer<typeof inputSchemaZodObject>;
10
+ export declare class FindProjectsTool extends Tool<FindProjectsInputSchema> {
11
+ constructor(mcpServer: McpServer, logger: McpLogger);
12
+ handler({ absoluteDirectory, }: FindProjectsInputSchema): Promise<TextContentResponse>;
13
+ register(): RegisteredTool;
14
+ }
15
+ export {};