@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.
- package/commands/__tests__/create.test.js +20 -0
- package/commands/create/function.js +2 -2
- package/commands/create/module.js +2 -2
- package/commands/create/template.js +2 -2
- package/commands/create.js +47 -0
- package/commands/getStarted.js +3 -2
- package/commands/project/deploy.js +31 -1
- package/lang/en.d.ts +40 -4
- package/lang/en.js +44 -7
- package/lang/en.lyaml +23 -2
- package/lib/process.js +15 -4
- package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
- package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
- package/lib/prompts/createFunctionPrompt.d.ts +2 -1
- package/lib/prompts/createFunctionPrompt.js +36 -7
- package/lib/prompts/createModulePrompt.d.ts +2 -1
- package/lib/prompts/createModulePrompt.js +48 -1
- package/lib/prompts/createTemplatePrompt.d.ts +3 -24
- package/lib/prompts/createTemplatePrompt.js +9 -1
- package/mcp-server/tools/index.js +4 -0
- package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
- package/mcp-server/tools/project/DocFetchTool.js +49 -0
- package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
- package/mcp-server/tools/project/DocsSearchTool.js +62 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +2 -0
- package/mcp-server/tools/project/constants.js +6 -0
- package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
- package/mcp-server/utils/toolUsageTracking.js +2 -1
- package/package.json +1 -1
- package/types/Cms.d.ts +16 -0
- 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
|
}
|
package/commands/create.js
CHANGED
|
@@ -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, {
|
package/commands/getStarted.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
12
|
+
uncaughtException,
|
|
10
13
|
'SIGTERM', // Represents a graceful termination
|
|
11
|
-
|
|
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 ===
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|