@hubspot/cli 8.1.0-beta.0 → 8.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/cms/__tests__/watch.test.js +0 -8
- package/commands/cms/function/logs.js +1 -0
- package/commands/cms/theme/preview.js +9 -64
- package/commands/cms/watch.d.ts +0 -1
- package/commands/cms/watch.js +2 -8
- package/commands/feedback.js +1 -1
- package/commands/mcp/__tests__/start.test.js +8 -1
- package/commands/mcp/setup.js +1 -9
- package/commands/mcp/start.js +0 -1
- package/commands/project/__tests__/create.test.js +1 -1
- package/commands/project/create.js +2 -2
- package/commands/project/watch.js +15 -2
- package/lang/en.d.ts +17 -6
- package/lang/en.js +18 -7
- package/lib/__tests__/commandSuggestion.test.js +2 -0
- package/lib/__tests__/serverlessLogs.test.js +79 -64
- package/lib/commandSuggestion.js +1 -7
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/generateSelectors.js +1 -2
- package/lib/getStartedV2Actions.d.ts +13 -0
- package/lib/getStartedV2Actions.js +53 -0
- package/lib/mcp/__tests__/setup.test.js +357 -28
- package/lib/mcp/setup.d.ts +1 -0
- package/lib/mcp/setup.js +77 -30
- package/lib/projects/create/__tests__/legacy.test.js +6 -24
- package/lib/projects/create/index.js +1 -4
- package/lib/projects/create/legacy.js +3 -8
- package/lib/projects/create/v2.js +1 -9
- package/lib/projects/ensureProjectExists.js +1 -2
- package/lib/projects/pollProjectBuildAndDeploy.js +90 -85
- package/lib/projects/upload.d.ts +1 -0
- package/lib/projects/upload.js +37 -46
- package/lib/projects/watch.d.ts +2 -1
- package/lib/projects/watch.js +32 -24
- package/lib/serverlessLogs.js +50 -44
- package/lib/theme/cmsDevServerProcess.d.ts +12 -0
- package/lib/theme/cmsDevServerProcess.js +148 -0
- package/lib/theme/cmsDevServerRunner.d.ts +14 -0
- package/lib/theme/cmsDevServerRunner.js +90 -0
- package/lib/usageTracking.js +8 -5
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -2
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +7 -11
- package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
- package/mcp-server/tools/project/CreateProjectTool.js +6 -11
- package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
- package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
- package/mcp-server/tools/project/GetBuildLogsTool.js +6 -7
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
- package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +0 -32
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +12 -1
- package/mcp-server/tools/project/constants.js +12 -16
- package/mcp-server/utils/__tests__/command.test.js +233 -3
- package/mcp-server/utils/__tests__/feedbackTracking.test.js +9 -64
- package/mcp-server/utils/command.d.ts +5 -0
- package/mcp-server/utils/command.js +24 -0
- package/mcp-server/utils/feedbackTracking.js +2 -17
- package/package.json +4 -5
- package/ui/components/getStarted/GetStartedFlow.js +79 -2
- package/ui/components/getStarted/reducer.d.ts +20 -0
- package/ui/components/getStarted/reducer.js +36 -0
- package/ui/components/getStarted/screens/InstallationScreen.d.ts +7 -0
- package/ui/components/getStarted/screens/InstallationScreen.js +16 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +2 -1
- package/ui/lib/constants.d.ts +1 -0
- package/ui/lib/constants.js +1 -0
- package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
- package/mcp-server/utils/__tests__/project.test.js +0 -140
- package/mcp-server/utils/project.d.ts +0 -5
- package/mcp-server/utils/project.js +0 -18
package/lib/mcp/setup.js
CHANGED
|
@@ -47,23 +47,56 @@ export async function addMcpServerToConfig(targets) {
|
|
|
47
47
|
else {
|
|
48
48
|
derivedTargets = targets;
|
|
49
49
|
}
|
|
50
|
+
// Prompt for standalone mode
|
|
51
|
+
const { useStandaloneMode } = await promptUser({
|
|
52
|
+
name: 'useStandaloneMode',
|
|
53
|
+
type: 'confirm',
|
|
54
|
+
message: commands.mcp.setup.prompts.standaloneMode,
|
|
55
|
+
default: false,
|
|
56
|
+
});
|
|
57
|
+
const { cliVersion } = useStandaloneMode
|
|
58
|
+
? await promptUser({
|
|
59
|
+
name: 'cliVersion',
|
|
60
|
+
type: 'input',
|
|
61
|
+
message: commands.mcp.setup.prompts.cliVersion,
|
|
62
|
+
validate: (v) => !v || /^[\d]+\.[\d]+\.[\d]+([-+][\w.]+)?$/.test(v.trim())
|
|
63
|
+
? true
|
|
64
|
+
: 'Please enter a valid semver version (e.g. 8.0.1) or leave blank for latest.',
|
|
65
|
+
})
|
|
66
|
+
: { cliVersion: '' };
|
|
67
|
+
const cliPackage = cliVersion
|
|
68
|
+
? `@hubspot/cli@${cliVersion}`
|
|
69
|
+
: '@hubspot/cli';
|
|
70
|
+
const standaloneEnv = {
|
|
71
|
+
HUBSPOT_MCP_STANDALONE: 'true',
|
|
72
|
+
};
|
|
73
|
+
if (cliVersion) {
|
|
74
|
+
standaloneEnv.HUBSPOT_CLI_VERSION = cliVersion;
|
|
75
|
+
}
|
|
76
|
+
const mcpCommand = useStandaloneMode
|
|
77
|
+
? {
|
|
78
|
+
command: 'npx',
|
|
79
|
+
args: ['-y', '-p', cliPackage, 'hs', 'mcp', 'start'],
|
|
80
|
+
env: standaloneEnv,
|
|
81
|
+
}
|
|
82
|
+
: defaultMcpCommand;
|
|
50
83
|
if (derivedTargets.includes(claudeCode)) {
|
|
51
|
-
await runSetupFunction(setupClaudeCode);
|
|
84
|
+
await runSetupFunction(() => setupClaudeCode(mcpCommand));
|
|
52
85
|
}
|
|
53
86
|
if (derivedTargets.includes(cursor)) {
|
|
54
|
-
await runSetupFunction(setupCursor);
|
|
87
|
+
await runSetupFunction(() => setupCursor(mcpCommand));
|
|
55
88
|
}
|
|
56
89
|
if (derivedTargets.includes(windsurf)) {
|
|
57
|
-
await runSetupFunction(setupWindsurf);
|
|
90
|
+
await runSetupFunction(() => setupWindsurf(mcpCommand));
|
|
58
91
|
}
|
|
59
92
|
if (derivedTargets.includes(vscode)) {
|
|
60
|
-
await runSetupFunction(setupVsCode);
|
|
93
|
+
await runSetupFunction(() => setupVsCode(mcpCommand));
|
|
61
94
|
}
|
|
62
95
|
if (derivedTargets.includes(codex)) {
|
|
63
|
-
await runSetupFunction(setupCodex);
|
|
96
|
+
await runSetupFunction(() => setupCodex(mcpCommand));
|
|
64
97
|
}
|
|
65
98
|
if (derivedTargets.includes(gemini)) {
|
|
66
|
-
await runSetupFunction(setupGemini);
|
|
99
|
+
await runSetupFunction(() => setupGemini(mcpCommand));
|
|
67
100
|
}
|
|
68
101
|
uiLogger.info(commands.mcp.setup.success(derivedTargets));
|
|
69
102
|
return derivedTargets;
|
|
@@ -121,10 +154,7 @@ function setupMcpConfigFile(config) {
|
|
|
121
154
|
if (!mcpConfig.mcpServers) {
|
|
122
155
|
mcpConfig.mcpServers = {};
|
|
123
156
|
}
|
|
124
|
-
|
|
125
|
-
mcpConfig.mcpServers[mcpServerName] = {
|
|
126
|
-
...config.mcpCommand,
|
|
127
|
-
};
|
|
157
|
+
mcpConfig.mcpServers[mcpServerName] = config.mcpCommand;
|
|
128
158
|
// Write the updated config
|
|
129
159
|
fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
|
|
130
160
|
SpinniesManager.succeed('spinner', {
|
|
@@ -145,10 +175,12 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
|
145
175
|
SpinniesManager.add('vsCode', {
|
|
146
176
|
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
147
177
|
});
|
|
148
|
-
const
|
|
178
|
+
const commandWithAgent = buildCommandWithAgentString(mcpCommand, vscode);
|
|
179
|
+
const configObject = {
|
|
149
180
|
name: mcpServerName,
|
|
150
|
-
...
|
|
151
|
-
}
|
|
181
|
+
...commandWithAgent,
|
|
182
|
+
};
|
|
183
|
+
const mcpConfig = JSON.stringify(configObject);
|
|
152
184
|
await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
153
185
|
SpinniesManager.succeed('vsCode', {
|
|
154
186
|
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
@@ -172,25 +204,27 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
207
|
+
SpinniesManager.add('claudeCode', {
|
|
208
|
+
text: commands.mcp.setup.spinners.configuringClaudeCode,
|
|
209
|
+
});
|
|
175
210
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
211
|
+
// Check if claude command is available
|
|
212
|
+
await execAsync('claude --version');
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
SpinniesManager.fail('claudeCode', {
|
|
216
|
+
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
178
217
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
SpinniesManager.fail('claudeCode', {
|
|
185
|
-
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
186
|
-
});
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
189
221
|
// Run claude mcp add command
|
|
190
|
-
const
|
|
222
|
+
const commandWithAgent = buildCommandWithAgentString(mcpCommand, claudeCode);
|
|
223
|
+
const configObject = {
|
|
191
224
|
type: 'stdio',
|
|
192
|
-
...
|
|
193
|
-
}
|
|
225
|
+
...commandWithAgent,
|
|
226
|
+
};
|
|
227
|
+
const mcpConfig = JSON.stringify(configObject);
|
|
194
228
|
const { stdout } = await execAsync('claude mcp list');
|
|
195
229
|
if (stdout.includes(mcpServerName)) {
|
|
196
230
|
SpinniesManager.update('claudeCode', {
|
|
@@ -248,7 +282,7 @@ export async function setupCodex(mcpCommand = defaultMcpCommand) {
|
|
|
248
282
|
return false;
|
|
249
283
|
}
|
|
250
284
|
const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, codex);
|
|
251
|
-
await execAsync(`codex mcp add "${mcpServerName}" -- ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
285
|
+
await execAsync(`codex mcp add "${mcpServerName}"${buildEnvFlagString(mcpCommand)} -- ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
252
286
|
SpinniesManager.succeed('codexSpinner', {
|
|
253
287
|
text: commands.mcp.setup.spinners.configuredCodex,
|
|
254
288
|
});
|
|
@@ -277,7 +311,7 @@ export async function setupGemini(mcpCommand = defaultMcpCommand) {
|
|
|
277
311
|
return false;
|
|
278
312
|
}
|
|
279
313
|
const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, gemini);
|
|
280
|
-
await execAsync(`gemini mcp add -s user "${mcpServerName}" ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
314
|
+
await execAsync(`gemini mcp add -s user${buildEnvFlagString(mcpCommand)} "${mcpServerName}" ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
281
315
|
SpinniesManager.succeed('geminiSpinner', {
|
|
282
316
|
text: commands.mcp.setup.spinners.configuredGemini,
|
|
283
317
|
});
|
|
@@ -296,3 +330,16 @@ function buildCommandWithAgentString(mcpCommand, agent) {
|
|
|
296
330
|
mcpCommandCopy.args.push('--ai-agent', agent);
|
|
297
331
|
return mcpCommandCopy;
|
|
298
332
|
}
|
|
333
|
+
function buildEnvFlagString(mcpCommand) {
|
|
334
|
+
const envFlags = [];
|
|
335
|
+
if (mcpCommand.env) {
|
|
336
|
+
const env = Object.entries(mcpCommand.env);
|
|
337
|
+
env.forEach(([key, value]) => {
|
|
338
|
+
envFlags.push(`--env ${key}="${value}"`);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (envFlags.length === 0) {
|
|
342
|
+
return '';
|
|
343
|
+
}
|
|
344
|
+
return ` ${envFlags.join(' ')}`;
|
|
345
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { uiLogger } from '../../../ui/logger.js';
|
|
2
1
|
import * as github from '@hubspot/local-dev-lib/api/github';
|
|
3
|
-
import { EXIT_CODES } from '../../../enums/exitCodes.js';
|
|
4
2
|
import { getProjectComponentListFromRepo, getProjectTemplateListFromRepo, } from '../legacy.js';
|
|
5
3
|
import { PROJECT_COMPONENT_TYPES, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, } from '../../../constants.js';
|
|
6
4
|
vi.mock('@hubspot/local-dev-lib/api/github');
|
|
@@ -38,16 +36,6 @@ describe('lib/projects/create/legacy', () => {
|
|
|
38
36
|
});
|
|
39
37
|
});
|
|
40
38
|
describe('getProjectTemplateListFromRepo()', () => {
|
|
41
|
-
let exitMock;
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
// @ts-expect-error - Mocking process.exit
|
|
44
|
-
exitMock = vi
|
|
45
|
-
.spyOn(process, 'exit')
|
|
46
|
-
.mockImplementation(() => undefined);
|
|
47
|
-
});
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
exitMock.mockRestore();
|
|
50
|
-
});
|
|
51
39
|
it('returns a list of project templates', async () => {
|
|
52
40
|
// @ts-expect-error - Mocking AxiosResponse
|
|
53
41
|
mockedFetchRepoFile.mockResolvedValue({
|
|
@@ -56,20 +44,16 @@ describe('lib/projects/create/legacy', () => {
|
|
|
56
44
|
const templates = await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
57
45
|
expect(templates).toEqual(repoConfig[PROJECT_COMPONENT_TYPES.PROJECTS]);
|
|
58
46
|
});
|
|
59
|
-
it('
|
|
47
|
+
it('throws an error if the request for the template list fails', async () => {
|
|
60
48
|
mockedFetchRepoFile.mockRejectedValue(new Error('Not found'));
|
|
61
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
62
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to fetch the config.json file from the target repository/));
|
|
63
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
49
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Failed to fetch the config.json file from the target repository');
|
|
64
50
|
});
|
|
65
|
-
it('
|
|
51
|
+
it('throws an error if there are no projects listed in the repo config', async () => {
|
|
66
52
|
// @ts-expect-error - Mocking AxiosResponse
|
|
67
53
|
mockedFetchRepoFile.mockResolvedValue({});
|
|
68
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
69
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Unable to find any projects in the target repository's config.json file/));
|
|
70
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
54
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Unable to find any projects in the target repository');
|
|
71
55
|
});
|
|
72
|
-
it('
|
|
56
|
+
it('throws an error if any of the projects in the repo config are missing required properties', async () => {
|
|
73
57
|
// @ts-expect-error - Mocking AxiosResponse
|
|
74
58
|
mockedFetchRepoFile.mockResolvedValue({
|
|
75
59
|
data: {
|
|
@@ -82,9 +66,7 @@ describe('lib/projects/create/legacy', () => {
|
|
|
82
66
|
],
|
|
83
67
|
},
|
|
84
68
|
});
|
|
85
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
86
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Found misconfigured projects in the target repository's config.json file/));
|
|
87
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
69
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Found misconfigured projects in the target repository');
|
|
88
70
|
});
|
|
89
71
|
});
|
|
90
72
|
});
|
|
@@ -4,9 +4,7 @@ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH
|
|
|
4
4
|
import { isV2Project } from '../platformVersion.js';
|
|
5
5
|
import { v2ComponentFlow } from './v2.js';
|
|
6
6
|
import { getProjectTemplateListFromRepo } from './legacy.js';
|
|
7
|
-
import { uiLogger } from '../../ui/logger.js';
|
|
8
7
|
import { commands } from '../../../lang/en.js';
|
|
9
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
10
8
|
export async function handleProjectCreationFlow(args) {
|
|
11
9
|
const { platformVersion, templateSource, projectBase, auth: providedAuth, distribution: providedDistribution, } = args;
|
|
12
10
|
const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
|
|
@@ -25,8 +23,7 @@ export async function handleProjectCreationFlow(args) {
|
|
|
25
23
|
}
|
|
26
24
|
const projectTemplates = await getProjectTemplateListFromRepo(repo, DEFAULT_PROJECT_TEMPLATE_BRANCH);
|
|
27
25
|
if (!projectTemplates.length) {
|
|
28
|
-
|
|
29
|
-
process.exit(EXIT_CODES.ERROR);
|
|
26
|
+
throw new Error(commands.project.create.errors.failedToFetchProjectList);
|
|
30
27
|
}
|
|
31
28
|
const selectProjectTemplatePromptResponse = await selectProjectTemplatePrompt(args, projectTemplates);
|
|
32
29
|
return {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { fetchRepoFile } from '@hubspot/local-dev-lib/api/github';
|
|
2
2
|
import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_COMPONENT_TYPES, } from '../../constants.js';
|
|
3
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
4
3
|
import { debugError } from '../../errorHandlers/index.js';
|
|
5
|
-
import { uiLogger } from '../../ui/logger.js';
|
|
6
4
|
import { isV2Project } from '../platformVersion.js';
|
|
7
5
|
import { lib } from '../../../lang/en.js';
|
|
8
6
|
const PROJECT_TEMPLATE_PROPERTIES = ['name', 'label', 'path'];
|
|
@@ -36,18 +34,15 @@ export async function getProjectTemplateListFromRepo(templateSource, githubRef)
|
|
|
36
34
|
}
|
|
37
35
|
catch (e) {
|
|
38
36
|
debugError(e);
|
|
39
|
-
|
|
40
|
-
return process.exit(EXIT_CODES.ERROR);
|
|
37
|
+
throw new Error(lib.projects.create.errors.missingConfigFileTemplateSource);
|
|
41
38
|
}
|
|
42
39
|
if (!config || !config[PROJECT_COMPONENT_TYPES.PROJECTS]) {
|
|
43
|
-
|
|
44
|
-
return process.exit(EXIT_CODES.ERROR);
|
|
40
|
+
throw new Error(lib.projects.create.errors.noProjectsInConfig);
|
|
45
41
|
}
|
|
46
42
|
const templates = config[PROJECT_COMPONENT_TYPES.PROJECTS];
|
|
47
43
|
const templatesContainAllProperties = templates.every(config => PROJECT_TEMPLATE_PROPERTIES.every(p => Object.prototype.hasOwnProperty.call(config, p)));
|
|
48
44
|
if (!templatesContainAllProperties) {
|
|
49
|
-
|
|
50
|
-
return process.exit(EXIT_CODES.ERROR);
|
|
45
|
+
throw new Error(lib.projects.create.errors.missingPropertiesInConfig);
|
|
51
46
|
}
|
|
52
47
|
return templates;
|
|
53
48
|
}
|
|
@@ -5,8 +5,6 @@ import { APP_EVENTS_KEY as AppEventsKey, PAGES_KEY as PagesKey, } from '@hubspot
|
|
|
5
5
|
import { isV2Project } from '../platformVersion.js';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { getConfigForPlatformVersion } from './legacy.js';
|
|
8
|
-
import { logError } from '../../errorHandlers/index.js';
|
|
9
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
10
8
|
import { hasFeature } from '../../hasFeature.js';
|
|
11
9
|
export async function createV2App(providedAuth, providedDistribution) {
|
|
12
10
|
let authType;
|
|
@@ -111,13 +109,7 @@ export async function v2ComponentFlow(platformVersion, projectBase, providedAuth
|
|
|
111
109
|
let repoConfig = undefined;
|
|
112
110
|
let authType;
|
|
113
111
|
let distribution;
|
|
114
|
-
|
|
115
|
-
repoConfig = await getConfigForPlatformVersion(platformVersion);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
logError(error);
|
|
119
|
-
return process.exit(EXIT_CODES.SUCCESS);
|
|
120
|
-
}
|
|
112
|
+
repoConfig = await getConfigForPlatformVersion(platformVersion);
|
|
121
113
|
const projectContentsChoice = projectBase ||
|
|
122
114
|
(await listPrompt(commands.project.create.prompts.parentComponents, {
|
|
123
115
|
choices: [
|
|
@@ -2,7 +2,6 @@ import { createProject, fetchProject, } from '@hubspot/local-dev-lib/api/project
|
|
|
2
2
|
import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
|
|
3
3
|
import { DEFAULT_POLLING_DELAY } from '../constants.js';
|
|
4
4
|
import { promptUser } from '../prompts/promptUtils.js';
|
|
5
|
-
import { EXIT_CODES } from '../enums/exitCodes.js';
|
|
6
5
|
import { uiAccountDescription } from '../ui/index.js';
|
|
7
6
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
8
7
|
import { logError, ApiErrorContext } from '../errorHandlers/index.js';
|
|
@@ -85,6 +84,6 @@ export async function ensureProjectExists(accountId, projectName, { forceCreate
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
logError(err, new ApiErrorContext({ accountId }));
|
|
88
|
-
|
|
87
|
+
return { projectExists: false };
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -5,7 +5,6 @@ import SpinniesManager from '../ui/SpinniesManager.js';
|
|
|
5
5
|
import { logError, ApiErrorContext } from '../errorHandlers/index.js';
|
|
6
6
|
import { uiLine, uiLink, uiAccountDescription } from '../ui/index.js';
|
|
7
7
|
import { getProjectBuildDetailUrl, getProjectDeployDetailUrl, getProjectActivityUrl, } from './urls.js';
|
|
8
|
-
import { EXIT_CODES } from '../enums/exitCodes.js';
|
|
9
8
|
import { lib } from '../../lang/en.js';
|
|
10
9
|
import { uiLogger } from '../ui/logger.js';
|
|
11
10
|
import { APP_FUNCTIONS_PACKAGE_KEY as AppFunctionsPackageKey } from '@hubspot/project-parsing-lib/constants';
|
|
@@ -32,8 +31,8 @@ function getSubtaskType(task) {
|
|
|
32
31
|
return task.deployType;
|
|
33
32
|
}
|
|
34
33
|
function handleTaskStatusError(statusText) {
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const taskType = statusText.TYPE_KEY === PROJECT_BUILD_TEXT.TYPE_KEY ? 'build' : 'deploy';
|
|
35
|
+
throw new Error(lib.projectBuildAndDeploy.makePollTaskStatusFunc.errorFetchingTaskStatus(taskType));
|
|
37
36
|
}
|
|
38
37
|
function makePollTaskStatusFunc({ statusFn, structureFn, statusText, statusStrings, linkToHubSpot, }) {
|
|
39
38
|
return async function (accountId, taskName, taskId, deployedBuildId, silenceLogs = false) {
|
|
@@ -111,97 +110,103 @@ function makePollTaskStatusFunc({ statusFn, structureFn, statusText, statusStrin
|
|
|
111
110
|
task.subtasks.forEach((subtask, i) => addTaskSpinner(subtask, 4, i === task.subtasks.length - 1));
|
|
112
111
|
});
|
|
113
112
|
}
|
|
114
|
-
return new Promise(resolve => {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
115
114
|
const pollInterval = setInterval(async () => {
|
|
116
|
-
let taskStatus;
|
|
117
115
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (status === statusText.STATES.SUCCESS ||
|
|
143
|
-
status === statusText.STATES.FAILURE) {
|
|
144
|
-
const taskStatusText = subtask.status === statusText.STATES.SUCCESS
|
|
145
|
-
? lib.projectBuildAndDeploy.makePollTaskStatusFunc
|
|
146
|
-
.successStatusText
|
|
147
|
-
: lib.projectBuildAndDeploy.makePollTaskStatusFunc
|
|
148
|
-
.failedStatusText;
|
|
149
|
-
const hasNewline = spinner?.text?.includes('\n') || Boolean(topLevelTask);
|
|
150
|
-
const updatedText = `${spinner?.text?.replace('\n', '')} ${taskStatusText}${hasNewline ? '\n' : ''}`;
|
|
151
|
-
if (status === statusText.STATES.SUCCESS) {
|
|
152
|
-
SpinniesManager.succeed(id, { text: updatedText });
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
SpinniesManager.fail(id, { text: updatedText });
|
|
116
|
+
let taskStatus;
|
|
117
|
+
try {
|
|
118
|
+
const { data } = await statusFn(accountId, taskName, taskId);
|
|
119
|
+
taskStatus = data;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
uiLogger.debug(e);
|
|
123
|
+
logError(e, new ApiErrorContext({
|
|
124
|
+
accountId,
|
|
125
|
+
projectName: taskName,
|
|
126
|
+
}));
|
|
127
|
+
handleTaskStatusError(statusText);
|
|
128
|
+
}
|
|
129
|
+
const subtasks = getSubtasks(taskStatus);
|
|
130
|
+
if (!taskStatus || !taskStatus.status || !subtasks) {
|
|
131
|
+
handleTaskStatusError(statusText);
|
|
132
|
+
}
|
|
133
|
+
const { status } = taskStatus;
|
|
134
|
+
if (SpinniesManager.hasActiveSpinners()) {
|
|
135
|
+
subtasks.forEach(subtask => {
|
|
136
|
+
const { id, status } = subtask;
|
|
137
|
+
const spinner = SpinniesManager.pick(id);
|
|
138
|
+
if (!spinner || spinner.status !== SPINNER_STATUS.SPINNING) {
|
|
139
|
+
return;
|
|
156
140
|
}
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
const topLevelTask = structuredTasks.find(t => t.id == id);
|
|
142
|
+
if (status === statusText.STATES.SUCCESS ||
|
|
143
|
+
status === statusText.STATES.FAILURE) {
|
|
144
|
+
const taskStatusText = subtask.status === statusText.STATES.SUCCESS
|
|
145
|
+
? lib.projectBuildAndDeploy.makePollTaskStatusFunc
|
|
146
|
+
.successStatusText
|
|
147
|
+
: lib.projectBuildAndDeploy.makePollTaskStatusFunc
|
|
148
|
+
.failedStatusText;
|
|
149
|
+
const hasNewline = spinner?.text?.includes('\n') || Boolean(topLevelTask);
|
|
150
|
+
const updatedText = `${spinner?.text?.replace('\n', '')} ${taskStatusText}${hasNewline ? '\n' : ''}`;
|
|
151
|
+
if (status === statusText.STATES.SUCCESS) {
|
|
152
|
+
SpinniesManager.succeed(id, { text: updatedText });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
SpinniesManager.fail(id, { text: updatedText });
|
|
156
|
+
}
|
|
157
|
+
if (topLevelTask) {
|
|
158
|
+
topLevelTask.subtasks.forEach(currentSubtask => SpinniesManager.remove(currentSubtask.id));
|
|
159
|
+
}
|
|
159
160
|
}
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
if (status === statusText.STATES.SUCCESS) {
|
|
163
|
-
SpinniesManager.succeed(overallTaskSpinniesKey, {
|
|
164
|
-
text: statusStrings.SUCCESS(taskName, displayId),
|
|
165
|
-
});
|
|
166
|
-
clearInterval(pollInterval);
|
|
167
|
-
resolve(taskStatus);
|
|
168
|
-
}
|
|
169
|
-
else if (status === statusText.STATES.FAILURE) {
|
|
170
|
-
SpinniesManager.fail(overallTaskSpinniesKey, {
|
|
171
|
-
text: statusStrings.FAIL(taskName, displayId),
|
|
172
161
|
});
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
uiLogger.log(`${statusStrings.SUBTASK_FAIL(failedSubtasks.length === 1
|
|
177
|
-
? getSubtaskName(failedSubtasks[0])
|
|
178
|
-
: failedSubtasks.length + ' components', displayId)}\n`);
|
|
179
|
-
uiLogger.log(lib.projectBuildAndDeploy.makePollTaskStatusFunc.errorSummary);
|
|
180
|
-
uiLine();
|
|
181
|
-
const displayErrors = failedSubtasks.filter(subtask => subtask?.standardError?.subCategory !==
|
|
182
|
-
PROJECT_ERROR_TYPES.SUBBUILD_FAILED &&
|
|
183
|
-
subtask?.standardError?.subCategory !==
|
|
184
|
-
PROJECT_ERROR_TYPES.SUBDEPLOY_FAILED);
|
|
185
|
-
displayErrors.forEach(subTask => {
|
|
186
|
-
uiLogger.log(`\n--- ${chalk.bold(getSubtaskName(subTask))} failed with the following error ---`);
|
|
187
|
-
uiLogger.error(subTask.errorMessage);
|
|
188
|
-
// Log nested errors
|
|
189
|
-
if (subTask.standardError && subTask.standardError.errors) {
|
|
190
|
-
uiLogger.log('');
|
|
191
|
-
subTask.standardError.errors.forEach(error => {
|
|
192
|
-
uiLogger.log(error.message);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
162
|
+
if (status === statusText.STATES.SUCCESS) {
|
|
163
|
+
SpinniesManager.succeed(overallTaskSpinniesKey, {
|
|
164
|
+
text: statusStrings.SUCCESS(taskName, displayId),
|
|
195
165
|
});
|
|
166
|
+
clearInterval(pollInterval);
|
|
167
|
+
resolve(taskStatus);
|
|
168
|
+
}
|
|
169
|
+
else if (status === statusText.STATES.FAILURE) {
|
|
170
|
+
SpinniesManager.fail(overallTaskSpinniesKey, {
|
|
171
|
+
text: statusStrings.FAIL(taskName, displayId),
|
|
172
|
+
});
|
|
173
|
+
if (!silenceLogs) {
|
|
174
|
+
const failedSubtasks = subtasks.filter(subtask => subtask.status === 'FAILURE');
|
|
175
|
+
uiLine();
|
|
176
|
+
uiLogger.log(`${statusStrings.SUBTASK_FAIL(failedSubtasks.length === 1
|
|
177
|
+
? getSubtaskName(failedSubtasks[0])
|
|
178
|
+
: failedSubtasks.length + ' components', displayId)}\n`);
|
|
179
|
+
uiLogger.log(lib.projectBuildAndDeploy.makePollTaskStatusFunc.errorSummary);
|
|
180
|
+
uiLine();
|
|
181
|
+
const displayErrors = failedSubtasks.filter(subtask => subtask?.standardError?.subCategory !==
|
|
182
|
+
PROJECT_ERROR_TYPES.SUBBUILD_FAILED &&
|
|
183
|
+
subtask?.standardError?.subCategory !==
|
|
184
|
+
PROJECT_ERROR_TYPES.SUBDEPLOY_FAILED);
|
|
185
|
+
displayErrors.forEach(subTask => {
|
|
186
|
+
uiLogger.log(`\n--- ${chalk.bold(getSubtaskName(subTask))} failed with the following error ---`);
|
|
187
|
+
uiLogger.error(subTask.errorMessage);
|
|
188
|
+
// Log nested errors
|
|
189
|
+
if (subTask.standardError && subTask.standardError.errors) {
|
|
190
|
+
uiLogger.log('');
|
|
191
|
+
subTask.standardError.errors.forEach(error => {
|
|
192
|
+
uiLogger.log(error.message);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
clearInterval(pollInterval);
|
|
198
|
+
resolve(taskStatus);
|
|
199
|
+
}
|
|
200
|
+
else if (!subtasks.length) {
|
|
201
|
+
clearInterval(pollInterval);
|
|
202
|
+
resolve(taskStatus);
|
|
196
203
|
}
|
|
197
|
-
clearInterval(pollInterval);
|
|
198
|
-
resolve(taskStatus);
|
|
199
|
-
}
|
|
200
|
-
else if (!subtasks.length) {
|
|
201
|
-
clearInterval(pollInterval);
|
|
202
|
-
resolve(taskStatus);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
clearInterval(pollInterval);
|
|
208
|
+
reject(e);
|
|
209
|
+
}
|
|
205
210
|
}, DEFAULT_POLLING_DELAY);
|
|
206
211
|
});
|
|
207
212
|
};
|
package/lib/projects/upload.d.ts
CHANGED
package/lib/projects/upload.js
CHANGED
|
@@ -8,13 +8,11 @@ import { isTranslationError, translate, } from '@hubspot/project-parsing-lib/tra
|
|
|
8
8
|
import { projectContainsHsMetaFiles } from '@hubspot/project-parsing-lib/projects';
|
|
9
9
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
10
10
|
import { uiAccountDescription } from '../ui/index.js';
|
|
11
|
-
import { logError } from '../errorHandlers/index.js';
|
|
12
11
|
import util from 'node:util';
|
|
13
12
|
import { lib } from '../../lang/en.js';
|
|
14
13
|
import { ensureProjectExists } from './ensureProjectExists.js';
|
|
15
14
|
import { uiLogger } from '../ui/logger.js';
|
|
16
15
|
import { isV2Project } from './platformVersion.js';
|
|
17
|
-
import { EXIT_CODES } from '../enums/exitCodes.js';
|
|
18
16
|
import ProjectValidationError from '../errors/ProjectValidationError.js';
|
|
19
17
|
import { walk } from '@hubspot/local-dev-lib/fs';
|
|
20
18
|
import { LEGACY_CONFIG_FILES } from '../constants.js';
|
|
@@ -46,57 +44,50 @@ async function uploadProjectFiles(accountId, projectName, filePath, uploadMessag
|
|
|
46
44
|
}
|
|
47
45
|
export async function handleProjectUpload({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage = '', forceCreate = false, isUploadCommand = false, sendIR = false, skipValidation = false, }) {
|
|
48
46
|
const srcDir = path.resolve(projectDir, projectConfig.srcDir);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
logError(e);
|
|
54
|
-
process.exit(EXIT_CODES.ERROR);
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
await validateNoHSMetaMismatch(srcDir, projectConfig);
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
logError(e);
|
|
61
|
-
process.exit(EXIT_CODES.ERROR);
|
|
62
|
-
}
|
|
47
|
+
await validateSourceDirectory(srcDir, projectConfig, projectDir);
|
|
48
|
+
await validateNoHSMetaMismatch(srcDir, projectConfig);
|
|
63
49
|
const tempFile = tmp.fileSync({ postfix: '.zip' });
|
|
64
50
|
uiLogger.debug(lib.projectUpload.handleProjectUpload.compressing(tempFile.name));
|
|
65
51
|
const output = fs.createWriteStream(tempFile.name);
|
|
66
52
|
const archive = archiver('zip');
|
|
67
|
-
const result = new Promise(resolve => output.on('close', async function () {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
53
|
+
const result = new Promise((resolve, reject) => output.on('close', async function () {
|
|
54
|
+
try {
|
|
55
|
+
uiLogger.debug(lib.projectUpload.handleProjectUpload.compressed(archive.pointer()));
|
|
56
|
+
let intermediateRepresentation;
|
|
57
|
+
if (sendIR) {
|
|
58
|
+
try {
|
|
59
|
+
intermediateRepresentation = await handleTranslate({
|
|
60
|
+
projectDir,
|
|
61
|
+
projectConfig,
|
|
62
|
+
accountId,
|
|
63
|
+
skipValidation,
|
|
64
|
+
profile,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return resolve({ uploadError: e });
|
|
69
|
+
}
|
|
79
70
|
}
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
const { projectExists } = await ensureProjectExists(accountId, projectConfig.name, {
|
|
72
|
+
forceCreate,
|
|
73
|
+
uploadCommand: isUploadCommand,
|
|
74
|
+
noLogs: true,
|
|
75
|
+
});
|
|
76
|
+
if (!projectExists) {
|
|
77
|
+
uiLogger.log(lib.projectUpload.handleProjectUpload.projectDoesNotExist(accountId));
|
|
78
|
+
return resolve({ projectNotFound: true });
|
|
79
|
+
}
|
|
80
|
+
const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation);
|
|
81
|
+
if (error) {
|
|
82
|
+
resolve({ uploadError: error });
|
|
83
|
+
}
|
|
84
|
+
else if (callbackFunc) {
|
|
85
|
+
const uploadResult = await callbackFunc(accountId, projectConfig, tempFile, buildId);
|
|
86
|
+
resolve({ result: uploadResult });
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
uploadCommand: isUploadCommand,
|
|
87
|
-
noLogs: true,
|
|
88
|
-
});
|
|
89
|
-
if (!projectExists) {
|
|
90
|
-
uiLogger.log(lib.projectUpload.handleProjectUpload.projectDoesNotExist(accountId));
|
|
91
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
92
|
-
}
|
|
93
|
-
const { buildId, error } = await uploadProjectFiles(accountId, projectConfig.name, tempFile.name, uploadMessage, projectConfig.platformVersion, intermediateRepresentation);
|
|
94
|
-
if (error) {
|
|
95
|
-
resolve({ uploadError: error });
|
|
96
|
-
}
|
|
97
|
-
else if (callbackFunc) {
|
|
98
|
-
const uploadResult = await callbackFunc(accountId, projectConfig, tempFile, buildId);
|
|
99
|
-
resolve({ result: uploadResult });
|
|
89
|
+
catch (e) {
|
|
90
|
+
reject(e);
|
|
100
91
|
}
|
|
101
92
|
}));
|
|
102
93
|
archive.pipe(output);
|