@hubspot/cli 8.2.0-experimental.2 → 8.2.1-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 (147) hide show
  1. package/commands/account/clean.js +1 -1
  2. package/commands/account/list.js +1 -1
  3. package/commands/account/remove.js +1 -1
  4. package/commands/account/rename.js +1 -1
  5. package/commands/account/use.js +1 -1
  6. package/commands/api.d.ts +4 -1
  7. package/commands/api.js +11 -14
  8. package/commands/config/set.js +24 -5
  9. package/commands/customObject/createSchema.js +2 -3
  10. package/commands/customObject/updateSchema.js +2 -3
  11. package/commands/getStarted.js +1 -0
  12. package/commands/hubdb/list.d.ts +1 -1
  13. package/commands/hubdb/list.js +7 -8
  14. package/commands/mcp/setup.d.ts +2 -0
  15. package/commands/mcp/setup.js +29 -1
  16. package/commands/project/appInstallStatus.d.ts +2 -1
  17. package/commands/project/appInstallStatus.js +21 -19
  18. package/commands/project/create.js +0 -1
  19. package/commands/project/deploy.d.ts +2 -1
  20. package/commands/project/deploy.js +31 -14
  21. package/commands/project/dev/index.js +18 -64
  22. package/commands/project/dev/unifiedFlow.js +3 -1
  23. package/commands/project/info.d.ts +2 -1
  24. package/commands/project/info.js +8 -6
  25. package/commands/project/installApp.d.ts +2 -1
  26. package/commands/project/installApp.js +26 -27
  27. package/commands/project/list.js +1 -1
  28. package/commands/project/listBuilds.d.ts +1 -1
  29. package/commands/project/listBuilds.js +8 -6
  30. package/commands/project/logs.js +2 -4
  31. package/commands/project/profile/add.d.ts +2 -1
  32. package/commands/project/profile/add.js +22 -1
  33. package/commands/project/profile/delete.d.ts +1 -1
  34. package/commands/project/release/create.d.ts +2 -1
  35. package/commands/project/release/create.js +68 -69
  36. package/commands/project/release/info.d.ts +2 -1
  37. package/commands/project/release/info.js +7 -6
  38. package/commands/project/release/list.d.ts +2 -1
  39. package/commands/project/release/list.js +15 -7
  40. package/commands/project/upload.d.ts +4 -1
  41. package/commands/project/upload.js +48 -21
  42. package/commands/project/validate.d.ts +1 -1
  43. package/commands/project/watch.js +3 -2
  44. package/commands/project.js +2 -2
  45. package/commands/sandbox/delete.js +2 -3
  46. package/commands/testAccount/create.d.ts +2 -1
  47. package/commands/testAccount/create.js +7 -7
  48. package/commands/testAccount/importData.d.ts +1 -1
  49. package/lang/en.d.ts +61 -33
  50. package/lang/en.js +77 -48
  51. package/lib/app/urls.js +2 -3
  52. package/lib/commonOpts.js +6 -1
  53. package/lib/configOptions.d.ts +10 -10
  54. package/lib/configOptions.js +10 -11
  55. package/lib/constants.d.ts +2 -0
  56. package/lib/constants.js +2 -0
  57. package/lib/dependencyManagement.js +7 -7
  58. package/lib/getStartedV2Actions.js +2 -1
  59. package/lib/importData.js +3 -4
  60. package/lib/jsonOutput.d.ts +109 -0
  61. package/lib/jsonOutput.js +94 -0
  62. package/lib/links.js +6 -5
  63. package/lib/mcp/clients.d.ts +1 -1
  64. package/lib/mcp/clients.js +8 -1
  65. package/lib/mcp/setup.d.ts +9 -4
  66. package/lib/mcp/setup.js +115 -35
  67. package/lib/npm/npmCli.d.ts +1 -0
  68. package/lib/npm/npmCli.js +13 -0
  69. package/lib/projects/builds.d.ts +2 -0
  70. package/lib/projects/builds.js +22 -0
  71. package/lib/projects/create/v2.js +1 -0
  72. package/lib/projects/installApp.d.ts +1 -1
  73. package/lib/projects/installApp.js +8 -3
  74. package/lib/projects/localDev/DevSessionManager.d.ts +2 -2
  75. package/lib/projects/localDev/DevSessionManager.js +4 -30
  76. package/lib/projects/localDev/LocalDevProcess.js +7 -5
  77. package/lib/projects/localDev/helpers/account.d.ts +0 -7
  78. package/lib/projects/localDev/helpers/account.js +1 -80
  79. package/lib/projects/localDev/helpers/project.js +3 -2
  80. package/lib/projects/release.d.ts +4 -0
  81. package/lib/projects/release.js +90 -0
  82. package/lib/projects/ui.d.ts +1 -0
  83. package/lib/projects/ui.js +14 -0
  84. package/lib/projects/upload.d.ts +10 -2
  85. package/lib/projects/upload.js +14 -7
  86. package/lib/projects/urls.d.ts +0 -1
  87. package/lib/projects/urls.js +6 -13
  88. package/lib/yargs/makeWrappedYargsHandler.d.ts +5 -1
  89. package/lib/yargs/makeWrappedYargsHandler.js +123 -65
  90. package/mcp-server/Tool.d.ts +8 -3
  91. package/mcp-server/Tool.js +58 -32
  92. package/mcp-server/server.js +10 -6
  93. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +2 -2
  94. package/mcp-server/tools/cms/HsCreateFunctionTool.js +10 -17
  95. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +2 -2
  96. package/mcp-server/tools/cms/HsCreateModuleTool.js +12 -18
  97. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +2 -2
  98. package/mcp-server/tools/cms/HsCreateTemplateTool.js +8 -10
  99. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +2 -2
  100. package/mcp-server/tools/cms/HsFunctionLogsTool.js +9 -9
  101. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +2 -2
  102. package/mcp-server/tools/cms/HsListFunctionsTool.js +7 -7
  103. package/mcp-server/tools/cms/HsListTool.d.ts +2 -2
  104. package/mcp-server/tools/cms/HsListTool.js +6 -10
  105. package/mcp-server/tools/index.js +2 -0
  106. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -2
  107. package/mcp-server/tools/project/AddFeatureToProjectTool.js +8 -9
  108. package/mcp-server/tools/project/AuthAccountTool.d.ts +18 -0
  109. package/mcp-server/tools/project/AuthAccountTool.js +78 -0
  110. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -2
  111. package/mcp-server/tools/project/CreateProjectTool.js +13 -12
  112. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +2 -2
  113. package/mcp-server/tools/project/CreateTestAccountTool.js +14 -15
  114. package/mcp-server/tools/project/DeployProjectTool.d.ts +2 -2
  115. package/mcp-server/tools/project/DeployProjectTool.js +10 -10
  116. package/mcp-server/tools/project/DocFetchTool.js +1 -1
  117. package/mcp-server/tools/project/DocsSearchTool.js +8 -7
  118. package/mcp-server/tools/project/FindProjectsTool.js +1 -1
  119. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +2 -1
  120. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +9 -8
  121. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +2 -1
  122. package/mcp-server/tools/project/GetApplicationInfoTool.js +12 -9
  123. package/mcp-server/tools/project/GetBuildLogsTool.js +11 -7
  124. package/mcp-server/tools/project/GetBuildStatusTool.js +11 -7
  125. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +2 -1
  126. package/mcp-server/tools/project/GetConfigValuesTool.js +9 -7
  127. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  128. package/mcp-server/tools/project/UploadProjectTools.d.ts +2 -2
  129. package/mcp-server/tools/project/UploadProjectTools.js +9 -8
  130. package/mcp-server/tools/project/ValidateProjectTool.d.ts +2 -2
  131. package/mcp-server/tools/project/ValidateProjectTool.js +5 -4
  132. package/mcp-server/tools/project/constants.d.ts +1 -0
  133. package/mcp-server/tools/project/constants.js +2 -1
  134. package/mcp-server/utils/command.d.ts +25 -2
  135. package/mcp-server/utils/command.js +67 -17
  136. package/mcp-server/utils/config.js +5 -0
  137. package/mcp-server/utils/logger.d.ts +3 -0
  138. package/mcp-server/utils/logger.js +30 -0
  139. package/mcp-server/utils/toolUsageTracking.js +18 -3
  140. package/package.json +11 -9
  141. package/types/Yargs.d.ts +3 -1
  142. package/commands/project/dev/deprecatedFlow.d.ts +0 -11
  143. package/commands/project/dev/deprecatedFlow.js +0 -180
  144. package/lib/projects/localDev/DevServerManager_DEPRECATED.d.ts +0 -43
  145. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +0 -128
  146. package/lib/projects/localDev/LocalDevManager_DEPRECATED.d.ts +0 -64
  147. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +0 -394
