@hubspot/cli 7.8.0-experimental.0 → 7.8.2-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +0 -2
- package/commands/__tests__/getStarted.test.js +2 -2
- package/commands/__tests__/mcp.test.js +1 -1
- package/commands/__tests__/project.test.js +0 -3
- package/commands/app/__tests__/migrate.test.js +0 -1
- package/commands/app/migrate.js +4 -5
- package/commands/app/secret/add.js +2 -1
- package/commands/app/secret/delete.js +2 -1
- package/commands/app/secret/list.js +2 -1
- package/commands/app/secret/update.js +2 -1
- package/commands/app/secret.js +2 -1
- package/commands/app.js +2 -2
- package/commands/config/set.js +0 -1
- package/commands/feedback.js +1 -1
- package/commands/getStarted.d.ts +1 -3
- package/commands/getStarted.js +66 -18
- package/commands/mcp/__tests__/setup.test.js +2 -2
- package/commands/mcp/setup.js +11 -2
- package/commands/mcp.js +3 -3
- package/commands/project/__tests__/create.test.js +6 -6
- package/commands/project/__tests__/deploy.test.js +0 -3
- package/commands/project/__tests__/devUnifiedFlow.test.js +2 -4
- package/commands/project/__tests__/logs.test.js +0 -3
- package/commands/project/__tests__/migrate.test.js +1 -2
- package/commands/project/__tests__/migrateApp.test.js +1 -2
- package/commands/project/__tests__/profile.test.js +1 -1
- package/commands/project/add.js +1 -5
- package/commands/project/create.js +3 -9
- package/commands/project/deploy.js +2 -2
- package/commands/project/dev/index.js +4 -3
- package/commands/project/dev/unifiedFlow.js +6 -4
- package/commands/project/download.js +1 -2
- package/commands/project/installDeps.js +1 -2
- package/commands/project/listBuilds.js +2 -2
- package/commands/project/logs.js +2 -2
- package/commands/project/migrate.js +28 -10
- package/commands/project/migrateApp.js +1 -2
- package/commands/project/open.js +1 -2
- package/commands/project/profile/add.js +3 -3
- package/commands/project/profile/delete.js +1 -2
- package/commands/project/profile.js +2 -3
- package/commands/project/upload.js +2 -2
- package/commands/project/validate.js +1 -1
- package/commands/project/watch.js +2 -2
- package/commands/project.js +1 -2
- package/commands/sandbox/delete.js +1 -1
- package/commands/testAccount/importData.d.ts +1 -1
- package/commands/testAccount/importData.js +1 -1
- package/commands/testAccount.js +1 -1
- package/lang/en.d.ts +15 -4
- package/lang/en.js +18 -6
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/app/__tests__/migrate.test.js +14 -51
- package/lib/app/migrate.d.ts +2 -8
- package/lib/app/migrate.js +5 -80
- package/lib/constants.d.ts +8 -0
- package/lib/constants.js +8 -0
- package/lib/dependencyManagement.d.ts +0 -5
- package/lib/dependencyManagement.js +0 -9
- package/lib/hasFeature.js +6 -0
- package/lib/links.d.ts +1 -0
- package/lib/links.js +10 -3
- package/lib/mcp/setup.js +1 -1
- package/lib/middleware/fireAlarmMiddleware.js +15 -5
- package/lib/projects/__tests__/LocalDevProcess.test.js +227 -16
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +16 -21
- package/lib/projects/__tests__/deploy.test.js +71 -6
- package/lib/projects/__tests__/localDevProjectHelpers.test.js +4 -2
- package/lib/projects/create/__tests__/v3.test.js +79 -4
- package/lib/projects/create/v3.js +11 -8
- package/lib/projects/localDev/AppDevModeInterface.js +5 -5
- package/lib/projects/localDev/LocalDevLogger.d.ts +4 -0
- package/lib/projects/localDev/LocalDevLogger.js +22 -0
- package/lib/projects/localDev/LocalDevProcess.d.ts +7 -5
- package/lib/projects/localDev/LocalDevProcess.js +90 -19
- package/lib/projects/localDev/LocalDevState.d.ts +9 -8
- package/lib/projects/localDev/LocalDevState.js +18 -17
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.js +55 -23
- package/lib/projects/localDev/helpers/project.d.ts +2 -2
- package/lib/projects/localDev/helpers/project.js +10 -7
- package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +4 -0
- package/lib/projects/localDev/localDevWebsocketServerUtils.js +10 -0
- package/lib/projects/pollProjectBuildAndDeploy.js +4 -4
- package/lib/prompts/projectAddPrompt.js +2 -1
- package/lib/prompts/promptUtils.js +3 -0
- package/lib/prompts/selectProjectTemplatePrompt.js +2 -0
- package/lib/theme/__tests__/migrate.test.d.ts +1 -0
- package/lib/theme/__tests__/migrate.test.js +233 -0
- package/lib/theme/migrate.d.ts +13 -0
- package/lib/theme/migrate.js +90 -0
- package/lib/ui/SpinniesManager.js +105 -8
- package/lib/usageTracking.js +2 -2
- 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 +2 -2
- 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 -1
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +5 -5
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/DocFetchTool.js +2 -2
- package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocsSearchTool.js +7 -7
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +11 -5
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
- package/mcp-server/tools/project/UploadProjectTools.js +2 -2
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
- package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +9 -3
- package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
- package/mcp-server/utils/cliConfig.d.ts +1 -0
- package/mcp-server/utils/cliConfig.js +12 -0
- package/package.json +4 -9
- package/types/LocalDev.d.ts +19 -3
- package/ui/components/HorizontalSelectPrompt.js +1 -1
- package/ui/index.js +1 -1
- package/commands/getStartedV2.d.ts +0 -9
- package/commands/getStartedV2.js +0 -39
- package/ui/components/Ascii.d.ts +0 -10
- package/ui/components/Ascii.js +0 -11
- package/ui/views/GetStarted.d.ts +0 -7
- package/ui/views/GetStarted.js +0 -157
package/lib/constants.js
CHANGED
|
@@ -73,17 +73,25 @@ export const FEATURES = {
|
|
|
73
73
|
SANDBOXES_V2: 'sandboxes:v2:enabled',
|
|
74
74
|
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
75
75
|
APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
|
|
76
|
+
APPS_HOME: 'UIE:AppHome',
|
|
77
|
+
MCP_ACCESS: 'Developers:CLIMCPAccess',
|
|
78
|
+
THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
|
|
79
|
+
AGENT_TOOLS: 'ThirdPartyAgentTools',
|
|
76
80
|
};
|
|
77
81
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
78
82
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
|
79
83
|
UPLOAD_FAILURE: 'server:uploadFailure',
|
|
84
|
+
DEPLOY_SUCCESS: 'server:deploySuccess',
|
|
85
|
+
DEPLOY_FAILURE: 'server:deployFailure',
|
|
80
86
|
UPDATE_PROJECT_NODES: 'server:updateProjectNodes',
|
|
81
87
|
UPDATE_APP_DATA: 'server:updateAppData',
|
|
82
88
|
UPDATE_PROJECT_DATA: 'server:updateProjectData',
|
|
83
89
|
UPDATE_UPLOAD_WARNINGS: 'server:updateUploadWarnings',
|
|
90
|
+
CLI_METADATA: 'server:cliMetadata',
|
|
84
91
|
};
|
|
85
92
|
export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
|
|
86
93
|
UPLOAD: 'client:upload',
|
|
94
|
+
DEPLOY: 'client:deploy',
|
|
87
95
|
VIEWED_WELCOME_SCREEN: 'client:viewedWelcomeScreen',
|
|
88
96
|
};
|
|
89
97
|
export const APP_INSTALLATION_STATES = {
|
|
@@ -2,10 +2,5 @@ export declare function installPackages({ packages, installLocations, }: {
|
|
|
2
2
|
packages?: string[];
|
|
3
3
|
installLocations?: string[];
|
|
4
4
|
}): Promise<void>;
|
|
5
|
-
export declare function installPackagesV2({ packages, installLocations, }: {
|
|
6
|
-
packages?: string[];
|
|
7
|
-
installLocations?: string[];
|
|
8
|
-
}): Promise<void>;
|
|
9
|
-
export declare function installPackagesInDirectoryV2(directory: string, packages?: string[]): Promise<void>;
|
|
10
5
|
export declare function getProjectPackageJsonLocations(dir?: string): Promise<string[]>;
|
|
11
6
|
export declare function hasMissingPackages(directory: string): Promise<boolean>;
|
|
@@ -22,15 +22,6 @@ export async function installPackages({ packages, installLocations, }) {
|
|
|
22
22
|
await installPackagesInDirectory(dir, packages);
|
|
23
23
|
}));
|
|
24
24
|
}
|
|
25
|
-
export async function installPackagesV2({ packages, installLocations, }) {
|
|
26
|
-
const installDirs = installLocations || (await getProjectPackageJsonLocations());
|
|
27
|
-
await Promise.all(installDirs.map(async (dir) => {
|
|
28
|
-
await installPackagesInDirectoryV2(dir, packages);
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
export async function installPackagesInDirectoryV2(directory, packages) {
|
|
32
|
-
await executeInstall(packages, null, { cwd: directory });
|
|
33
|
-
}
|
|
34
25
|
async function installPackagesInDirectory(directory, packages) {
|
|
35
26
|
const spinner = `installingDependencies-${directory}`;
|
|
36
27
|
const relativeDir = path.relative(process.cwd(), directory);
|
package/lib/hasFeature.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { http } from '@hubspot/local-dev-lib/http';
|
|
2
2
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
3
|
+
import { FEATURES } from './constants.js';
|
|
4
|
+
const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
|
|
3
5
|
export async function hasFeature(accountId, feature) {
|
|
4
6
|
const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
|
|
7
|
+
if (enabledFeatures[feature] === undefined &&
|
|
8
|
+
FEATURES_THAT_DEFAULT_ON.includes(feature)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
5
11
|
return Boolean(enabledFeatures[feature]);
|
|
6
12
|
}
|
|
7
13
|
export async function hasUnfiedAppsAccess(accountId) {
|
package/lib/links.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ type SiteLink = {
|
|
|
7
7
|
export declare function getSiteLinksAsArray(accountId: number): SiteLink[];
|
|
8
8
|
export declare function logSiteLinks(accountId: number): void;
|
|
9
9
|
export declare function openLink(accountId: number, shortcut: string): void;
|
|
10
|
+
export declare function getProductUpdatesUrl(rolloutId: string, accountId?: number): string;
|
|
10
11
|
export {};
|
package/lib/links.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
|
|
|
4
4
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
5
5
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
6
6
|
import { getTableContents, getTableHeader } from './ui/table.js';
|
|
7
|
-
const
|
|
7
|
+
const COMMON_SITE_LINKS = {
|
|
8
8
|
APPS_MARKETPLACE: {
|
|
9
9
|
shortcut: 'apps-marketplace',
|
|
10
10
|
alias: 'apm',
|
|
@@ -76,7 +76,7 @@ const SITE_LINKS = {
|
|
|
76
76
|
};
|
|
77
77
|
export function getSiteLinksAsArray(accountId) {
|
|
78
78
|
const baseUrl = getHubSpotWebsiteOrigin(getEnv() === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
|
|
79
|
-
return Object.values(
|
|
79
|
+
return Object.values(COMMON_SITE_LINKS)
|
|
80
80
|
.sort((a, b) => (a.shortcut < b.shortcut ? -1 : 1))
|
|
81
81
|
.map(l => ({ ...l, url: l.getUrl(accountId, baseUrl) }));
|
|
82
82
|
}
|
|
@@ -90,7 +90,7 @@ export function logSiteLinks(accountId) {
|
|
|
90
90
|
logger.log(getTableContents(linksAsArray));
|
|
91
91
|
}
|
|
92
92
|
export function openLink(accountId, shortcut) {
|
|
93
|
-
const match = Object.values(
|
|
93
|
+
const match = Object.values(COMMON_SITE_LINKS).find(l => l.shortcut === shortcut || (l.alias && l.alias === shortcut));
|
|
94
94
|
if (!match) {
|
|
95
95
|
logger.error(`We couldn't find a shortcut matching ${shortcut}. Type 'hs open list' to see a list of available shortcuts`);
|
|
96
96
|
return;
|
|
@@ -99,3 +99,10 @@ export function openLink(accountId, shortcut) {
|
|
|
99
99
|
open(match.getUrl(accountId, baseUrl), { url: true });
|
|
100
100
|
logger.success(`We opened ${match.getUrl(accountId, baseUrl)} in your browser`);
|
|
101
101
|
}
|
|
102
|
+
export function getProductUpdatesUrl(rolloutId, accountId) {
|
|
103
|
+
const baseUrl = getHubSpotWebsiteOrigin(getEnv() === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
|
|
104
|
+
if (accountId) {
|
|
105
|
+
return `${baseUrl}/product-updates/${accountId}/in-beta?rollout=${rolloutId}`;
|
|
106
|
+
}
|
|
107
|
+
return `${baseUrl}/l/product-updates/in-beta?rollout=${rolloutId}`;
|
|
108
|
+
}
|
package/lib/mcp/setup.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
10
|
import { existsSync } from 'fs';
|
|
11
|
-
const mcpServerName = '
|
|
11
|
+
const mcpServerName = 'HubSpotDev';
|
|
12
12
|
const claudeCode = 'claude';
|
|
13
13
|
const windsurf = 'windsurf';
|
|
14
14
|
const cursor = 'cursor';
|
|
@@ -3,6 +3,8 @@ import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
|
|
|
3
3
|
import { debugError } from '../errorHandlers/index.js';
|
|
4
4
|
import pkg from '../../package.json' with { type: 'json' };
|
|
5
5
|
import { logInBox } from '../ui/boxen.js';
|
|
6
|
+
import { renderInline } from '../../ui/index.js';
|
|
7
|
+
import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
6
8
|
/*
|
|
7
9
|
* Versions can be formatted like this:
|
|
8
10
|
* =7.2.2 -> targets the exact version 7.2.2
|
|
@@ -98,12 +100,20 @@ async function logFireAlarms(accountId, command, version) {
|
|
|
98
100
|
}
|
|
99
101
|
return acc;
|
|
100
102
|
}, '');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
if (!process.env.HUBSPOT_ENABLE_INK) {
|
|
104
|
+
await logInBox({
|
|
105
|
+
contents: notifications,
|
|
106
|
+
options: {
|
|
107
|
+
title: 'Notifications',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await renderInline(getWarningBox({
|
|
104
113
|
title: 'Notifications',
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
message: notifications,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
export async function checkFireAlarms(argv) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { translateForLocalDev } from '@hubspot/project-parsing-lib';
|
|
3
3
|
import { handleProjectUpload } from '../upload.js';
|
|
4
|
+
import { handleProjectDeploy } from '../deploy.js';
|
|
4
5
|
import { getProjectConfig } from '../config.js';
|
|
6
|
+
import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
|
|
7
|
+
import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
|
|
5
8
|
import LocalDevProcess from '../localDev/LocalDevProcess.js';
|
|
6
9
|
import LocalDevLogger from '../localDev/LocalDevLogger.js';
|
|
7
10
|
import DevServerManagerV2 from '../localDev/DevServerManagerV2.js';
|
|
@@ -19,7 +22,10 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => ({
|
|
|
19
22
|
vi.mock('open');
|
|
20
23
|
vi.mock('@hubspot/project-parsing-lib');
|
|
21
24
|
vi.mock('../upload');
|
|
25
|
+
vi.mock('../deploy');
|
|
22
26
|
vi.mock('../config');
|
|
27
|
+
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
28
|
+
vi.mock('@hubspot/local-dev-lib/errors/index');
|
|
23
29
|
vi.mock('../localDev/LocalDevLogger');
|
|
24
30
|
vi.mock('../localDev/DevServerManagerV2');
|
|
25
31
|
// Tests for LocalDevProcess and LocalDevState
|
|
@@ -37,11 +43,35 @@ describe('LocalDevProcess', () => {
|
|
|
37
43
|
projectConfig: mockProjectConfig,
|
|
38
44
|
targetProjectAccountId: 123,
|
|
39
45
|
targetTestingAccountId: 456,
|
|
40
|
-
|
|
46
|
+
projectData: {
|
|
47
|
+
id: 789,
|
|
48
|
+
name: 'test-project',
|
|
49
|
+
portalId: 123,
|
|
50
|
+
createdAt: 0,
|
|
51
|
+
deletedAt: 0,
|
|
52
|
+
isLocked: false,
|
|
53
|
+
updatedAt: 0,
|
|
54
|
+
latestBuild: {
|
|
55
|
+
activitySource: { type: 'HUBSPOT_USER', userId: 456 },
|
|
56
|
+
buildId: 123,
|
|
57
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
58
|
+
deployableState: 'DEPLOYABLE',
|
|
59
|
+
deployStatusTaskLocator: { id: 'task-123', links: [] },
|
|
60
|
+
enqueuedAt: '2023-01-01T00:00:00Z',
|
|
61
|
+
finishedAt: '2023-01-01T00:05:00Z',
|
|
62
|
+
isAutoDeployEnabled: false,
|
|
63
|
+
portalId: 123,
|
|
64
|
+
projectName: 'test-project',
|
|
65
|
+
startedAt: '2023-01-01T00:01:00Z',
|
|
66
|
+
status: 'SUCCESS',
|
|
67
|
+
subbuildStatuses: [],
|
|
68
|
+
uploadMessage: 'Build completed',
|
|
69
|
+
autoDeployId: 0,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
41
72
|
initialProjectNodes: {},
|
|
42
73
|
initialProjectProfileData: {},
|
|
43
74
|
env: ENVIRONMENTS.PROD,
|
|
44
|
-
projectName: 'test-project',
|
|
45
75
|
};
|
|
46
76
|
beforeEach(() => {
|
|
47
77
|
vi.clearAllMocks();
|
|
@@ -62,6 +92,9 @@ describe('LocalDevProcess', () => {
|
|
|
62
92
|
uploadSuccess: vi.fn(),
|
|
63
93
|
fileChangeError: vi.fn(),
|
|
64
94
|
uploadWarning: vi.fn(),
|
|
95
|
+
deployInitiated: vi.fn(),
|
|
96
|
+
deployError: vi.fn(),
|
|
97
|
+
deploySuccess: vi.fn(),
|
|
65
98
|
};
|
|
66
99
|
mockDevServerManager = {
|
|
67
100
|
setup: vi.fn().mockResolvedValue(undefined),
|
|
@@ -72,6 +105,8 @@ describe('LocalDevProcess', () => {
|
|
|
72
105
|
// Mock constructors
|
|
73
106
|
LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
|
|
74
107
|
DevServerManagerV2.mockImplementation(() => mockDevServerManager);
|
|
108
|
+
// Mock external functions
|
|
109
|
+
isHubSpotHttpError.mockReturnValue(false);
|
|
75
110
|
// Create process instance
|
|
76
111
|
process = new LocalDevProcess(mockOptions);
|
|
77
112
|
// Mock process.exit
|
|
@@ -141,9 +176,14 @@ describe('LocalDevProcess', () => {
|
|
|
141
176
|
handleProjectUpload.mockResolvedValue({
|
|
142
177
|
uploadError: new Error('Upload failed'),
|
|
143
178
|
});
|
|
144
|
-
const
|
|
179
|
+
const result = await process.uploadProject();
|
|
145
180
|
expect(mockLocalDevLogger.uploadError).toHaveBeenCalledWith(new Error('Upload failed'));
|
|
146
|
-
expect(
|
|
181
|
+
expect(result).toEqual({
|
|
182
|
+
uploadSuccess: false,
|
|
183
|
+
buildSuccess: false,
|
|
184
|
+
deploySuccess: false,
|
|
185
|
+
deployId: undefined,
|
|
186
|
+
});
|
|
147
187
|
});
|
|
148
188
|
it('should handle successful upload', async () => {
|
|
149
189
|
await process.handleConfigFileChange();
|
|
@@ -152,38 +192,120 @@ describe('LocalDevProcess', () => {
|
|
|
152
192
|
});
|
|
153
193
|
handleProjectUpload.mockResolvedValue({
|
|
154
194
|
uploadError: null,
|
|
195
|
+
result: {
|
|
196
|
+
deployResult: {
|
|
197
|
+
id: 'deploy-123',
|
|
198
|
+
deployId: 123,
|
|
199
|
+
status: 'SUCCESS',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
fetchProject.mockResolvedValue({
|
|
204
|
+
data: {
|
|
205
|
+
id: 789,
|
|
206
|
+
name: 'test-project',
|
|
207
|
+
portalId: 123,
|
|
208
|
+
createdAt: 0,
|
|
209
|
+
deletedAt: 0,
|
|
210
|
+
isLocked: false,
|
|
211
|
+
updatedAt: 0,
|
|
212
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
213
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
214
|
+
},
|
|
155
215
|
});
|
|
156
|
-
const
|
|
216
|
+
const result = await process.uploadProject();
|
|
217
|
+
expect(fetchProject).toHaveBeenCalledWith(mockOptions.targetProjectAccountId, mockOptions.projectConfig.name);
|
|
157
218
|
expect(mockLocalDevLogger.uploadSuccess).toHaveBeenCalled();
|
|
158
219
|
// @ts-expect-error accessing private property for testing
|
|
159
220
|
expect(process.state.uploadWarnings.size).toBe(0);
|
|
160
|
-
expect(
|
|
221
|
+
expect(result).toEqual({
|
|
222
|
+
uploadSuccess: true,
|
|
223
|
+
buildSuccess: true,
|
|
224
|
+
deploySuccess: true,
|
|
225
|
+
deployId: 123,
|
|
226
|
+
});
|
|
161
227
|
});
|
|
162
|
-
it('should reset projectNodesAtLastUpload', async () => {
|
|
163
|
-
const
|
|
164
|
-
|
|
228
|
+
it('should reset projectNodesAtLastUpload if deploy is successful', async () => {
|
|
229
|
+
const mockInitialNodes = {
|
|
230
|
+
node1: {
|
|
231
|
+
uid: 'node1',
|
|
232
|
+
componentType: 'APP',
|
|
233
|
+
localDev: {
|
|
234
|
+
componentRoot: '/test/path',
|
|
235
|
+
componentConfigPath: '/test/path/config.json',
|
|
236
|
+
configUpdatedSinceLastUpload: false,
|
|
237
|
+
},
|
|
238
|
+
componentDeps: {},
|
|
239
|
+
metaFilePath: '/test/path',
|
|
240
|
+
config: { name: 'Node 1' },
|
|
241
|
+
files: [],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const mockNewNodes = {
|
|
245
|
+
node1: {
|
|
246
|
+
uid: 'node2',
|
|
247
|
+
componentType: 'APP',
|
|
248
|
+
localDev: {
|
|
249
|
+
componentRoot: '/test/path',
|
|
250
|
+
componentConfigPath: '/test/path/config.json',
|
|
251
|
+
configUpdatedSinceLastUpload: false,
|
|
252
|
+
},
|
|
253
|
+
componentDeps: {},
|
|
254
|
+
metaFilePath: '/test/path',
|
|
255
|
+
config: { name: 'Node 2' },
|
|
256
|
+
files: [],
|
|
257
|
+
},
|
|
258
|
+
};
|
|
165
259
|
// @ts-expect-error accessing private property for testing
|
|
166
|
-
process.state.
|
|
260
|
+
process.state.projectNodesAtLastDeploy = mockInitialNodes;
|
|
167
261
|
getProjectConfig.mockResolvedValue({
|
|
168
262
|
projectConfig: mockOptions.projectConfig,
|
|
169
263
|
});
|
|
170
264
|
handleProjectUpload.mockResolvedValue({
|
|
171
265
|
uploadError: null,
|
|
266
|
+
result: {
|
|
267
|
+
deployResult: {
|
|
268
|
+
id: 'deploy-123',
|
|
269
|
+
deployId: 456,
|
|
270
|
+
status: 'SUCCESS',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
172
273
|
});
|
|
173
274
|
translateForLocalDev.mockResolvedValue({
|
|
174
|
-
intermediateNodesIndexedByUid:
|
|
275
|
+
intermediateNodesIndexedByUid: mockNewNodes,
|
|
175
276
|
});
|
|
176
|
-
|
|
177
|
-
|
|
277
|
+
fetchProject.mockResolvedValue({
|
|
278
|
+
data: {
|
|
279
|
+
id: 789,
|
|
280
|
+
name: 'test-project',
|
|
281
|
+
portalId: 123,
|
|
282
|
+
createdAt: 0,
|
|
283
|
+
deletedAt: 0,
|
|
284
|
+
isLocked: false,
|
|
285
|
+
updatedAt: 0,
|
|
286
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
287
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const result = await process.uploadProject();
|
|
291
|
+
// Verify translateForLocalDev was called during updateProjectNodesAfterDeploy
|
|
178
292
|
expect(translateForLocalDev).toHaveBeenCalledWith({
|
|
179
293
|
projectSourceDir: path.join(mockOptions.projectDir, mockOptions.projectConfig.srcDir),
|
|
180
294
|
platformVersion: mockOptions.projectConfig.platformVersion,
|
|
181
295
|
accountId: mockOptions.targetProjectAccountId,
|
|
182
|
-
}, {
|
|
296
|
+
}, {
|
|
297
|
+
profile: undefined,
|
|
298
|
+
projectNodesAtLastUpload: undefined,
|
|
299
|
+
});
|
|
183
300
|
// Verify projectNodesAtLastUpload was reset to the new nodes
|
|
184
301
|
// @ts-expect-error accessing private property for testing
|
|
185
|
-
expect(process.state.
|
|
186
|
-
expect(
|
|
302
|
+
expect(process.state.projectNodesAtLastDeploy).toEqual(mockNewNodes);
|
|
303
|
+
expect(result).toEqual({
|
|
304
|
+
uploadSuccess: true,
|
|
305
|
+
buildSuccess: true,
|
|
306
|
+
deploySuccess: true,
|
|
307
|
+
deployId: 456,
|
|
308
|
+
});
|
|
187
309
|
});
|
|
188
310
|
});
|
|
189
311
|
describe('handleFileChange()', () => {
|
|
@@ -252,4 +374,93 @@ describe('LocalDevProcess', () => {
|
|
|
252
374
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
253
375
|
});
|
|
254
376
|
});
|
|
377
|
+
describe('deployLatestBuild()', () => {
|
|
378
|
+
beforeEach(() => {
|
|
379
|
+
vi.clearAllMocks();
|
|
380
|
+
});
|
|
381
|
+
it('should successfully deploy latest build', async () => {
|
|
382
|
+
const mockDeploy = {
|
|
383
|
+
deployId: 456,
|
|
384
|
+
buildId: 123,
|
|
385
|
+
status: 'SUCCESS',
|
|
386
|
+
enqueuedAt: '2023-01-01T00:00:00Z',
|
|
387
|
+
startedAt: '2023-01-01T00:01:00Z',
|
|
388
|
+
finishedAt: '2023-01-01T00:05:00Z',
|
|
389
|
+
portalId: 123,
|
|
390
|
+
projectName: 'test-project',
|
|
391
|
+
userId: 789,
|
|
392
|
+
source: 'HUBSPOT_USER',
|
|
393
|
+
subdeployStatuses: [],
|
|
394
|
+
};
|
|
395
|
+
handleProjectDeploy.mockResolvedValue(mockDeploy);
|
|
396
|
+
const result = await process.deployLatestBuild();
|
|
397
|
+
expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
|
|
398
|
+
expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
|
|
399
|
+
'test-project', // projectName
|
|
400
|
+
123, // buildId
|
|
401
|
+
true, // useV3Api
|
|
402
|
+
false // force
|
|
403
|
+
);
|
|
404
|
+
expect(mockLocalDevLogger.deploySuccess).toHaveBeenCalled();
|
|
405
|
+
expect(result).toEqual({
|
|
406
|
+
success: true,
|
|
407
|
+
deployId: 456,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
it('should deploy with force parameter', async () => {
|
|
411
|
+
const mockDeploy = {
|
|
412
|
+
deployId: 456,
|
|
413
|
+
buildId: 123,
|
|
414
|
+
status: 'SUCCESS',
|
|
415
|
+
enqueuedAt: '2023-01-01T00:00:00Z',
|
|
416
|
+
startedAt: '2023-01-01T00:01:00Z',
|
|
417
|
+
finishedAt: '2023-01-01T00:05:00Z',
|
|
418
|
+
portalId: 123,
|
|
419
|
+
projectName: 'test-project',
|
|
420
|
+
userId: 789,
|
|
421
|
+
source: 'HUBSPOT_USER',
|
|
422
|
+
subdeployStatuses: [],
|
|
423
|
+
};
|
|
424
|
+
handleProjectDeploy.mockResolvedValue(mockDeploy);
|
|
425
|
+
const result = await process.deployLatestBuild(true);
|
|
426
|
+
expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
|
|
427
|
+
'test-project', // projectName
|
|
428
|
+
123, // buildId
|
|
429
|
+
true, // useV3Api
|
|
430
|
+
true // force
|
|
431
|
+
);
|
|
432
|
+
expect(result).toEqual({
|
|
433
|
+
success: true,
|
|
434
|
+
deployId: 456,
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
it('should return error when no build exists', async () => {
|
|
438
|
+
// Create a process without latestBuild
|
|
439
|
+
const optionsWithoutBuild = {
|
|
440
|
+
...mockOptions,
|
|
441
|
+
projectData: {
|
|
442
|
+
...mockOptions.projectData,
|
|
443
|
+
latestBuild: undefined,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
const processWithoutBuild = new LocalDevProcess(optionsWithoutBuild);
|
|
447
|
+
const result = await processWithoutBuild.deployLatestBuild();
|
|
448
|
+
expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
|
|
449
|
+
expect(mockLocalDevLogger.deployError).toHaveBeenCalledWith('Error deploying project. No build was found to deploy.');
|
|
450
|
+
expect(result).toEqual({
|
|
451
|
+
success: false,
|
|
452
|
+
});
|
|
453
|
+
expect(handleProjectDeploy).not.toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
it('should handle deploy failure when no deploy object returned', async () => {
|
|
456
|
+
handleProjectDeploy.mockResolvedValue(undefined);
|
|
457
|
+
const result = await process.deployLatestBuild();
|
|
458
|
+
expect(mockLocalDevLogger.deployInitiated).toHaveBeenCalled();
|
|
459
|
+
expect(handleProjectDeploy).toHaveBeenCalled();
|
|
460
|
+
expect(result).toEqual({
|
|
461
|
+
success: false,
|
|
462
|
+
});
|
|
463
|
+
expect(mockLocalDevLogger.deploySuccess).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
255
466
|
});
|
|
@@ -30,8 +30,16 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
30
30
|
mockLocalDevProcess = {
|
|
31
31
|
addStateListener: vi.fn(),
|
|
32
32
|
removeStateListener: vi.fn(),
|
|
33
|
-
uploadProject: vi.fn(),
|
|
33
|
+
uploadProject: vi.fn().mockResolvedValue({}),
|
|
34
34
|
sendDevServerMessage: vi.fn(),
|
|
35
|
+
projectData: {
|
|
36
|
+
name: 'test-project',
|
|
37
|
+
id: 123,
|
|
38
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
39
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
40
|
+
},
|
|
41
|
+
targetProjectAccountId: 456,
|
|
42
|
+
targetTestingAccountId: 789,
|
|
35
43
|
};
|
|
36
44
|
// Mock WebSocketServer constructor
|
|
37
45
|
WebSocketServer.mockImplementation(() => mockWebSocketServer);
|
|
@@ -225,23 +233,6 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
225
233
|
expect(mockWebSocket3.close).not.toHaveBeenCalled();
|
|
226
234
|
});
|
|
227
235
|
it('should send project data to each connection independently', () => {
|
|
228
|
-
// Setup mock project data properties as getters
|
|
229
|
-
Object.defineProperty(mockLocalDevProcess, 'projectName', {
|
|
230
|
-
get: () => 'test-project',
|
|
231
|
-
configurable: true,
|
|
232
|
-
});
|
|
233
|
-
Object.defineProperty(mockLocalDevProcess, 'projectId', {
|
|
234
|
-
get: () => 123,
|
|
235
|
-
configurable: true,
|
|
236
|
-
});
|
|
237
|
-
Object.defineProperty(mockLocalDevProcess, 'targetProjectAccountId', {
|
|
238
|
-
get: () => 456,
|
|
239
|
-
configurable: true,
|
|
240
|
-
});
|
|
241
|
-
Object.defineProperty(mockLocalDevProcess, 'targetTestingAccountId', {
|
|
242
|
-
get: () => 789,
|
|
243
|
-
configurable: true,
|
|
244
|
-
});
|
|
245
236
|
// Establish multiple connections
|
|
246
237
|
connectionCallback(mockWebSocket1, {
|
|
247
238
|
headers: { origin: 'https://app.hubspot.com' },
|
|
@@ -255,6 +246,8 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
255
246
|
data: {
|
|
256
247
|
projectName: 'test-project',
|
|
257
248
|
projectId: 123,
|
|
249
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
250
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
258
251
|
targetProjectAccountId: 456,
|
|
259
252
|
targetTestingAccountId: 789,
|
|
260
253
|
},
|
|
@@ -264,6 +257,8 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
264
257
|
data: {
|
|
265
258
|
projectName: 'test-project',
|
|
266
259
|
projectId: 123,
|
|
260
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
261
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
267
262
|
targetProjectAccountId: 456,
|
|
268
263
|
targetTestingAccountId: 789,
|
|
269
264
|
},
|
|
@@ -284,11 +279,11 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
284
279
|
const closeCallbacks2 = mockWebSocket2.on.mock.calls
|
|
285
280
|
.filter(call => call[0] === 'close')
|
|
286
281
|
.map(call => call[1]);
|
|
287
|
-
expect(closeCallbacks1).toHaveLength(3); // projectNodes and
|
|
288
|
-
expect(closeCallbacks2).toHaveLength(3); // projectNodes and
|
|
282
|
+
expect(closeCallbacks1).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
|
|
283
|
+
expect(closeCallbacks2).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
|
|
289
284
|
// Simulate first connection closing (call all close callbacks)
|
|
290
285
|
closeCallbacks1.forEach(callback => callback());
|
|
291
|
-
// Should have removed listeners for first connection (
|
|
286
|
+
// Should have removed listeners for first connection (3 listeners: projectNodes, appData, and uploadWarnings)
|
|
292
287
|
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
|
|
293
288
|
// Simulate second connection closing
|
|
294
289
|
closeCallbacks2.forEach(callback => callback());
|
|
@@ -126,9 +126,9 @@ describe('lib/projects/deploy', () => {
|
|
|
126
126
|
data: mockDeployResponseData,
|
|
127
127
|
});
|
|
128
128
|
mockPollDeployStatus.mockResolvedValue(mockDeployResult);
|
|
129
|
-
const
|
|
129
|
+
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
130
130
|
expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
|
|
131
|
-
expect(
|
|
131
|
+
expect(deploy).toEqual(mockDeployResult);
|
|
132
132
|
});
|
|
133
133
|
it('handles blocked deploy with warnings', async () => {
|
|
134
134
|
const mockBlockedResponse = {
|
|
@@ -150,15 +150,80 @@ describe('lib/projects/deploy', () => {
|
|
|
150
150
|
mockDeployProject.mockResolvedValue({
|
|
151
151
|
data: mockBlockedResponse,
|
|
152
152
|
});
|
|
153
|
-
|
|
153
|
+
await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
154
154
|
expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
|
|
155
|
-
|
|
155
|
+
});
|
|
156
|
+
it('handles blocked deploy with errors (cannot be forced)', async () => {
|
|
157
|
+
const mockBlockedResponse = {
|
|
158
|
+
buildResultType: 'DEPLOY_BLOCKED',
|
|
159
|
+
issues: [
|
|
160
|
+
{
|
|
161
|
+
uid: 'component-1',
|
|
162
|
+
componentTypeName: 'module',
|
|
163
|
+
errorMessages: [],
|
|
164
|
+
blockingMessages: [
|
|
165
|
+
{
|
|
166
|
+
message: 'This is an error',
|
|
167
|
+
isWarning: false,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
mockDeployProject.mockResolvedValue({
|
|
174
|
+
data: mockBlockedResponse,
|
|
175
|
+
});
|
|
176
|
+
await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
177
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deployBlockedHeader);
|
|
178
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentWarning('component-1', 'module', 'This is an error'));
|
|
179
|
+
});
|
|
180
|
+
it('handles blocked deploy with no blocking messages', async () => {
|
|
181
|
+
const mockBlockedResponse = {
|
|
182
|
+
buildResultType: 'DEPLOY_BLOCKED',
|
|
183
|
+
issues: [
|
|
184
|
+
{
|
|
185
|
+
uid: 'component-1',
|
|
186
|
+
componentTypeName: 'module',
|
|
187
|
+
errorMessages: [],
|
|
188
|
+
blockingMessages: [],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
mockDeployProject.mockResolvedValue({
|
|
193
|
+
data: mockBlockedResponse,
|
|
194
|
+
});
|
|
195
|
+
await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
196
|
+
expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
|
|
197
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentGeneric('component-1', 'module'));
|
|
156
198
|
});
|
|
157
199
|
it('handles general deploy failure', async () => {
|
|
158
200
|
mockDeployProject.mockResolvedValue({ data: null });
|
|
159
|
-
const
|
|
201
|
+
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
202
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
203
|
+
expect(deploy).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
it('handles undefined deploy response', async () => {
|
|
206
|
+
mockDeployProject.mockResolvedValue({ data: undefined });
|
|
207
|
+
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
160
208
|
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
161
|
-
expect(
|
|
209
|
+
expect(deploy).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
it('passes correct parameters to deployProject', async () => {
|
|
212
|
+
const mockDeployResponseData = {
|
|
213
|
+
id: 'deploy-123',
|
|
214
|
+
buildResultType: 'DEPLOY_QUEUED',
|
|
215
|
+
links: {
|
|
216
|
+
status: 'http://status-url',
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
mockDeployProject.mockResolvedValue({
|
|
220
|
+
data: mockDeployResponseData,
|
|
221
|
+
});
|
|
222
|
+
mockPollDeployStatus.mockResolvedValue({});
|
|
223
|
+
await handleProjectDeploy(targetAccountId, projectName, buildId, false, true);
|
|
224
|
+
expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, false, // useV3Api
|
|
225
|
+
true // force
|
|
226
|
+
);
|
|
162
227
|
});
|
|
163
228
|
});
|
|
164
229
|
});
|