@hubspot/cli 8.0.11-experimental.2 → 8.0.12-experimental.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 (75) hide show
  1. package/bin/cli.js +4 -3
  2. package/commands/account/clean.js +2 -0
  3. package/commands/account/createOverride.js +3 -0
  4. package/commands/account/info.js +34 -16
  5. package/commands/account/link.d.ts +4 -0
  6. package/commands/account/link.js +89 -0
  7. package/commands/account/list.js +29 -71
  8. package/commands/account/remove.js +2 -0
  9. package/commands/account/removeOverride.js +3 -0
  10. package/commands/account/unlink.d.ts +4 -0
  11. package/commands/account/unlink.js +70 -0
  12. package/commands/account/use.js +71 -1
  13. package/commands/account.js +4 -0
  14. package/commands/project/appInstallStatus.d.ts +4 -0
  15. package/commands/project/appInstallStatus.js +132 -0
  16. package/commands/project/create.js +8 -0
  17. package/commands/project/dev/deprecatedFlow.js +20 -2
  18. package/commands/project/dev/index.js +6 -0
  19. package/commands/project/dev/unifiedFlow.js +20 -3
  20. package/commands/project/lint.js +20 -2
  21. package/commands/project/upload.d.ts +2 -0
  22. package/commands/project/upload.js +47 -3
  23. package/commands/project.js +2 -0
  24. package/lang/en.d.ts +122 -0
  25. package/lang/en.js +136 -8
  26. package/lib/app/migrate.js +2 -1
  27. package/lib/constants.d.ts +2 -0
  28. package/lib/constants.js +4 -0
  29. package/lib/doctor/Doctor.js +5 -5
  30. package/lib/link/accountTableUtils.d.ts +10 -0
  31. package/lib/link/accountTableUtils.js +39 -0
  32. package/lib/link/index.d.ts +18 -0
  33. package/lib/link/index.js +185 -0
  34. package/lib/link/linkUtils.d.ts +5 -0
  35. package/lib/link/linkUtils.js +49 -0
  36. package/lib/link/prompts.d.ts +7 -0
  37. package/lib/link/prompts.js +126 -0
  38. package/lib/link/renderLinkedAccountsTable.d.ts +2 -0
  39. package/lib/link/renderLinkedAccountsTable.js +14 -0
  40. package/lib/link/warnIfLinkedDirectory.d.ts +1 -0
  41. package/lib/link/warnIfLinkedDirectory.js +9 -0
  42. package/lib/projects/ProjectLogsManager.js +4 -1
  43. package/lib/projects/localDev/DevServerManager_DEPRECATED.d.ts +2 -1
  44. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +2 -2
  45. package/lib/projects/localDev/LocalDevManager_DEPRECATED.d.ts +2 -0
  46. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -0
  47. package/lib/projects/preview.d.ts +7 -0
  48. package/lib/projects/preview.js +58 -0
  49. package/lib/projects/uieLinting.d.ts +17 -3
  50. package/lib/projects/uieLinting.js +93 -28
  51. package/lib/projects/upload.d.ts +1 -0
  52. package/lib/projects/upload.js +4 -3
  53. package/lib/prompts/projectsLogsPrompt.js +3 -0
  54. package/lib/prompts/promptUtils.js +1 -0
  55. package/lib/ui/accountTable.d.ts +8 -0
  56. package/lib/ui/accountTable.js +67 -0
  57. package/lib/yargs/parseYargsOrExit.d.ts +4 -0
  58. package/lib/yargs/parseYargsOrExit.js +25 -0
  59. package/mcp-server/server.js +39 -1
  60. package/mcp-server/tools/index.js +2 -0
  61. package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
  62. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  63. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  64. package/mcp-server/tools/project/FindProjectsTool.d.ts +15 -0
  65. package/mcp-server/tools/project/FindProjectsTool.js +60 -0
  66. package/mcp-server/tools/project/GetBuildLogsTool.js +1 -1
  67. package/mcp-server/tools/project/GetBuildStatusTool.js +1 -1
  68. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  69. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  70. package/package.json +7 -7
  71. package/types/Link.d.ts +32 -0
  72. package/types/Link.js +5 -0
  73. package/types/PackageJson.d.ts +1 -0
  74. package/types/Prompts.d.ts +1 -0
  75. package/types/Yargs.d.ts +1 -0
