@hubspot/cli 7.6.0-beta.7 → 7.6.0-beta.9

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 (41) hide show
  1. package/commands/__tests__/create.test.js +20 -0
  2. package/commands/create/function.js +2 -2
  3. package/commands/create/module.js +2 -2
  4. package/commands/create/template.js +2 -2
  5. package/commands/create.js +47 -0
  6. package/commands/getStarted.js +3 -2
  7. package/commands/project/deploy.js +31 -1
  8. package/lang/en.d.ts +40 -4
  9. package/lang/en.js +44 -7
  10. package/lang/en.lyaml +23 -2
  11. package/lib/process.js +15 -4
  12. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  13. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  14. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  15. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  16. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  17. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  18. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  19. package/lib/prompts/createFunctionPrompt.js +36 -7
  20. package/lib/prompts/createModulePrompt.d.ts +2 -1
  21. package/lib/prompts/createModulePrompt.js +48 -1
  22. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  23. package/lib/prompts/createTemplatePrompt.js +9 -1
  24. package/mcp-server/tools/index.js +4 -0
  25. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  26. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  27. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  28. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  29. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  30. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  31. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  32. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  33. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  34. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  35. package/mcp-server/tools/project/constants.d.ts +2 -0
  36. package/mcp-server/tools/project/constants.js +6 -0
  37. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  38. package/mcp-server/utils/toolUsageTracking.js +2 -1
  39. package/package.json +1 -1
  40. package/types/Cms.d.ts +16 -0
  41. package/types/Cms.js +25 -1
@@ -1,5 +1,6 @@
1
1
  import yargs from 'yargs';
2
2
  import createCommand from '../create.js';
3
+ import { TEMPLATE_TYPES, HTTP_METHODS } from '../../types/Cms.js';
3
4
  const positionalSpy = vi
4
5
  .spyOn(yargs, 'positional')
5
6
  .mockReturnValue(yargs);
@@ -28,6 +29,25 @@ describe('commands/create', () => {
28
29
  it('should support the correct options', () => {
29
30
  createCommand.builder(yargs);
30
31
  expect(optionSpy).toHaveBeenCalledWith('internal', expect.objectContaining({ type: 'boolean', hidden: true }));
32
+ // Template creation flags
33
+ expect(optionSpy).toHaveBeenCalledWith('template-type', expect.objectContaining({
34
+ type: 'string',
35
+ choices: [...TEMPLATE_TYPES],
36
+ }));
37
+ // Module creation flags
38
+ expect(optionSpy).toHaveBeenCalledWith('module-label', expect.objectContaining({ type: 'string' }));
39
+ expect(optionSpy).toHaveBeenCalledWith('react-type', expect.objectContaining({ type: 'boolean' }));
40
+ expect(optionSpy).toHaveBeenCalledWith('content-types', expect.objectContaining({ type: 'string' }));
41
+ expect(optionSpy).toHaveBeenCalledWith('global', expect.objectContaining({ type: 'boolean' }));
42
+ expect(optionSpy).toHaveBeenCalledWith('available-for-new-content', expect.objectContaining({ type: 'boolean' }));
43
+ // Function creation flags
44
+ expect(optionSpy).toHaveBeenCalledWith('functions-folder', expect.objectContaining({ type: 'string' }));
45
+ expect(optionSpy).toHaveBeenCalledWith('filename', expect.objectContaining({ type: 'string' }));
46
+ expect(optionSpy).toHaveBeenCalledWith('endpoint-method', expect.objectContaining({
47
+ type: 'string',
48
+ choices: [...HTTP_METHODS],
49
+ }));
50
+ expect(optionSpy).toHaveBeenCalledWith('endpoint-path', expect.objectContaining({ type: 'string' }));
31
51
  });
32
52
  });
33
53
  });