@@ -0,0 +1,90 @@
1
+ import chalk from 'chalk';
2
+ import { fetchProjectBuilds, getBuildStatus, } from '@hubspot/local-dev-lib/api/projects';
3
+ import { BUILD_STATUS } from '@hubspot/local-dev-lib/enums/build';
4
+ import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
5
+ import { meetsMinimumPlatformVersion } from '@hubspot/project-parsing-lib/projects';
6
+ import { PLATFORM_VERSIONS } from '@hubspot/project-parsing-lib/constants';
7
+ import { getLastSuccessfulBuild } from './builds.js';
8
+ import { logError, ApiErrorContext } from '../errorHandlers/index.js';
9
+ import { uiLogger } from '../ui/logger.js';
10
+ import { listPrompt } from '../prompts/promptUtils.js';
11
+ import { commands } from '../../lang/en.js';
12
+ import { createRelease } from '../../api/releases.js';
13
+ function buildChoiceName(build) {
14
+ const base = build.uploadMessage
15
+ ? `${build.buildId} — ${build.uploadMessage}`
16
+ : `${build.buildId} — ${commands.project.release.create.noUploadMessage}`;
17
+ return build.status !== BUILD_STATUS.SUCCESS
18
+ ? `[${chalk.yellow('DISABLED')}] ${base}`
19
+ : base;
20
+ }
21
+ export async function resolveBuildId(accountId, projectName, buildOption, force) {
22
+ if (buildOption) {
23
+ return buildOption;
24
+ }
25
+ if (force) {
26
+ const lastSuccessful = await getLastSuccessfulBuild(accountId, projectName);
27
+ return lastSuccessful ? lastSuccessful.buildId : null;
28
+ }
29
+ let results;
30
+ try {
31
+ ({
32
+ data: { results },
33
+ } = await fetchProjectBuilds(accountId, projectName));
34
+ }
35
+ catch (e) {
36
+ if (isSpecifiedError(e, { statusCode: 404 }) ||
37
+ isSpecifiedError(e, { statusCode: 400 })) {
38
+ return undefined;
39
+ }
40
+ throw e;
41
+ }
42
+ if (results.length === 0) {
43
+ return undefined;
44
+ }
45
+ const lastSuccessful = await getLastSuccessfulBuild(accountId, projectName);
46
+ if (!lastSuccessful) {
47
+ uiLogger.error(commands.project.release.create.errors.noSuccessfulBuilds(projectName));
48
+ return null;
49
+ }
50
+ return listPrompt(commands.project.release.create.buildIdPrompt, {
51
+ choices: results.map(b => ({
52
+ name: buildChoiceName(b),
53
+ value: b.buildId,
54
+ disabled: b.status !== BUILD_STATUS.SUCCESS
55
+ ? `– ${commands.project.release.create.buildStatus[b.status] ?? b.status}`
56
+ : undefined,
57
+ })),
58
+ });
59
+ }
60
+ export async function validateBuildForRelease(accountId, projectName, buildId) {
61
+ try {
62
+ const { data: build } = await getBuildStatus(accountId, projectName, buildId);
63
+ return meetsMinimumPlatformVersion(build.platformVersion, PLATFORM_VERSIONS.v2026_09_BETA);
64
+ }
65
+ catch (e) {
66
+ if (isSpecifiedError(e, { statusCode: 404 })) {
67
+ uiLogger.error(commands.project.release.create.errors.buildNotFound(buildId, projectName));
68
+ }
69
+ else {
70
+ logError(e, new ApiErrorContext({
71
+ accountId,
72
+ request: 'project release create',
73
+ }));
74
+ }
75
+ throw e;
76
+ }
77
+ }
78
+ export async function executeRelease(accountId, projectName, buildId) {
79
+ try {
80
+ const { data: release } = await createRelease(accountId, projectName, buildId);
81
+ return release;
82
+ }
83
+ catch (e) {
84
+ logError(e, new ApiErrorContext({
85
+ accountId,
86
+ request: 'project release create',
87
+ }));
88
+ throw e;
89
+ }
90
+ }
@@ -1 +1,2 @@
1
1
  export declare function logFeedbackMessage(buildId: number): void;