@@ -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
+ }
@@ -5,6 +5,7 @@ type ProjectUploadResult<T> = {
5
5
  result?: T;
6
6
  uploadError?: unknown;
7
7
  projectNotFound?: boolean;
8
+ projectId?: number;
8
9
  };
9
10
  type HandleProjectUploadArg<T> = {
10
11
  accountId: number;
@@ -79,7 +79,7 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
79
79
  return resolve({ uploadError: e });
80
80
  }
81
81
  }
82
- const { projectExists } = await ensureProjectExists(accountId, projectConfig.name, {
82
+ const { projectExists, project } = await ensureProjectExists(accountId, projectConfig.name, {
83
83
  forceCreate,
84
84
  uploadCommand: isUploadCommand,
85
85
  noLogs: true,
@@ -88,13 +88,14 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
88
88
  uiLogger.log(lib.projectUpload.handleProjectUpload.projectDoesNotExist(accountId));
89
89
  return resolve({ projectNotFound: true });
90
90
  }
91
+ const projectId = project?.id;
91
92
  const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation);
92
93
  if (error) {
93
- resolve({ uploadError: error });
94
+ resolve({ uploadError: error, projectId });
94
95
  }
95
96
  else if (callbackFunc) {
96
97
  const uploadResult = await callbackFunc(accountId, projectConfig, tempFile, buildId);
97
- resolve({ result: uploadResult });
98
+ resolve({ result: uploadResult, projectId });
98
99
  }
99
100
  }
100
101
  catch (e) {
@@ -4,6 +4,9 @@ export async function projectLogsPrompt({ functionChoices, promptOptions, projec
4
4
  if (!functionChoices) {
5
5
  return {};
6
6
  }
7
+ if (promptOptions?.function) {
8
+ return { functionName: promptOptions.function };
9
+ }
7
10
  if (functionChoices.length === 1) {
8
11
  return { functionName: functionChoices[0] };
9
12
  }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import { ArgumentsCamelCase, Argv } from 'yargs';
2
+ type YargsFailureHandler<T> = (message: string | null, error: unknown, parser: Argv<T>) => never;
3
+ export declare function parseYargsOrExit<T>(parser: Argv<T>, handleFailure: YargsFailureHandler<T>): Promise<ArgumentsCamelCase<T>>;
4
+ export {};
@@ -0,0 +1,25 @@
1
+ function getYargsErrorMessage(error) {
2
+ return error instanceof Error && error.name === 'YError' ? error.message : '';
3
+ }
4
+ function getYargsFailureMessage(message, error) {
5
+ if (message) {
6
+ return message;
7
+ }
8
+ return getYargsErrorMessage(error) || message;
9
+ }
10
+ export async function parseYargsOrExit(parser, handleFailure) {
11
+ let failureHandled = false;
12
+ const parserWithFailureHandler = parser.fail((message, error, yargs) => {
13
+ failureHandled = true;
14
+ return handleFailure(getYargsFailureMessage(message, error), error, yargs);
15
+ });
16
+ try {
17
+ return await parserWithFailureHandler.parseAsync();
18
+ }
19
+ catch (error) {
20
+ if (failureHandled) {
21
+ throw error;
22
+ }
23
+ return handleFailure(getYargsErrorMessage(error), error, parserWithFailureHandler);
24
+ }
25
+ }
@@ -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 {};
@@ -0,0 +1,60 @@
1
+ import { Tool } from '../../Tool.js';
2
+ import { z } from 'zod';
3
+ import { formatTextContents } from '../../utils/content.js';
4
+ import { walk } from '@hubspot/local-dev-lib/fs';
5
+ import path from 'path';
6
+ import { PROJECT_CONFIG_FILE } from '../../../lib/constants.js';
7
+ const TOOL_NAME = 'find-projects';
8
+ const IGNORE_DIRS = ['node_modules', '.git', '.vite'];
9
+ const inputSchema = {
10
+ absoluteDirectory: z
11
+ .string()
12
+ .describe('The absolute path to the directory to search for HubSpot projects in.'),
13
+ };
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ const inputSchemaZodObject = z.object({ ...inputSchema });
16
+ export class FindProjectsTool extends Tool {
17
+ constructor(mcpServer, logger) {
18
+ super(mcpServer, logger, TOOL_NAME);
19
+ }
20
+ async handler({ absoluteDirectory, }) {
21
+ try {
22
+ const allFiles = await walk(absoluteDirectory, IGNORE_DIRS);
23
+ const projectFiles = allFiles.filter(file => path.basename(file) === PROJECT_CONFIG_FILE);
24
+ if (projectFiles.length === 0) {
25
+ return formatTextContents(`No ${PROJECT_CONFIG_FILE} files found under ${absoluteDirectory}.`);
26
+ }
27
+ const projectDirs = projectFiles.map(file => path.dirname(file));
28
+ const output = [
29
+ `Found ${projectFiles.length} project(s):`,
30
+ '',
31
+ ...projectDirs.map(dir => ` ${dir}`),
32
+ ].join('\n');
33
+ return formatTextContents(output);
34
+ }
35
+ catch (error) {
36
+ this.logger.debug(TOOL_NAME, {
37
+ message: 'Handler caught error',
38
+ error: error instanceof Error ? error.message : String(error),
39
+ });
40
+ const cause = error instanceof Error ? error.cause : undefined;
41
+ const causeMessage = cause instanceof Error ? `: ${cause.message}` : '';
42
+ const errorMessage = error instanceof Error
43
+ ? error.message || `FileSystemError${causeMessage}`
44
+ : String(error);
45
+ return formatTextContents(`Error searching for projects: ${errorMessage}`);
46
+ }
47
+ }
48
+ register() {
49
+ return this.mcpServer.registerTool(TOOL_NAME, {
50
+ title: 'Find HubSpot Projects',
51
+ description: 'Use this tool to locate HubSpot projects. Traverses child directories of the given directory to find hsproject.json files, returning the paths of all discovered HubSpot projects.',
52
+ inputSchema,
53
+ annotations: {
54
+ readOnlyHint: true,
55
+ openWorldHint: false,
56
+ idempotentHint: true,
57
+ },
58
+ }, input => this.wrappedHandler(input));
59
+ }
60
+ }
@@ -114,7 +114,7 @@ export class GetBuildLogsTool extends Tool {
114
114
  register() {
115
115
  return this.mcpServer.registerTool(TOOL_NAME, {
116
116
  title: 'Get HubSpot Project Build Logs',
117
- description: 'Retrieves build logs for a specific HubSpot project build. Use this to debug build failures by viewing the full build pipeline output. This tool is for more comprehensive troubleshootings or addressing build WARNINGs, build errors should be troubleshooted with get-build-status tool first. Logs can be filtered by level (ERROR, WARN, INFO, or ALL). Use `hs project list-builds` first to identify the build ID and error messages.',
117
+ description: 'Retrieves build logs for a specific HubSpot project build. Use this to debug build failures by viewing the full build pipeline output. This tool is for more comprehensive troubleshootings or addressing build WARNINGs, build errors should be troubleshooted with get-build-status tool first. Logs can be filtered by level (ERROR, WARN, INFO, or ALL). Use `hs project list-builds` first to identify the build ID and error messages. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.',
118
118
  inputSchema,
119
119
  annotations: {
120
120
  readOnlyHint: true,
@@ -155,7 +155,7 @@ export class GetBuildStatusTool extends Tool {
155
155
  register() {
156
156
  return this.mcpServer.registerTool(TOOL_NAME, {
157
157
  title: 'Get HubSpot Projects Build Status and Errors',
158
- description: 'Retrieves build status and error messages for HubSpot projects. When buildId is omitted, shows recent builds with their status(default 3) - use this to find the latest builds when troubleshooting. When buildId is provided, shows detailed error information for that specific build. Displays buildErrorMessage and subbuild failures to help diagnose build issues.',
158
+ description: 'Retrieves build status and error messages for HubSpot projects. When buildId is omitted, shows recent builds with their status(default 3) - use this to find the latest builds when troubleshooting. When buildId is provided, shows detailed error information for that specific build. Displays buildErrorMessage and subbuild failures to help diagnose build issues. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.',
159
159
  inputSchema,
160
160
  annotations: {
161
161
  readOnlyHint: true,
@@ -72,7 +72,7 @@ export class UploadProjectTools extends Tool {
72
72
  register() {
73
73
  return this.mcpServer.registerTool(toolName, {
74
74
  title: 'Upload HubSpot Project',
75
- description: 'DO NOT run this tool unless the user specifies they would like to upload the project, it is potentially destructive. Uploads the HubSpot project in current working directory. If the project does not exist, it will be created. MUST be ran from within the project directory. IMPORTANT: Uploading a project does NOT automatically make cards live or visible to users. Cards must be manually added to a view in HubSpot after upload to become visible.',
75
+ description: 'DO NOT run this tool unless the user specifies they would like to upload the project, it is potentially destructive. Uploads the HubSpot project in current working directory. If the project does not exist, it will be created. MUST be ran from within the project directory. IMPORTANT: Uploading a project does NOT automatically make cards live or visible to users. Cards must be manually added to a view in HubSpot after upload to become visible. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.',
76
76
  inputSchema,
77
77
  annotations: {
78
78
  readOnlyHint: false,
@@ -33,7 +33,7 @@ export class ValidateProjectTool extends Tool {
33
33
  register() {
34
34
  return this.mcpServer.registerTool(toolName, {
35
35
  title: 'Validate HubSpot Project',
36
- description: 'Validates the HubSpot project and its configuration files. This tool does not need to be ran before uploading the project',
36
+ description: 'Validates the HubSpot project and its configuration files. This tool does not need to be ran before uploading the project. If you do not know the project path, use the find-projects tool first to locate HubSpot projects in the workspace.',
37
37
  inputSchema,
38
38
  annotations: {
39
39
  readOnlyHint: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.0.11-experimental.2",
3
+ "version": "8.0.12-experimental.1",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
@@ -10,8 +10,8 @@
10
10
  "!**/__tests__/**"
11
11
  ],
12
12
  "dependencies": {
13
- "@hubspot/local-dev-lib": "5.3.3",
14
- "@hubspot/project-parsing-lib": "0.2.1-experimental.0",
13
+ "@hubspot/local-dev-lib": "0.7.9-experimental.0",
14
+ "@hubspot/project-parsing-lib": "0.16.0",
15
15
  "@hubspot/serverless-dev-runtime": "7.0.7",
16
16
  "@hubspot/ui-extensions-dev-server": "2.0.5",
17
17
  "@inquirer/prompts": "7.1.0",
@@ -21,14 +21,14 @@
21
21
  "chokidar": "3.6.0",
22
22
  "cli-cursor": "3.1.0",
23
23
  "cli-progress": "3.12.0",
24
- "express": "4.21.2",
24
+ "express": "4.22.1",
25
25
  "findup-sync": "4.0.0",
26
26
  "fs-extra": "8.1.0",
27
27
  "ink": "6.6.0",
28
28
  "ink-spinner": "5.0.0",
29
29
  "ink-text-input": "6.0.0",
30
- "js-yaml": "4.1.0",
31
- "minimatch": "10.0.1",
30
+ "js-yaml": "4.1.1",
31
+ "minimatch": "10.2.5",
32
32
  "moment": "2.30.1",
33
33
  "open": "7.4.2",
34
34
  "p-queue": "8.1.0",
@@ -59,7 +59,7 @@
59
59
  "@typescript-eslint/eslint-plugin": "^8.30.1",
60
60
  "@typescript-eslint/parser": "^8.11.0",
61
61
  "@vitest/coverage-v8": "^2.1.9",
62
- "axios": "1.14.0",
62
+ "axios": "1.15.2",
63
63
  "eslint": "^8.56.0",
64
64
  "eslint-plugin-import": "^2.31.0",
65
65
  "husky": "^4.3.8",
@@ -0,0 +1,32 @@
1
+ import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ import { HsSettingsFile } from '@hubspot/local-dev-lib/types/HsSettings';
3
+ import { CommonArgs, ConfigArgs } from './Yargs.js';
4
+ import { ArgumentsCamelCase } from 'yargs';
5
+ export type LinkArgs = CommonArgs & ConfigArgs;
6
+ export type LinkContext = {
7
+ globalAccountsList: HubSpotConfigAccount[];
8
+ globalDefaultAccount: HubSpotConfigAccount | undefined;
9
+ accountOverrideId: number | null;
10
+ preselectedAccountId?: number;
11
+ };
12
+ export declare const ACTION_RESULT_STATUS: {
13
+ readonly SUCCESS: "success";
14
+ readonly ERROR: "error";
15
+ readonly NOOP: "noop";
16
+ };
17
+ export type ActionResult = {
18
+ status: typeof ACTION_RESULT_STATUS.SUCCESS;
19
+ settings: HsSettingsFile;
20
+ } | {
21
+ status: typeof ACTION_RESULT_STATUS.ERROR;
22
+ reason: string;
23
+ } | {
24
+ status: typeof ACTION_RESULT_STATUS.NOOP;
25
+ };
26
+ export type ActionHandlerParams = {
27
+ state: HsSettingsFile;
28
+ context: LinkContext;
29
+ args: ArgumentsCamelCase<LinkArgs>;
30
+ };
31
+ export type ActionHandler = (params: ActionHandlerParams) => Promise<ActionResult>;
32
+ export type ActionName = 'link' | 'unlink' | 'authenticate' | 'cancel';
package/types/Link.js ADDED
@@ -0,0 +1,5 @@
1
+ export const ACTION_RESULT_STATUS = {
2
+ SUCCESS: 'success',
3
+ ERROR: 'error',
4
+ NOOP: 'noop',
5
+ };
@@ -5,6 +5,7 @@ export interface PackageJson {
5
5
  name: string;
6
6
  version?: string;
7
7
  workspaces?: string[];
8
+ scripts?: Record<string, string>;
8
9
  dependencies?: Record<string, string>;
9
10
  devDependencies?: Record<string, string>;
10
11
  }
@@ -5,6 +5,7 @@ export type GenericPromptResponse = {
5
5
  type PromptType = 'confirm' | 'list' | 'checkbox' | 'input' | 'password' | 'number' | 'rawlist';
6
6
  export type PromptChoices<T = any> = Array<string | {
7
7
  name: string;
8
+ short?: string;
8
9
  value?: T;
9
10
  disabled?: string | boolean;
10
11
  checked?: boolean;
package/types/Yargs.d.ts CHANGED
@@ -46,6 +46,7 @@ export type ProjectDevArgs = CommonArgs & ConfigArgs & EnvironmentArgs & {
46
46
  profile?: string;
47
47
  testingAccount?: string | number;
48
48
  projectAccount?: string | number;
49
+ port?: number;
49
50
  };
50
51
  export type TestingArgs = {
51
52
  qa?: boolean;