@@ -5,8 +5,8 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
5
5
  const functionAssetType = {
6
6
  hidden: false,
7
7
  dest: ({ name }) => name,
8
- execute: async ({ dest }) => {
9
- const functionDefinition = await createFunctionPrompt();
8
+ execute: async ({ dest, commandArgs }) => {
9
+ const functionDefinition = await createFunctionPrompt(commandArgs);
10
10
  try {
11
11
  await createFunction(functionDefinition, dest);
12
12
  }
@@ -14,8 +14,8 @@ const moduleAssetType = {
14
14
  }
15
15
  return true;
16
16
  },
17
- execute: async ({ name, dest, getInternalVersion }) => {
18
- const moduleDefinition = await createModulePrompt();
17
+ execute: async ({ name, dest, getInternalVersion, commandArgs }) => {
18
+ const moduleDefinition = await createModulePrompt(commandArgs);
19
19
  try {
20
20
  await createModule(moduleDefinition, name, dest, getInternalVersion);
21
21
  }
@@ -14,8 +14,8 @@ const templateAssetType = {
14
14
  }
15
15
  return true;
16
16
  },
17
- execute: async ({ name, dest }) => {
18
- const { templateType } = await createTemplatePrompt();
17
+ execute: async ({ name, dest, commandArgs }) => {
18
+ const { templateType } = await createTemplatePrompt(commandArgs);
19
19
  try {
20
20
  await createTemplate(name, dest, templateType);
21
21
  }
@@ -8,6 +8,7 @@ import { commands } from '../lang/en.js';
8
8
  import { uiLogger } from '../lib/ui/logger.js';
9
9
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
10
10
  import { EXIT_CODES } from '../lib/enums/exitCodes.js';
11
+ import { TEMPLATE_TYPES, HTTP_METHODS, CONTENT_TYPES } from '../types/Cms.js';
11
12
  const SUPPORTED_ASSET_TYPES = Object.keys(assets)
12
13
  .filter(t => !assets[t].hidden)
13
14
  .join(', ');
@@ -73,6 +74,52 @@ function createBuilder(yargs) {
73
74
  type: 'boolean',
74
75
  hidden: true,
75
76
  });
77
+ yargs.option('template-type', {
78
+ describe: commands.create.flags.templateType.describe,
79
+ type: 'string',
80
+ choices: [...TEMPLATE_TYPES],
81
+ });
82
+ yargs.option('module-label', {
83
+ describe: commands.create.flags.moduleLabel.describe,
84
+ type: 'string',
85
+ });
86
+ yargs.option('react-type', {
87
+ describe: commands.create.flags.reactType.describe,
88
+ type: 'boolean',
89
+ default: false,
90
+ });
91
+ yargs.option('content-types', {
92
+ describe: commands.create.flags.contentTypes.describe(CONTENT_TYPES),
93
+ type: 'string',
94
+ });
95
+ yargs.option('global', {
96
+ describe: commands.create.flags.global.describe,
97
+ type: 'boolean',
98
+ default: false,
99
+ });
100
+ yargs.option('available-for-new-content', {
101
+ describe: commands.create.flags.availableForNewContent.describe,
102
+ type: 'boolean',
103
+ default: true,
104
+ });
105
+ yargs.option('functions-folder', {
106
+ describe: commands.create.flags.functionsFolder.describe,
107
+ type: 'string',
108
+ });
109
+ yargs.option('filename', {
110
+ describe: commands.create.flags.filename.describe,
111
+ type: 'string',
112
+ });
113
+ yargs.option('endpoint-method', {
114
+ describe: commands.create.flags.endpointMethod.describe,
115
+ type: 'string',
116
+ choices: [...HTTP_METHODS],
117
+ default: 'GET',
118
+ });
119
+ yargs.option('endpoint-path', {
120
+ describe: commands.create.flags.endpointPath.describe,
121
+ type: 'string',
122
+ });
76
123
  return yargs;
77
124
  }
78
125
  const builder = makeYargsBuilder(createBuilder, command, describe, {
@@ -9,7 +9,7 @@ import { EXIT_CODES } from '../lib/enums/exitCodes.js';
9
9
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
10
10
  import { promptUser } from '../lib/prompts/promptUtils.js';
11
11
  import { projectNameAndDestPrompt } from '../lib/prompts/projectNameAndDestPrompt.js';
12
- import { uiFeatureHighlight, uiInfoSection } from '../lib/ui/index.js';
12
+ import { uiAccountDescription, uiFeatureHighlight, uiInfoSection, } from '../lib/ui/index.js';
13
13
  import { uiLogger } from '../lib/ui/logger.js';
14
14
  import { debugError, logError } from '../lib/errorHandlers/index.js';
15
15
  import { handleProjectUpload } from '../lib/projects/upload.js';
@@ -138,11 +138,12 @@ async function handler(args) {
138
138
  uiLogger.log(' ');
139
139
  }
140
140
  // 6. Ask user if they want to upload the project
141
+ const accountName = uiAccountDescription(derivedAccountId);
141
142
  const { shouldUpload } = await promptUser([
142
143
  {
143
144
  type: 'confirm',
144
145
  name: 'shouldUpload',
145
- message: commands.getStarted.prompts.uploadProject,
146
+ message: commands.getStarted.prompts.uploadProject(accountName),
146
147
  default: true,
147
148
  },
148
149
  ]);
@@ -29,6 +29,7 @@ function validateBuildId(buildId, deployedBuildId, latestBuildId, projectName, a
29
29
  function logDeployErrors(errorData) {
30
30
  uiLogger.error(errorData.message);
31
31
  errorData.errors.forEach(err => {
32
+ // This is how the pre-deploy check manifests itself in < 2025.2 projects
32
33
  if (err.subCategory === PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS) {
33
34
  uiLogger.log(commands.project.deploy.errors.deployContainsRemovals(err.context.COMPONENT_NAME));
34
35
  }
@@ -37,6 +38,29 @@ function logDeployErrors(errorData) {
37
38
  }
38
39
  });
39
40
  }
41
+ function handleBlockedDeploy(deployResp) {
42
+ const deployCanBeForced = deployResp.issues.every(issue => issue.blockingMessages.every(message => message.isWarning));
43
+ uiLogger.log('');
44
+ if (deployCanBeForced) {
45
+ uiLogger.warn(commands.project.deploy.errors.deployWarningsHeader);
46
+ uiLogger.log('');
47
+ }
48
+ else {
49
+ uiLogger.error(commands.project.deploy.errors.deployBlockedHeader);
50
+ uiLogger.log('');
51
+ }
52
+ deployResp.issues.forEach(issue => {
53
+ if (issue.blockingMessages.length > 0) {
54
+ issue.blockingMessages.forEach(message => {
55
+ uiLogger.log(commands.project.deploy.errors.deployIssueComponentWarning(issue.uid, issue.componentTypeName, message.message));
56
+ });
57
+ }
58
+ else {
59
+ uiLogger.log(commands.project.deploy.errors.deployIssueComponentGeneric(issue.uid, issue.componentTypeName));
60
+ }
61
+ uiLogger.log('');
62
+ });
63
+ }
40
64
  async function handler(args) {
41
65
  const { derivedAccountId, project: projectOption, buildId: buildIdOption, force: forceOption, deployLatestBuild: deployLatestBuildOption, json: formatOutputAsJson, } = args;
42
66
  const accountConfig = getAccountConfig(derivedAccountId);
@@ -109,7 +133,13 @@ async function handler(args) {
109
133
  }
110
134
  const { data: deployResp } = await deployProject(targetAccountId, projectName, buildIdToDeploy, useV3Api(projectConfig?.platformVersion), forceOption);
111
135
  if (!deployResp || deployResp.buildResultType !== 'DEPLOY_QUEUED') {
112
- uiLogger.error(commands.project.deploy.errors.deploy);
136
+ if (deployResp?.buildResultType === 'DEPLOY_BLOCKED') {
137
+ handleBlockedDeploy(deployResp);
138
+ process.exit(EXIT_CODES.ERROR);
139
+ }
140
+ else {
141
+ uiLogger.error(commands.project.deploy.errors.deploy);
142
+ }
113
143
  return process.exit(EXIT_CODES.ERROR);
114
144
  }
115
145
  else if (formatOutputAsJson) {
package/lang/en.d.ts CHANGED
@@ -32,12 +32,12 @@ export declare const commands: {
32
32
  readonly openInstallUrl: "Open HubSpot to install your app in your account?";
33
33
  readonly openedDeveloperOverview: "HubSpot opened!";
34
34
  readonly prompts: {
35
- readonly selectOption: "Are you looking to build apps or CMS?";
35
+ readonly selectOption: "Are you looking to build apps or CMS assets?";
36
36
  readonly options: {
37
37
  readonly app: "App";
38
- readonly cms: "CMS";
38
+ readonly cms: "CMS assets";
39
39
  };
40
- readonly uploadProject: "Would you like to upload your project to HubSpot now?";
40
+ readonly uploadProject: (accountName: string) => string;
41
41
  readonly projectCreated: {
42
42
  readonly title: string;
43
43
  readonly description: `Let's prepare and upload your project to HubSpot.
@@ -46,7 +46,7 @@ You can use ${string} to ${string} and ${string} to ${string} your project.`;
46
46
  };
47
47
  readonly logs: {
48
48
  readonly appSelected: `We'll create a new project with a sample app for you.
49
- Projects are what you can use to create apps, themes, and more at HubSpot.
49
+ Projects are what you can use to create apps with HubSpot.
50
50
  Usually you'll use the ${string} command, but we'll go ahead and make one now.`;
51
51
  readonly dependenciesInstalled: "Dependencies installed successfully.";
52
52
  readonly uploadingProject: "Uploading your project to HubSpot...";
@@ -337,6 +337,38 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
337
337
  readonly describe: "Type of asset";
338
338
  };
339
339
  };
340
+ readonly flags: {
341
+ readonly templateType: {
342
+ readonly describe: "Template type for template creation - only used when type is template";
343
+ };
344
+ readonly moduleLabel: {
345
+ readonly describe: "Label for module creation - only used when type is module";
346
+ };
347
+ readonly reactType: {
348
+ readonly describe: "Whether to create a React module - only used when type is module";
349
+ };
350
+ readonly contentTypes: {
351
+ readonly describe: (contentTypes: readonly string[]) => string;
352
+ };
353
+ readonly global: {
354
+ readonly describe: "Whether to create a global module - only used when type is module";
355
+ };
356
+ readonly availableForNewContent: {
357
+ readonly describe: "Whether the template is available for new content - only used when type is template";
358
+ };
359
+ readonly functionsFolder: {
360
+ readonly describe: "Folder to create functions in - only used when type is function";
361
+ };
362
+ readonly filename: {
363
+ readonly describe: "Filename for the function - only used when type is function";
364
+ };
365
+ readonly endpointMethod: {
366
+ readonly describe: "HTTP method for the function endpoint - only used when type is function";
367
+ };
368
+ readonly endpointPath: {
369
+ readonly describe: "API endpoint path for the function - only used when type is function";
370
+ };
371
+ };
340
372
  readonly subcommands: {
341
373
  readonly apiSample: {
342
374
  readonly folderOverwritePrompt: (folderName: string) => string;
@@ -1177,6 +1209,10 @@ ${string}`;
1177
1209
  readonly buildIdDoesNotExist: (accountId: number, buildId: number, projectName: string) => string;
1178
1210
  readonly buildAlreadyDeployed: (accountId: number, buildId: number, projectName: string) => string;
1179
1211
  readonly deployContainsRemovals: (componentName: string) => string;
1212
+ readonly deployBlockedHeader: "This build couldn't be deployed because it will be too disruptive for existing users. Fix the following issues and try again:";
1213
+ readonly deployWarningsHeader: `Deploying this build might have unintended consequences for users. Review the following issues and run ${string} to try again:`;
1214
+ readonly deployIssueComponentGeneric: (uid: string, componentTypeName: string) => string;
1215
+ readonly deployIssueComponentWarning: (uid: string, componentTypeName: string, message: string) => string;
1180
1216
  };
1181
1217
  readonly examples: {
1182
1218
  readonly default: "Deploy the latest build of the current project";
package/lang/en.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { mapToUserFriendlyName } from '@hubspot/project-parsing-lib';
2
3
  import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
3
4
  import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
4
5
  import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
@@ -39,19 +40,19 @@ export const commands = {
39
40
  openInstallUrl: 'Open HubSpot to install your app in your account?',
40
41
  openedDeveloperOverview: 'HubSpot opened!',
41
42
  prompts: {
42
- selectOption: 'Are you looking to build apps or CMS?',
43
+ selectOption: 'Are you looking to build apps or CMS assets?',
43
44
  options: {
44
45
  app: 'App',
45
- cms: 'CMS',
46
+ cms: 'CMS assets',
46
47
  },
47
- uploadProject: 'Would you like to upload your project to HubSpot now?',
48
+ uploadProject: (accountName) => `Would you like to upload this project to account "${accountName}" now?`,
48
49
  projectCreated: {
49
50
  title: chalk.bold('Next steps:'),
50
51
  description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project install-deps')} to ${chalk.bold('install dependencies')} and ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
51
52
  },
52
53
  },
53
54
  logs: {
54
- appSelected: `We'll create a new project with a sample app for you.\nProjects are what you can use to create apps, themes, and more at HubSpot.\nUsually you'll use the ${uiCommandReference('hs project create')} command, but we'll go ahead and make one now.`,
55
+ appSelected: `We'll create a new project with a sample app for you.\nProjects are what you can use to create apps with HubSpot.\nUsually you'll use the ${uiCommandReference('hs project create')} command, but we'll go ahead and make one now.`,
55
56
  dependenciesInstalled: 'Dependencies installed successfully.',
56
57
  uploadingProject: 'Uploading your project to HubSpot...',
57
58
  uploadSuccess: 'Project uploaded successfully!',
@@ -337,6 +338,38 @@ export const commands = {
337
338
  describe: 'Type of asset',
338
339
  },
339
340
  },
341
+ flags: {
342
+ templateType: {
343
+ describe: 'Template type for template creation - only used when type is template',
344
+ },
345
+ moduleLabel: {
346
+ describe: 'Label for module creation - only used when type is module',
347
+ },
348
+ reactType: {
349
+ describe: 'Whether to create a React module - only used when type is module',
350
+ },
351
+ contentTypes: {
352
+ describe: (contentTypes) => `Content types where the module can be used (comma-separated list: ${contentTypes.join(', ')}) - only used when type is module`,
353
+ },
354
+ global: {
355
+ describe: 'Whether to create a global module - only used when type is module',
356
+ },
357
+ availableForNewContent: {
358
+ describe: 'Whether the template is available for new content - only used when type is template',
359
+ },
360
+ functionsFolder: {
361
+ describe: 'Folder to create functions in - only used when type is function',
362
+ },
363
+ filename: {
364
+ describe: 'Filename for the function - only used when type is function',
365
+ },
366
+ endpointMethod: {
367
+ describe: 'HTTP method for the function endpoint - only used when type is function',
368
+ },
369
+ endpointPath: {
370
+ describe: 'API endpoint path for the function - only used when type is function',
371
+ },
372
+ },
340
373
  subcommands: {
341
374
  apiSample: {
342
375
  folderOverwritePrompt: (folderName) => `The folder with name "${folderName}" already exists. Overwrite?`,
@@ -1174,6 +1207,10 @@ export const commands = {
1174
1207
  buildIdDoesNotExist: (accountId, buildId, projectName) => `Build ${buildId} does not exist for project ${chalk.bold(projectName)}. ${uiLink('View project builds in HubSpot', getProjectDetailUrl(projectName, accountId))}`,
1175
1208
  buildAlreadyDeployed: (accountId, buildId, projectName) => `Build ${buildId} is already deployed. ${uiLink('View project builds in HubSpot', getProjectDetailUrl(projectName, accountId))}`,
1176
1209
  deployContainsRemovals: (componentName) => `- This deploy would remove the ${chalk.bold(componentName)} component. To proceed, run the deploy command with the ${uiCommandReference('--force')} flag`,
1210
+ deployBlockedHeader: "This build couldn't be deployed because it will be too disruptive for existing users. Fix the following issues and try again:",
1211
+ deployWarningsHeader: `Deploying this build might have unintended consequences for users. Review the following issues and run ${uiCommandReference('hs project deploy --force')} to try again:`,
1212
+ deployIssueComponentGeneric: (uid, componentTypeName) => `- [${mapToUserFriendlyName(componentTypeName)}] ${chalk.bold('(' + uid + ')')} reported issues with the deploy`,
1213
+ deployIssueComponentWarning: (uid, componentTypeName, message) => `- [${mapToUserFriendlyName(componentTypeName)}] ${chalk.bold('(' + uid + ')')} ${message}`,
1177
1214
  },
1178
1215
  examples: {
1179
1216
  default: 'Deploy the latest build of the current project',
@@ -2489,14 +2526,14 @@ export const lib = {
2489
2526
  uiExtensionLabel: '[UI Extension]',
2490
2527
  missingComponents: (missingComponents) => `Couldn't find the following components in the deployed build for this project: ${chalk.bold(missingComponents)}. This may cause issues in local development.`,
2491
2528
  defaultWarning: chalk.bold('Changing project configuration requires a new project build.'),
2492
- defaultPublicAppWarning: (installCount, installText) => `${chalk.bold('Changing project configuration requires a new project build.')}\n\nThis will affect your public app's ${chalk.bold(`${installCount} existing ${installText}`)}. If your app has users in production, we strongly recommend creating a copy of this app to test your changes before proceding.`,
2529
+ defaultPublicAppWarning: (installCount, installText) => `${chalk.bold('Changing project configuration requires a new project build.')}\n\nThis will affect your public app's ${chalk.bold(`${installCount} existing ${installText}`)}. If your app has users in production, we strongly recommend creating a copy of this app to test your changes before proceeding.`,
2493
2530
  header: (warning) => `${warning} To reflect these changes and continue testing:`,
2494
2531
  instructionsHeader: 'To reflect these changes and continue testing:',
2495
2532
  stopDev: ` * Stop ${uiCommandReference('hs project dev')}`,
2496
2533
  runUpload: (command) => ` * Run ${command}`,
2497
2534
  restartDev: ` * Re-run ${uiCommandReference('hs project dev')}`,
2498
2535
  pushToGithub: ' * Commit and push your changes to GitHub',
2499
- defaultMarketplaceAppWarning: (installCount, accountText) => `${chalk.bold('Changing project configuration requires creating a new project build.')}\n\nYour marketplace app is currently installed in ${chalk.bold(`${installCount} ${accountText}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`,
2536
+ defaultMarketplaceAppWarning: (installCount, accountText) => `${chalk.bold('Changing project configuration requires creating a new project build.')}\n\nYour marketplace app is currently installed in ${chalk.bold(`${installCount} ${accountText}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceeding.`,
2500
2537
  },
2501
2538
  activeInstallWarning: {
2502
2539
  installCount: (appName, installCount) => `${chalk.bold(`The app ${appName} is installed in ${installCount} production ${installCount === 1 ? 'account' : 'accounts'}`)}`,
@@ -2512,7 +2549,7 @@ export const lib = {
2512
2549
  },
2513
2550
  },
2514
2551
  AppDevModeInterface: {
2515
- defaultMarketplaceAppWarning: (installCount) => `Your marketplace app is currently installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding.`,
2552
+ defaultMarketplaceAppWarning: (installCount) => `Your marketplace app is currently installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceeding.`,
2516
2553
  autoInstallDeclined: 'You must install your app on your target test account to proceed with local development.',
2517
2554
  autoInstallSuccess: (appName, targetTestAccountId) => `Successfully installed app ${appName} on account ${uiAccountDescription(targetTestAccountId)}\n`,
2518
2555
  autoInstallError: (appName, targetTestAccountId) => `Error installing app ${appName} on account ${uiAccountDescription(targetTestAccountId)}. You may still be able to install your app in your browser.`,
package/lang/en.lyaml CHANGED
@@ -231,6 +231,27 @@ en:
231
231
  describe: "Name of new asset"
232
232
  type:
233
233
  describe: "Type of asset"
234
+ flags:
235
+ templateType:
236
+ describe: "Template type for template creation (page-template, email-template, partial, global-partial, blog-listing-template, blog-post-template, search-template, section) - only used when type is template"
237
+ moduleLabel:
238
+ describe: "Label for module creation - only used when type is module"
239
+ reactType:
240
+ describe: "Whether to create a React module - only used when type is module"
241
+ contentTypes:
242
+ describe: "Content types where the module can be used (comma-separated list: ANY, LANDING_PAGE, SITE_PAGE, BLOG_POST, BLOG_LISTING, EMAIL, KNOWLEDGE_BASE, QUOTE_TEMPLATE, CUSTOMER_PORTAL, WEB_INTERACTIVE, SUBSCRIPTION, MEMBERSHIP) - only used when type is module"
243
+ global:
244
+ describe: "Whether to create a global module - only used when type is module"
245
+ availableForNewContent:
246
+ describe: "Whether the template is available for new content"
247
+ functionsFolder:
248
+ describe: "Folder to create functions in - only used when type is function"
249
+ filename:
250
+ describe: "Filename for the function - only used when type is function"
251
+ endpointMethod:
252
+ describe: "HTTP method for the function endpoint - only used when type is function"
253
+ endpointPath:
254
+ describe: "API endpoint path for the function - only used when type is function"
234
255
  subcommands:
235
256
  apiSample:
236
257
  folderOverwritePrompt: "The folder with name \"{{ folderName }}\" already exists. Overwrite?"
@@ -962,8 +983,8 @@ en:
962
983
  uiExtensionLabel: "[UI Extension]"
963
984
  missingComponents: "Couldn't find the following components in the deployed build for this project: {{#bold}}'{{ missingComponents }}'{{/bold}}. This may cause issues in local development."
964
985
  defaultWarning: "{{#bold}}Changing project configuration requires a new project build.{{/bold}}"
965
- defaultPublicAppWarning: "{{#bold}}Changing project configuration requires a new project build.{{/bold}}\n\nThis will affect your public app's {{#bold}}{{ installCount }} existing {{ installText }}{{/bold}}. If your app has users in production, we strongly recommend creating a copy of this app to test your changes before proceding."
966
- defaultMarketplaceAppWarning: "{{#bold}}Changing project configuration requires creating a new project build.{{/bold}}\n\nYour marketplace app is currently installed in {{#bold}}{{ installCount }} {{ accountText }}{{/bold}}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceding."
986
+ defaultPublicAppWarning: "{{#bold}}Changing project configuration requires a new project build.{{/bold}}\n\nThis will affect your public app's {{#bold}}{{ installCount }} existing {{ installText }}{{/bold}}. If your app has users in production, we strongly recommend creating a copy of this app to test your changes before proceeding."
987
+ defaultMarketplaceAppWarning: "{{#bold}}Changing project configuration requires creating a new project build.{{/bold}}\n\nYour marketplace app is currently installed in {{#bold}}{{ installCount }} {{ accountText }}{{/bold}}. Any uploaded changes will impact your app's users. We strongly recommend creating a copy of this app to test your changes before proceeding."
967
988
  header: "{{ warning }} To reflect these changes and continue testing:"
968
989
  stopDev: " * Stop {{ command }}"
969
990
  runUpload: " * Run {{ command }}"
package/lib/process.js CHANGED
@@ -1,29 +1,40 @@
1
1
  import readline from 'readline';
2
2
  import { logger, setLogLevel, LOG_LEVEL } from '@hubspot/local-dev-lib/logger';
3
3
  import { i18n } from './lang.js';
4
+ import { logError } from './errorHandlers/index.js';
5
+ const SIGHUP = 'SIGHUP';
6
+ const uncaughtException = 'uncaughtException';
4
7
  export const TERMINATION_SIGNALS = [
5
8
  'beforeExit',
6
9
  'SIGINT', // Terminal trying to interrupt (Ctrl + C)
7
10
  'SIGUSR1', // Start Debugger User-defined signal 1
8
11
  'SIGUSR2', // User-defined signal 2
9
- 'uncaughtException',
12
+ uncaughtException,
10
13
  'SIGTERM', // Represents a graceful termination
11
- 'SIGHUP', // Parent terminal has been closed
14
+ SIGHUP, // Parent terminal has been closed
12
15
  ];
13
16
  export function handleExit(callback) {
14
17
  let exitInProgress = false;
15
18
  TERMINATION_SIGNALS.forEach(signal => {
16
19
  process.removeAllListeners(signal);
17
- process.on(signal, async () => {
20
+ process.on(signal, async (...args) => {
18
21
  // Prevent duplicate exit handling
19
22
  if (!exitInProgress) {
20
23
  exitInProgress = true;
21
- const isSIGHUP = signal === 'SIGHUP';
24
+ const isSIGHUP = signal === SIGHUP;
22
25
  // Prevent logs when terminal closes
23
26
  if (isSIGHUP) {
24
27
  setLogLevel(LOG_LEVEL.NONE);
25
28
  }
26
29
  logger.debug(i18n(`lib.process.exitDebug`, { signal }));
30
+ if (signal === uncaughtException && args && args.length > 0) {
31
+ try {
32
+ logError(args[0]);
33
+ }
34
+ catch (e) {
35
+ logger.error(args[0]);
36
+ }
37
+ }
27
38
  await callback({ isSIGHUP });
28
39
  }
29
40
  });
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createFunctionPrompt } from '../createFunctionPrompt.js';
3
+ import { promptUser } from '../promptUtils.js';
4
+ vi.mock('../promptUtils.js');
5
+ const mockPromptUser = vi.mocked(promptUser);
6
+ describe('createFunctionPrompt', () => {
7
+ beforeEach(() => {
8
+ vi.resetAllMocks();
9
+ });
10
+ describe('when all parameters are provided', () => {
11
+ it('should return provided values without prompting', async () => {
12
+ const commandArgs = {
13
+ functionsFolder: 'my-functions',
14
+ filename: 'my-function',
15
+ endpointMethod: 'POST',
16
+ endpointPath: '/api/test',
17
+ };
18
+ const result = await createFunctionPrompt(commandArgs);
19
+ expect(mockPromptUser).not.toHaveBeenCalled();
20
+ expect(result).toEqual({
21
+ functionsFolder: 'my-functions',
22
+ filename: 'my-function',
23
+ endpointMethod: 'POST',
24
+ endpointPath: '/api/test',
25
+ });
26
+ });
27
+ it('should use default GET method when endpointMethod not provided', async () => {
28
+ const commandArgs = {
29
+ functionsFolder: 'my-functions',
30
+ filename: 'my-function',
31
+ endpointPath: '/api/test',
32
+ };
33
+ const result = await createFunctionPrompt(commandArgs);
34
+ expect(mockPromptUser).not.toHaveBeenCalled();
35
+ expect(result).toEqual({
36
+ functionsFolder: 'my-functions',
37
+ filename: 'my-function',
38
+ endpointMethod: 'GET',
39
+ endpointPath: '/api/test',
40
+ });
41
+ });
42
+ });
43
+ describe('when some parameters are missing', () => {
44
+ it('should only prompt for missing parameters', async () => {
45
+ const commandArgs = {
46
+ functionsFolder: 'my-functions',
47
+ endpointMethod: 'POST',
48
+ };
49
+ mockPromptUser.mockResolvedValue({
50
+ filename: 'prompted-function',
51
+ endpointPath: '/prompted-path',
52
+ });
53
+ const result = await createFunctionPrompt(commandArgs);
54
+ expect(mockPromptUser).toHaveBeenCalledWith([
55
+ expect.objectContaining({ name: 'filename' }),
56
+ expect.objectContaining({ name: 'endpointPath' }),
57
+ ]);
58
+ expect(result).toEqual({
59
+ functionsFolder: 'my-functions',
60
+ filename: 'prompted-function',
61
+ endpointMethod: 'POST',
62
+ endpointPath: '/prompted-path',
63
+ });
64
+ });
65
+ });
66
+ describe('when no parameters are provided', () => {
67
+ it('should prompt for all parameters', async () => {
68
+ mockPromptUser.mockResolvedValue({
69
+ functionsFolder: 'prompted-functions',
70
+ filename: 'prompted-function',
71
+ endpointMethod: 'GET',
72
+ endpointPath: '/prompted-path',
73
+ });
74
+ const result = await createFunctionPrompt();
75
+ expect(mockPromptUser).toHaveBeenCalledWith([
76
+ expect.objectContaining({ name: 'functionsFolder' }),
77
+ expect.objectContaining({ name: 'filename' }),
78
+ expect.objectContaining({ name: 'endpointMethod' }),
79
+ expect.objectContaining({ name: 'endpointPath' }),
80
+ ]);
81
+ expect(result).toEqual({
82
+ functionsFolder: 'prompted-functions',
83
+ filename: 'prompted-function',
84
+ endpointMethod: 'GET',
85
+ endpointPath: '/prompted-path',
86
+ });
87
+ });
88
+ });
89
+ describe('parameter precedence', () => {
90
+ it('should prioritize command args over prompted values', async () => {
91
+ const commandArgs = {
92
+ functionsFolder: 'arg-functions',
93
+ };
94
+ mockPromptUser.mockResolvedValue({
95
+ filename: 'prompted-function',
96
+ endpointMethod: 'POST',
97
+ endpointPath: '/prompted-path',
98
+ });
99
+ const result = await createFunctionPrompt(commandArgs);
100
+ expect(result).toEqual({
101
+ functionsFolder: 'arg-functions', // from commandArgs
102
+ filename: 'prompted-function', // from prompt
103
+ endpointMethod: 'POST', // from prompt
104
+ endpointPath: '/prompted-path', // from prompt
105
+ });
106
+ });
107
+ it('should handle mixed scenario with partial command args and prompting', async () => {
108
+ const commandArgs = {
109
+ functionsFolder: 'my-funcs',
110
+ endpointMethod: 'DELETE',
111
+ };
112
+ mockPromptUser.mockResolvedValue({
113
+ filename: 'delete-handler',
114
+ endpointPath: '/api/delete',
115
+ });
116
+ const result = await createFunctionPrompt(commandArgs);
117
+ expect(mockPromptUser).toHaveBeenCalledWith([
118
+ expect.objectContaining({ name: 'filename' }),
119
+ expect.objectContaining({ name: 'endpointPath' }),
120
+ ]);
121
+ expect(result).toEqual({
122
+ functionsFolder: 'my-funcs', // from commandArgs
123
+ filename: 'delete-handler', // from prompt
124
+ endpointMethod: 'DELETE', // from commandArgs
125
+ endpointPath: '/api/delete', // from prompt
126
+ });
127
+ });
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};