2
+ export declare function warnAboutSkippedHsMetaFiles(skippedHsMetaFiles: string[], force: boolean, isUpload?: boolean): Promise<boolean>;
@@ -2,6 +2,7 @@ import { uiLogger } from '../ui/logger.js';
2
2
  import { FEEDBACK_INTERVAL } from '../constants.js';
3
3
  import { uiLine } from '../ui/index.js';
4
4
  import { lib } from '../../lang/en.js';
5
+ import { confirmPrompt } from '../prompts/promptUtils.js';
5
6
  export function logFeedbackMessage(buildId) {
6
7
  if (buildId > 0 && buildId % FEEDBACK_INTERVAL === 0) {
7
8
  uiLine();
@@ -10,3 +11,16 @@ export function logFeedbackMessage(buildId) {
10
11
  uiLogger.log(lib.projects.logFeedbackMessage.feedbackMessage);
11
12
  }
12
13
  }
14
+ export async function warnAboutSkippedHsMetaFiles(skippedHsMetaFiles, force, isUpload = true) {
15
+ if (skippedHsMetaFiles.length === 0)
16
+ return true;
17
+ const message = isUpload
18
+ ? lib.projects.skippedHsMetaFiles.uploadWarning(skippedHsMetaFiles)
19
+ : lib.projects.skippedHsMetaFiles.warning(skippedHsMetaFiles);
20
+ uiLogger.warn(message);
21
+ if (force)
22
+ return true;
23
+ return confirmPrompt(lib.projects.skippedHsMetaFiles.prompt, {
24
+ defaultAnswer: false,
25
+ });
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { FileResult } from 'tmp';
2
+ import { type IntermediateRepresentation } from '@hubspot/project-parsing-lib/translate';
2
3
  import { ProjectConfig } from '../../types/Projects.js';
3
4
  type ProjectUploadCallbackFunction<T> = (accountId: number, projectConfig: ProjectConfig, tempFile: FileResult, buildId: number) => Promise<T>;
4
5
  type ProjectUploadResult<T> = {
@@ -6,6 +7,7 @@ type ProjectUploadResult<T> = {
6
7
  uploadError?: unknown;
7
8
  projectNotFound?: boolean;
8
9
  projectId?: number;
10
+ userDeclined?: boolean;
9
11
  };
10
12
  type HandleProjectUploadArg<T> = {
11
13
  accountId: number;
@@ -18,9 +20,11 @@ type HandleProjectUploadArg<T> = {
18
20
  sendIR?: boolean;
19
21
  skipValidation?: boolean;
20
22
  skipNpmAudit?: boolean;
23
+ skipAutoDeploy?: boolean;
21
24
  profile?: string;
25
+ force?: boolean;
22
26
  };
23
- export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, skipNpmAudit, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
27
+ export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, skipNpmAudit, skipAutoDeploy, force, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
24
28
  export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig, projectDir: string): Promise<void>;
25
29
  export declare function validateNoHSMetaMismatch(srcDir: string, projectConfig: ProjectConfig): Promise<void>;
26
30
  type HandleTranslateArg = {
@@ -31,5 +35,9 @@ type HandleTranslateArg = {
31
35
  profile?: string;
32
36
  includeTranslationErrorMessage?: boolean;
33
37
  };
34
- export declare function handleTranslate({ projectDir, projectConfig, accountId, skipValidation, profile, includeTranslationErrorMessage, }: HandleTranslateArg): Promise<unknown>;
38
+ export type HandleTranslateResult = {
39
+ intermediateRepresentation: IntermediateRepresentation;
40
+ skippedHsMetaFiles: string[];
41
+ };
42
+ export declare function handleTranslate({ projectDir, projectConfig, accountId, skipValidation, profile, includeTranslationErrorMessage, }: HandleTranslateArg): Promise<HandleTranslateResult>;
35
43
  export {};
@@ -9,6 +9,7 @@ import { projectContainsHsMetaFiles } from '@hubspot/project-parsing-lib/project
9
9
  import { findAndParsePackageJsonFiles, collectWorkspaceDirectories, collectFileDependencies, } from '@hubspot/project-parsing-lib/workspaces';
10
10
  import SpinniesManager from '../ui/SpinniesManager.js';
11
11
  import { uiAccountDescription } from '../ui/index.js';
12
+ import { warnAboutSkippedHsMetaFiles } from './ui.js';
12
13
  import util from 'node:util';
13
14
  import { lib } from '../../lang/en.js';
14
15
  import { ensureProjectExists } from './ensureProjectExists.js';
@@ -20,7 +21,7 @@ import { archiveWorkspacesAndDependencies, getPackageJsonPathsToUpdate, getLockf
20
21
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
21
22
  import { validateLintConfigOnUpload } from './validateLintConfigOnUpload.js';
22
23
  import { runNpmAuditsBeforeProjectUpload } from './npmAuditOnUpload.js';
23
- async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
24
+ async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation, skipAutoDeploy) {
24
25
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
25
26
  SpinniesManager.add('upload', {
26
27
  text: lib.projectUpload.uploadProjectFiles.add(projectName, accountIdentifier),
@@ -29,7 +30,7 @@ async function uploadProjectFiles(accountId, projectName, filePath, uploadMessag
29
30
  let buildId;
30
31
  let error;
31
32
  try {
32
- const { data: upload } = await uploadProject(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation);
33
+ const { data: upload } = await uploadProject(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation, skipAutoDeploy);
33
34
  buildId = upload.buildId;
34
35
  SpinniesManager.succeed('upload', {
35
36
  text: lib.projectUpload.uploadProjectFiles.succeed(projectName, accountIdentifier),
@@ -46,7 +47,7 @@ async function uploadProjectFiles(accountId, projectName, filePath, uploadMessag
46
47
  }
47
48
  return { buildId, error };
48
49
  }
49
- export async function handleProjectUpload({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage = '', forceCreate = false, isUploadCommand = false, sendIR = false, skipValidation = false, skipNpmAudit = false, }) {
50
+ export async function handleProjectUpload({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage = '', forceCreate = false, isUploadCommand = false, sendIR = false, skipValidation = false, skipNpmAudit = false, skipAutoDeploy = false, force = false, }) {
50
51
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
51
52
  await validateSourceDirectory(srcDir, projectConfig, projectDir);
52
53
  await validateNoHSMetaMismatch(srcDir, projectConfig);
@@ -86,13 +87,19 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
86
87
  let intermediateRepresentation;
87
88
  if (sendIR) {
88
89
  try {
89
- intermediateRepresentation = await handleTranslate({
90
+ const translateResult = await handleTranslate({
90
91
  projectDir,
91
92
  projectConfig,
92
93
  accountId,
93
94
  skipValidation,
94
95
  profile,
95
96
  });
97
+ intermediateRepresentation =
98
+ translateResult.intermediateRepresentation;
99
+ const shouldContinue = await warnAboutSkippedHsMetaFiles(translateResult.skippedHsMetaFiles, force);
100
+ if (!shouldContinue) {
101
+ return resolve({ userDeclined: true });
102
+ }
96
103
  }
97
104
  catch (e) {
98
105
  return resolve({ uploadError: e });
@@ -108,7 +115,7 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
108
115
  return resolve({ projectNotFound: true });
109
116
  }
110
117
  const projectId = project?.id;
111
- const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation);
118
+ const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation, skipAutoDeploy);
112
119
  if (error) {
113
120
  resolve({ uploadError: error, projectId });
114
121
  }
@@ -169,13 +176,13 @@ export async function validateNoHSMetaMismatch(srcDir, projectConfig) {
169
176
  }
170
177
  export async function handleTranslate({ projectDir, projectConfig, accountId, skipValidation, profile, includeTranslationErrorMessage = true, }) {
171
178
  try {
172
- const intermediateRepresentation = await translate({
179
+ const { intermediateRepresentation, skippedHsMetaFiles } = await translate({
173
180
  projectSourceDir: path.join(projectDir, projectConfig.srcDir),
174
181
  platformVersion: projectConfig.platformVersion,
175
182
  accountId,
176
183
  }, { skipValidation, profile });
177
184
  uiLogger.debug(util.inspect(intermediateRepresentation, false, null, true));
178
- return intermediateRepresentation;
185
+ return { intermediateRepresentation, skippedHsMetaFiles };
179
186
  }
180
187
  catch (e) {
181
188
  if (isTranslationError(e)) {
@@ -1,4 +1,3 @@
1
- export declare function getBaseHubSpotUrlForAccount(accountId: number): string;
2
1
  export declare function getProjectComponentDistributionUrl(projectName: string, componentName: string, accountId: number): string;
3
2
  export declare function getDeveloperOverviewUrl(accountId: number): string;
4
3
  export declare function getProjectDetailUrl(projectName: string, accountId: number): string | undefined;
@@ -1,18 +1,12 @@
1
- import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
2
- import { getConfigAccountEnvironment } from '@hubspot/local-dev-lib/config';
3
- export function getBaseHubSpotUrlForAccount(accountId) {
4
- return getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId));
5
- }
1
+ import { getHubSpotWebsiteOriginByAccountId } from '@hubspot/local-dev-lib/urls';
6
2
  function getProjectHomeUrl(accountId) {
7
- return `${getBaseHubSpotUrlForAccount(accountId)}/developer-projects/${accountId}`;
3
+ return `${getHubSpotWebsiteOriginByAccountId(accountId)}/developer-projects/${accountId}`;
8
4
  }
9
5
  export function getProjectComponentDistributionUrl(projectName, componentName, accountId) {
10
- const baseUrl = getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId));
11
- return `${baseUrl}/developer-projects/${accountId}/project/${projectName}/component/${componentName}/distribution`;
6
+ return `${getHubSpotWebsiteOriginByAccountId(accountId)}/developer-projects/${accountId}/project/${projectName}/component/${componentName}/distribution`;
12
7
  }
13
8
  export function getDeveloperOverviewUrl(accountId) {
14
- const baseUrl = getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId));
15
- return `${baseUrl}/developer-overview/${accountId}`;
9
+ return `${getHubSpotWebsiteOriginByAccountId(accountId)}/developer-overview/${accountId}`;
16
10
  }
17
11
  export function getProjectDetailUrl(projectName, accountId) {
18
12
  if (!projectName)
@@ -34,9 +28,8 @@ export function getProjectDeployDetailUrl(projectName, deployId, accountId) {
34
28
  return `${getProjectActivityUrl(projectName, accountId)}/deploy/${deployId}`;
35
29
  }
36
30
  export function getLocalDevUiUrl(accountId, showWelcomeScreen) {
37
- return `${getBaseHubSpotUrlForAccount(accountId)}/developer-projects-local-dev/${accountId}${showWelcomeScreen ? '?welcome' : ''}`;
31
+ return `${getHubSpotWebsiteOriginByAccountId(accountId)}/developer-projects-local-dev/${accountId}${showWelcomeScreen ? '?welcome' : ''}`;
38
32
  }
39
33
  export function getAccountHomeUrl(accountId) {
40
- const baseUrl = getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId));
41
- return `${baseUrl}/home?portalId=${accountId}`;
34
+ return `${getHubSpotWebsiteOriginByAccountId(accountId)}/home?portalId=${accountId}`;
42
35
  }
@@ -1,3 +1,7 @@
1
1
  import { ArgumentsCamelCase } from 'yargs';
2
+ import { z } from 'zod';
2
3
  import { CommonArgs } from '../../types/Yargs.js';
3
- export declare function makeWrappedYargsHandler<T extends CommonArgs>(trackingName: string, handler: (args: ArgumentsCamelCase<T>) => Promise<void>): (args: ArgumentsCamelCase<T>) => Promise<void>;
4
+ export type WrappedHandlerOptions = {
5
+ jsonOutputSchema?: z.ZodType;
6
+ };
7
+ export declare function makeWrappedYargsHandler<T extends CommonArgs>(trackingName: string, handler: (args: ArgumentsCamelCase<T>) => Promise<void>, options?: WrappedHandlerOptions): (args: ArgumentsCamelCase<T>) => Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
+ import { z } from 'zod';
3
4
  import { getConfig } from '@hubspot/local-dev-lib/config';
4
5
  import { getStateValue, setStateValue, } from '@hubspot/local-dev-lib/config/state';
5
6
  import { STATE_FLAGS } from '@hubspot/local-dev-lib/constants/config';
@@ -11,7 +12,8 @@ import { lib } from '../../lang/en.js';
11
12
  import { EXIT_CODES } from '../enums/exitCodes.js';
12
13
  import { isPromptExitError } from '../errors/PromptExitError.js';
13
14
  import { debugError } from '../errorHandlers/index.js';
14
- const HANDLER_LOG_DIR = path.join(os.homedir(), '.hscli', 'logs');
15
+ import { MAX_LOG_FILES } from '../constants.js';
16
+ const HANDLER_LOG_DIR = path.join(os.homedir(), '.hscli', 'logs', 'cli');
15
17
  function logUsageTrackingMessage(isJsonOutput) {
16
18
  if (isJsonOutput) {
17
19
  return;
@@ -32,90 +34,146 @@ function logUsageTrackingMessage(isJsonOutput) {
32
34
  return;
33
35
  }
34
36
  }
35
- export function makeWrappedYargsHandler(trackingName, handler) {
36
- return async (args) => {
37
- const startTime = Date.now();
38
- const meta = {};
39
- let trackingFired = false;
40
- const trackingArgs = args;
41
- const addUsageMetadata = (newMeta) => {
42
- Object.assign(meta, newMeta);
43
- };
44
- trackingArgs.addUsageMetadata = addUsageMetadata;
45
- const trackCommandUsage = async (successful) => {
46
- if (trackingFired) {
47
- return;
48
- }
49
- trackingFired = true;
50
- try {
51
- const { accountId: overrideAccountId, ...trackingMeta } = meta;
52
- trackingMeta.successful = successful;
53
- trackingMeta.executionTime = Date.now() - startTime;
54
- await _trackCommandUsage(trackingName, trackingMeta, overrideAccountId ?? args.derivedAccountId);
37
+ function createUsageTracker(trackingName, derivedAccountId) {
38
+ const startTime = Date.now();
39
+ const meta = {};
40
+ let fired = false;
41
+ const addMetadata = (newMeta) => {
42
+ Object.assign(meta, newMeta);
43
+ };
44
+ const track = async (successful) => {
45
+ if (fired) {
46
+ return;
47
+ }
48
+ fired = true;
49
+ try {
50
+ const { accountId: overrideAccountId, ...trackingMeta } = meta;
51
+ trackingMeta.successful = successful;
52
+ trackingMeta.executionTime = Date.now() - startTime;
53
+ await _trackCommandUsage(trackingName, trackingMeta, overrideAccountId ?? derivedAccountId);
54
+ }
55
+ catch (_e) { }
56
+ };
57
+ const onForcedExit = () => {
58
+ process.exit(EXIT_CODES.SUCCESS);
59
+ };
60
+ const onSigint = async () => {
61
+ process.removeListener('SIGINT', onSigint);
62
+ process.on('SIGINT', onForcedExit);
63
+ try {
64
+ await track(false);
65
+ }
66
+ catch (_e) { }
67
+ process.removeListener('SIGINT', onForcedExit);
68
+ process.exit(EXIT_CODES.SUCCESS);
69
+ };
70
+ process.on('SIGINT', onSigint);
71
+ const trackAndCleanup = async (successful) => {
72
+ await track(successful);
73
+ process.removeListener('SIGINT', onSigint);
74
+ process.removeListener('SIGINT', onForcedExit);
75
+ };
76
+ return { addMetadata, trackAndCleanup };
77
+ }
78
+ function createJsonOutputManager(isJsonOutput, schema) {
79
+ const data = {};
80
+ let emitted = false;
81
+ const add = (newData) => {
82
+ Object.assign(data, newData);
83
+ };
84
+ const emit = () => {
85
+ if (!emitted && isJsonOutput && Object.keys(data).length > 0) {
86
+ emitted = true;
87
+ if (schema) {
88
+ const result = schema.safeParse(data);
89
+ if (!result.success) {
90
+ uiLogger.json(data);
91
+ uiLogger.warn(lib.jsonSchema.validationFailed);
92
+ return false;
93
+ }
55
94
  }
56
- catch (_e) { }
57
- };
58
- const onForcedExit = () => {
59
- process.exit(EXIT_CODES.SUCCESS);
60
- };
61
- const onSigint = async () => {
62
- process.removeListener('SIGINT', onSigint);
63
- process.on('SIGINT', onForcedExit);
64
- try {
65
- await trackCommandUsage(false);
95
+ uiLogger.json(data);
96
+ }
97
+ return true;
98
+ };
99
+ return { add, emit };
100
+ }
101
+ function createLogFileWriter(trackingName, isJsonOutput) {
102
+ const writeLogFile = () => {
103
+ if (isJsonOutput) {
104
+ return null;
105
+ }
106
+ return ldlLogger.writeBufferedLogsToFile({
107
+ dir: HANDLER_LOG_DIR,
108
+ filenamePrefix: trackingName,
109
+ maxFiles: MAX_LOG_FILES,
110
+ });
111
+ };
112
+ const writeFailureLogFile = () => {
113
+ const savedPath = writeLogFile();
114
+ if (savedPath) {
115
+ uiLogger.log('');
116
+ uiLogger.error(lib.handlerLogFile.saved(savedPath));
117
+ }
118
+ };
119
+ return { writeLogFile, writeFailureLogFile };
120
+ }
121
+ export function makeWrappedYargsHandler(trackingName, handler, options) {
122
+ return async (args) => {
123
+ const wrappedHandlerArgs = args;
124
+ const isJsonOutput = Boolean(wrappedHandlerArgs.json || wrappedHandlerArgs.formatOutputAsJson);
125
+ const schema = options?.jsonOutputSchema;
126
+ const tracker = createUsageTracker(trackingName, args.derivedAccountId);
127
+ if (wrappedHandlerArgs.jsonSchema) {
128
+ if (schema) {
129
+ uiLogger.json(z.toJSONSchema(schema));
66
130
  }
67
- catch (_e) { }
68
- process.removeListener('SIGINT', onForcedExit);
69
- process.exit(EXIT_CODES.SUCCESS);
70
- };
71
- process.on('SIGINT', onSigint);
72
- const trackCommandUsageAndRemoveListeners = async (successful) => {
73
- await trackCommandUsage(successful);
74
- process.removeListener('SIGINT', onSigint);
75
- process.removeListener('SIGINT', onForcedExit);
76
- };
77
- const jsonArgs = args;
78
- const isJsonOutput = Boolean(jsonArgs.json || jsonArgs.formatOutputAsJson);
79
- const writeFailureLogFile = () => {
80
- // Skip in JSON output modes so the side effect + stderr message don't
81
- // interfere with structured output consumers.
82
- if (isJsonOutput) {
83
- return;
131
+ else {
132
+ uiLogger.json({ error: lib.jsonSchema.noSchemaForCommand });
84
133
  }
85
- const savedPath = ldlLogger.writeBufferedLogsToFile({
86
- dir: HANDLER_LOG_DIR,
87
- filenamePrefix: trackingName,
88
- });
89
- if (savedPath) {
90
- uiLogger.log('');
91
- uiLogger.error(lib.handlerLogFile.saved(savedPath));
134
+ await tracker.trackAndCleanup(true);
135
+ return process.exit(EXIT_CODES.SUCCESS);
136
+ }
137
+ const json = createJsonOutputManager(isJsonOutput, schema);
138
+ const logs = createLogFileWriter(trackingName, isJsonOutput);
139
+ wrappedHandlerArgs.addUsageMetadata = tracker.addMetadata;
140
+ wrappedHandlerArgs.addJsonOutput = json.add;
141
+ wrappedHandlerArgs.exit = async (code) => {
142
+ const jsonValid = json.emit();
143
+ const exitCode = !jsonValid && code === EXIT_CODES.SUCCESS ? EXIT_CODES.WARNING : code;
144
+ await tracker.trackAndCleanup(exitCode !== EXIT_CODES.ERROR);
145
+ if (exitCode === EXIT_CODES.ERROR) {
146
+ logs.writeFailureLogFile();
92
147
  }
93
- };
94
- trackingArgs.exit = async (code) => {
95
- await trackCommandUsageAndRemoveListeners(code !== EXIT_CODES.ERROR);
96
- if (code === EXIT_CODES.ERROR) {
97
- writeFailureLogFile();
148
+ else {
149
+ logs.writeLogFile();
98
150
  }
99
- return process.exit(code);
151
+ return process.exit(exitCode);
100
152
  };
101
153
  logUsageTrackingMessage(isJsonOutput);
102
154
  try {
103
- await handler(trackingArgs);
155
+ await handler(wrappedHandlerArgs);
104
156
  }
105
157
  catch (e) {
106
158
  const isSuccessfulPromptExit = isPromptExitError(e)
107
159
  ? e.exitCode !== EXIT_CODES.ERROR
108
160
  : false;
109
- await trackCommandUsageAndRemoveListeners(isSuccessfulPromptExit);
161
+ await tracker.trackAndCleanup(isSuccessfulPromptExit);
110
162
  if (isPromptExitError(e)) {
163
+ logs.writeLogFile();
111
164
  return process.exit(e.exitCode);
112
165
  }
113
166
  else {
114
167
  debugError(e);
115
- writeFailureLogFile();
168
+ logs.writeFailureLogFile();
116
169
  return process.exit(EXIT_CODES.ERROR);
117
170
  }
118
171
  }
119
- await trackCommandUsageAndRemoveListeners(true);
172
+ const jsonValid = json.emit();
173
+ await tracker.trackAndCleanup(true);
174
+ logs.writeLogFile();
175
+ if (!jsonValid) {
176
+ return process.exit(EXIT_CODES.WARNING);
177
+ }
120
178
  };
121
179
  }
@@ -1,15 +1,20 @@
1
1
  import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
+ import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js';
2
4
  import { McpLogger } from './utils/logger.js';
3
5
  import { TextContentResponse } from './types.js';
6
+ import { CommandResults, Command } from './utils/command.js';
7
+ export type ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
4
8
  export declare class Tool<InputSchema> {
5
9
  protected mcpServer: McpServer;
6
10
  protected logger: McpLogger;
7
11
  protected toolName: string;
8
12
  constructor(mcpServer: McpServer, logger: McpLogger, toolName: string);
9
13
  register(): RegisteredTool;
10
- handler(input: InputSchema): TextContentResponse | Promise<TextContentResponse>;
11
- protected getTrackingMeta(input: InputSchema): {
14
+ handler(_input: InputSchema): TextContentResponse | Promise<TextContentResponse>;
15
+ protected runCommand(directory: string, command: Command, extra?: ToolExtra): Promise<CommandResults>;
16
+ protected getTrackingMeta(_input: InputSchema): {
12
17
  [key: string]: string;
13
18
  } | undefined;
14
- protected wrappedHandler(input: InputSchema): Promise<TextContentResponse>;
19
+ protected wrappedHandler(input: InputSchema, extra?: ToolExtra): Promise<TextContentResponse>;
15
20
  }
@@ -1,6 +1,7 @@
1
1
  import { formatTextContents } from './utils/content.js';
2
2
  import { getErrorMessage } from '../lib/errorHandlers/index.js';
3
3
  import { trackToolUsage } from './utils/toolUsageTracking.js';
4
+ import { runCommandInDir } from './utils/command.js';
4
5
  export class Tool {
5
6
  mcpServer;
6
7
  logger;
@@ -13,41 +14,66 @@ export class Tool {
13
14
  register() {
14
15
  throw new Error('Must implement register');
15
16
  }
16
- handler(
17
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
- input) {
17
+ handler(_input) {
19
18
  throw new Error('Must implement handler');
20
19
  }
21
- getTrackingMeta(
22
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
- input) {
20
+ runCommand(directory, command, extra) {
21
+ let progressCount = 0;
22
+ return runCommandInDir(directory, command, async (chunk) => {
23
+ try {
24
+ const message = `${chunk.trimEnd()}`;
25
+ this.logger.debug(this.toolName, message);
26
+ const token = extra?._meta?.progressToken;
27
+ if (token !== undefined && extra?.sendNotification) {
28
+ progressCount++;
29
+ await extra.sendNotification({
30
+ method: 'notifications/progress',
31
+ params: {
32
+ progressToken: token,
33
+ progress: progressCount,
34
+ message,
35
+ },
36
+ });
37
+ }
38
+ }
39
+ catch {
40
+ // Swallow notification errors so a failed notification never crashes the tool
41
+ }
42
+ });
43
+ }
44
+ getTrackingMeta(_input) {
24
45
  return undefined;
25
46
  }
26
- async wrappedHandler(input) {
27
- const startTime = Date.now();
28
- try {
29
- // `input` is logged unredacted. Tool input schemas MUST NOT include
30
- // credentials or other sensitive values, since MCP clients (Claude
31
- // Desktop, Inspector, etc.) will display these logs.
32
- this.logger.debug(this.toolName, {
33
- message: 'Tool invoked',
34
- args: input,
35
- });
36
- await trackToolUsage(this.toolName, this.getTrackingMeta(input));
37
- const result = await this.handler(input);
38
- this.logger.debug(this.toolName, {
39
- message: 'Tool completed',
40
- durationMs: Date.now() - startTime,
41
- });
42
- return result;
43
- }
44
- catch (error) {
45
- this.logger.error(this.toolName, {
46
- message: 'Tool failed',
47
- error: error instanceof Error ? error.message : String(error),
48
- durationMs: Date.now() - startTime,
49
- });
50
- return formatTextContents(getErrorMessage(error));
51
- }
47
+ wrappedHandler(input, extra) {
48
+ return this.logger.runWithBuffer(async () => {
49
+ const startTime = Date.now();
50
+ try {
51
+ // `input` is logged unredacted. Tool input schemas MUST NOT include
52
+ // credentials or other sensitive values, since MCP clients (Claude
53
+ // Desktop, Inspector, etc.) will display these logs.
54
+ this.logger.debug(this.toolName, {
55
+ message: 'Tool invoked',
56
+ args: input,
57
+ });
58
+ await trackToolUsage(this.toolName, this.getTrackingMeta(input));
59
+ const result = await this.handler(input, extra);
60
+ this.logger.debug(this.toolName, {
61
+ message: 'Tool completed',
62
+ durationMs: Date.now() - startTime,
63
+ });
64
+ return result;
65
+ }
66
+ catch (error) {
67
+ this.logger.error(this.toolName, {
68
+ message: 'Tool failed',
69
+ error: error instanceof Error ? error.message : String(error),
70
+ durationMs: Date.now() - startTime,
71
+ });
72
+ return formatTextContents(getErrorMessage(error));
73
+ }
74
+ finally {
75
+ this.logger.flushLogsToFile(this.toolName);
76
+ }
77
+ });
52
78
  }
53
79
  }