@hubspot/cli 7.7.22-experimental.0 → 7.7.24-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/commands/account/auth.js +15 -4
- package/commands/auth.js +1 -1
- package/commands/config/set.d.ts +1 -0
- package/commands/config/set.js +19 -9
- package/commands/mcp/start.d.ts +1 -0
- package/commands/mcp/start.js +12 -4
- package/commands/project/create.js +2 -2
- package/commands/project/validate.js +1 -0
- package/commands/sandbox/__tests__/create.test.js +207 -0
- package/commands/sandbox/create.d.ts +1 -1
- package/commands/sandbox/create.js +31 -16
- package/commands/testAccount/createConfig.js +1 -1
- package/lang/en.d.ts +17 -4
- package/lang/en.js +22 -6
- package/lang/en.lyaml +4 -2
- package/lib/__tests__/buildAccount.test.js +62 -4
- package/lib/__tests__/yargsUtils.test.js +12 -1
- package/lib/buildAccount.d.ts +4 -1
- package/lib/buildAccount.js +57 -2
- package/lib/commonOpts.js +25 -0
- package/lib/configOptions.d.ts +5 -0
- package/lib/configOptions.js +11 -1
- package/lib/constants.d.ts +8 -0
- package/lib/constants.js +8 -0
- package/lib/errorHandlers/index.js +1 -3
- package/lib/errors/ProjectValidationError.d.ts +4 -0
- package/lib/errors/ProjectValidationError.js +9 -0
- package/lib/mcp/setup.d.ts +4 -0
- package/lib/mcp/setup.js +36 -0
- package/lib/projects/__tests__/LocalDevProcess.test.js +35 -0
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +170 -1
- package/lib/projects/add/legacyAddComponent.js +1 -1
- package/lib/projects/add/v3AddComponent.js +3 -2
- package/lib/projects/create/index.js +3 -3
- package/lib/projects/create/legacy.js +2 -2
- package/lib/projects/create/v3.d.ts +0 -2
- package/lib/projects/create/v3.js +1 -3
- package/lib/projects/localDev/LocalDevLogger.js +11 -2
- package/lib/projects/localDev/LocalDevProcess.d.ts +2 -0
- package/lib/projects/localDev/LocalDevProcess.js +15 -0
- package/lib/projects/localDev/LocalDevState.d.ts +1 -0
- package/lib/projects/localDev/LocalDevState.js +5 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -2
- package/lib/projects/localDev/LocalDevWebsocketServer.js +40 -30
- package/lib/projects/upload.js +5 -12
- package/lib/projects/urls.d.ts +1 -1
- package/lib/projects/urls.js +2 -2
- package/lib/sandboxes.d.ts +4 -0
- package/lib/sandboxes.js +4 -0
- package/lib/ui/index.d.ts +6 -0
- package/lib/ui/index.js +3 -5
- package/lib/yargsUtils.d.ts +1 -0
- package/lib/yargsUtils.js +6 -0
- package/mcp-server/tools/index.js +6 -4
- package/mcp-server/tools/project/{AddFeatureToProject.d.ts → AddFeatureToProjectTool.d.ts} +4 -4
- package/mcp-server/tools/project/{AddFeatureToProject.js → AddFeatureToProjectTool.js} +6 -14
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +4 -14
- package/mcp-server/tools/project/{DeployProject.d.ts → DeployProjectTool.d.ts} +1 -1
- package/mcp-server/tools/project/{DeployProject.js → DeployProjectTool.js} +2 -2
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +20 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +51 -0
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.js → AddFeatureToProjectTool.test.js} +7 -7
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +3 -4
- package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js} +4 -4
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +198 -0
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +1 -0
- package/mcp-server/tools/project/constants.js +11 -0
- package/mcp-server/utils/__tests__/command.test.js +76 -3
- package/mcp-server/utils/command.d.ts +6 -0
- package/mcp-server/utils/command.js +19 -0
- package/package.json +3 -3
- package/mcp-server/utils/__tests__/project.test.js +0 -79
- package/mcp-server/utils/project.d.ts +0 -5
- package/mcp-server/utils/project.js +0 -14
- /package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.d.ts → AddFeatureToProjectTool.test.d.ts} +0 -0
- /package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts} +0 -0
- /package/mcp-server/{utils/__tests__/project.test.d.ts → tools/project/__tests__/GetConfigValuesTool.test.d.ts} +0 -0
package/lang/en.js
CHANGED
|
@@ -4,8 +4,7 @@ import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constant
|
|
|
4
4
|
import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
|
|
5
5
|
import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
|
|
6
6
|
import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, } from '../lib/projects/urls.js';
|
|
7
|
-
import { PROJECT_CONFIG_FILE } from '../lib/constants.js';
|
|
8
|
-
import { PROJECT_WITH_APP } from '../lib/projects/create/v3.js';
|
|
7
|
+
import { PROJECT_CONFIG_FILE, PROJECT_WITH_APP } from '../lib/constants.js';
|
|
9
8
|
export const commands = {
|
|
10
9
|
generalErrors: {
|
|
11
10
|
srcIsProject: (src, command) => `"${src}" is in a project folder. Did you mean "hs project ${command}"?`,
|
|
@@ -80,6 +79,7 @@ export const commands = {
|
|
|
80
79
|
personalAccessKey: 'Enter existing personal access key',
|
|
81
80
|
},
|
|
82
81
|
errors: {
|
|
82
|
+
invalidAccountIdProvided: `--account must be a number.`,
|
|
83
83
|
failedToUpdateConfig: 'Failed to update the configuration file. Please try again.',
|
|
84
84
|
migrationNotConfirmed: `Did not migrate your configuration file. Run ${uiCommandReference('hs auth')} to update your existing config, or use ${uiCommandReference('hs config migrate')} to switch to the new global configuration.`,
|
|
85
85
|
mergeNotConfirmed: `Did not merge configuration files. When you are ready to merge the deprecated config file with the global config file, run ${uiCommandReference('hs config migrate')}.`,
|
|
@@ -253,6 +253,9 @@ export const commands = {
|
|
|
253
253
|
allowAutoUpdates: {
|
|
254
254
|
describe: 'Enable or disable auto updates',
|
|
255
255
|
},
|
|
256
|
+
autoOpenBrowser: {
|
|
257
|
+
describe: 'Enable or disable automatic opening of the browser',
|
|
258
|
+
},
|
|
256
259
|
},
|
|
257
260
|
},
|
|
258
261
|
},
|
|
@@ -828,9 +831,9 @@ export const commands = {
|
|
|
828
831
|
setup: {
|
|
829
832
|
installingDocSearch: 'Adding the docs-search mcp server',
|
|
830
833
|
claudeCode: 'Claude Code',
|
|
831
|
-
claudeDesktop: 'Claude Desktop',
|
|
832
834
|
cursor: 'Cursor',
|
|
833
835
|
windsurf: 'Windsurf',
|
|
836
|
+
vsCode: 'VSCode',
|
|
834
837
|
args: {
|
|
835
838
|
client: 'Target applications to configure',
|
|
836
839
|
docsSearch: 'Should the docs search mcp server be installed',
|
|
@@ -842,20 +845,26 @@ export const commands = {
|
|
|
842
845
|
},
|
|
843
846
|
spinners: {
|
|
844
847
|
failedToConfigure: 'Failed to configure the HubSpot mcp server.',
|
|
845
|
-
|
|
846
|
-
configuredClaudeDesktop: 'Configured Claude Desktop',
|
|
848
|
+
// Claude
|
|
847
849
|
configuringClaudeCode: 'Configuring Claude Code...',
|
|
848
850
|
configuredClaudeCode: 'Configured Claude Code',
|
|
849
851
|
claudeCodeNotFound: 'Claude Code not found - skipping configuration',
|
|
850
852
|
claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
|
|
851
853
|
failedToConfigureClaudeDesktop: 'Failed to configure Claude Desktop',
|
|
854
|
+
// Cursor
|
|
852
855
|
configuringCursor: 'Configuring Cursor...',
|
|
853
856
|
failedToConfigureCursor: 'Failed to configure Cursor',
|
|
854
857
|
configuredCursor: 'Configured Cursor',
|
|
855
858
|
alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
|
|
859
|
+
// Windsurf
|
|
856
860
|
configuringWindsurf: 'Configuring Windsurf...',
|
|
857
861
|
failedToConfigureWindsurf: 'Failed to configure Windsurf',
|
|
858
862
|
configuredWindsurf: 'Configured Windsurf',
|
|
863
|
+
// VS Code
|
|
864
|
+
configuringVsCode: 'Configuring VSCode...',
|
|
865
|
+
failedToConfigureVsCode: 'Failed to configure VSCode',
|
|
866
|
+
configuredVsCode: 'Configured VSCode',
|
|
867
|
+
vsCodeNotFound: 'VSCode not found - skipping configuration',
|
|
859
868
|
},
|
|
860
869
|
prompts: {
|
|
861
870
|
targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
|
|
@@ -1375,6 +1384,7 @@ export const commands = {
|
|
|
1375
1384
|
default: 'Validate the project before uploading',
|
|
1376
1385
|
},
|
|
1377
1386
|
success: (projectName) => `Project ${projectName} is valid and ready to upload`,
|
|
1387
|
+
failure: (projectName) => `Project ${projectName} is invalid`,
|
|
1378
1388
|
options: {
|
|
1379
1389
|
profile: {
|
|
1380
1390
|
describe: 'The profile to target for this validation',
|
|
@@ -2451,7 +2461,8 @@ export const lib = {
|
|
|
2451
2461
|
running: (projectName, accountIdentifier) => chalk.hex(UI_COLORS.SORBET)(`Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`),
|
|
2452
2462
|
quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`,
|
|
2453
2463
|
viewProjectLink: (name, accountId) => uiLink('View project in HubSpot', getProjectDetailUrl(name, accountId) || ''),
|
|
2454
|
-
viewLocalDevUILink: (accountId) => uiLink('View local dev session in HubSpot', getLocalDevUiUrl(accountId)),
|
|
2464
|
+
viewLocalDevUILink: (accountId, showWelcomeScreen) => uiLink('View local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen)),
|
|
2465
|
+
localDevUIAutoMessage: (accountId, showWelcomeScreen) => `Opening your ${uiLink('local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen))}...`,
|
|
2455
2466
|
viewTestAccountLink: 'View developer test account in HubSpot',
|
|
2456
2467
|
exitingStart: 'Stopping local dev server ...',
|
|
2457
2468
|
exitingSucceed: 'Successfully exited',
|
|
@@ -2780,6 +2791,11 @@ export const lib = {
|
|
|
2780
2791
|
promptMessage: 'Enter http timeout duration',
|
|
2781
2792
|
success: (timeout) => `HTTP timeout set to: ${timeout}`,
|
|
2782
2793
|
},
|
|
2794
|
+
setAutoOpenBrowser: {
|
|
2795
|
+
fieldName: 'auto open browser',
|
|
2796
|
+
enabled: 'Auto opening your browser has been enabled',
|
|
2797
|
+
disabled: 'Auto opening your browser has been disabled',
|
|
2798
|
+
},
|
|
2783
2799
|
},
|
|
2784
2800
|
commonOpts: {
|
|
2785
2801
|
options: {
|
package/lang/en.lyaml
CHANGED
|
@@ -1038,12 +1038,14 @@ en:
|
|
|
1038
1038
|
compressing: "Compressing build files to \"{{ path }}\""
|
|
1039
1039
|
fileFiltered: "Ignore rule triggered for \"{{ filename }}\""
|
|
1040
1040
|
ui:
|
|
1041
|
-
betaTag: "
|
|
1041
|
+
betaTag: "[BETA]"
|
|
1042
|
+
betaTagWithStyle: "{{#bold}}[BETA]{{/bold}}"
|
|
1042
1043
|
betaWarning:
|
|
1043
1044
|
header: "{{#yellow}}***************************** WARNING ****************************{{/yellow}}"
|
|
1044
1045
|
footer: "{{#yellow}}******************************************************************{{/yellow}}"
|
|
1045
1046
|
infoTag: "{{#bold}}[INFO]{{/bold}}"
|
|
1046
|
-
deprecatedTag: "
|
|
1047
|
+
deprecatedTag: "[DEPRECATED]"
|
|
1048
|
+
deprecatedTagWithStyle: "{{#bold}}[DEPRECATED]{{/bold}}"
|
|
1047
1049
|
errorTag: "{{#bold}}[ERROR]{{/bold}}"
|
|
1048
1050
|
deprecatedMessage: "The {{ command }} command is deprecated and will be disabled soon. {{ url }}"
|
|
1049
1051
|
deprecatedDescription: "{{ message }}. The {{ command }} command is deprecated and will be disabled soon. {{ url }}"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
2
2
|
import { accountNameExistsInConfig, updateAccountConfig, writeConfig, getAccountId, } from '@hubspot/local-dev-lib/config';
|
|
3
3
|
import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
|
|
4
|
-
import { createSandbox } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
4
|
+
import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
5
5
|
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
|
|
6
6
|
import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
|
|
7
7
|
import { cliAccountNamePrompt } from '../prompts/accountNamePrompt.js';
|
|
@@ -33,6 +33,8 @@ const mockedCreateDeveloperTestAccount = createDeveloperTestAccount;
|
|
|
33
33
|
const mockedFetchDeveloperTestAccountGateSyncStatus = fetchDeveloperTestAccountGateSyncStatus;
|
|
34
34
|
const mockedGenerateDeveloperTestAccountPersonalAccessKey = generateDeveloperTestAccountPersonalAccessKey;
|
|
35
35
|
const mockedCreateSandbox = createSandbox;
|
|
36
|
+
const mockedCreateV2Sandbox = createV2Sandbox;
|
|
37
|
+
const mockedGetPersonalAccessKey = getSandboxPersonalAccessKey;
|
|
36
38
|
describe('lib/buildAccount', () => {
|
|
37
39
|
describe('saveAccountToConfig()', () => {
|
|
38
40
|
const mockAccountConfig = {
|
|
@@ -166,16 +168,17 @@ describe('lib/buildAccount', () => {
|
|
|
166
168
|
});
|
|
167
169
|
describe('buildSandbox()', () => {
|
|
168
170
|
const mockParentAccountConfig = {
|
|
169
|
-
name: '
|
|
171
|
+
name: 'Prod account',
|
|
170
172
|
accountId: 123456,
|
|
171
|
-
accountType: HUBSPOT_ACCOUNT_TYPES.
|
|
173
|
+
accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
|
|
172
174
|
env: 'prod',
|
|
173
175
|
};
|
|
174
176
|
const mockSandbox = {
|
|
175
177
|
sandboxHubId: 56789,
|
|
176
178
|
parentHubId: 123456,
|
|
177
179
|
createdAt: '2025-01-01',
|
|
178
|
-
type: '
|
|
180
|
+
type: 'STANDARD',
|
|
181
|
+
version: 'V1',
|
|
179
182
|
archived: false,
|
|
180
183
|
name: 'Test Sandbox',
|
|
181
184
|
domain: 'test-sandbox.hubspot.com',
|
|
@@ -221,4 +224,59 @@ describe('lib/buildAccount', () => {
|
|
|
221
224
|
await expect(buildAccount.buildSandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, mockParentAccountConfig.env, false)).rejects.toThrow();
|
|
222
225
|
});
|
|
223
226
|
});
|
|
227
|
+
describe('buildV2Sandbox()', () => {
|
|
228
|
+
const mockParentAccountConfig = {
|
|
229
|
+
name: 'Prod account',
|
|
230
|
+
accountId: 123456,
|
|
231
|
+
accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
|
|
232
|
+
env: 'prod',
|
|
233
|
+
};
|
|
234
|
+
const mockSandbox = {
|
|
235
|
+
sandboxHubId: 56789,
|
|
236
|
+
parentHubId: 123456,
|
|
237
|
+
createdAt: '2025-01-01',
|
|
238
|
+
type: 'STANDARD',
|
|
239
|
+
archived: false,
|
|
240
|
+
version: 'V2',
|
|
241
|
+
name: 'Test v2 Sandbox',
|
|
242
|
+
domain: 'test-v2-sandbox.hubspot.com',
|
|
243
|
+
createdByUser: {
|
|
244
|
+
id: 123456,
|
|
245
|
+
email: 'test@test.com',
|
|
246
|
+
firstName: 'Test',
|
|
247
|
+
lastName: 'User',
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
vi.spyOn(buildAccount, 'saveAccountToConfig').mockResolvedValue(mockParentAccountConfig.name);
|
|
252
|
+
mockedGetAccountId.mockReturnValue(mockParentAccountConfig.accountId);
|
|
253
|
+
mockedCreateV2Sandbox.mockResolvedValue({
|
|
254
|
+
data: mockSandbox,
|
|
255
|
+
});
|
|
256
|
+
mockedGetPersonalAccessKey.mockResolvedValue({
|
|
257
|
+
data: { personalAccessKey: { encodedOAuthRefreshToken: 'test-key' } },
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
vi.clearAllMocks();
|
|
262
|
+
});
|
|
263
|
+
it('should create a v2 standard sandbox successfully and fetch a personal access key', async () => {
|
|
264
|
+
const result = await buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false);
|
|
265
|
+
expect(result).toEqual({ sandbox: mockSandbox });
|
|
266
|
+
expect(mockedGetPersonalAccessKey).toHaveBeenCalledWith(mockParentAccountConfig.accountId, mockSandbox.sandboxHubId);
|
|
267
|
+
});
|
|
268
|
+
it('should create a development sandbox successfully and fetch a personal access key', async () => {
|
|
269
|
+
const result = await buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, false, mockParentAccountConfig.env, false);
|
|
270
|
+
expect(result).toEqual({ sandbox: mockSandbox });
|
|
271
|
+
expect(mockedGetPersonalAccessKey).toHaveBeenCalledWith(mockParentAccountConfig.accountId, mockSandbox.sandboxHubId);
|
|
272
|
+
});
|
|
273
|
+
it('should throw error if account ID is not found', async () => {
|
|
274
|
+
mockedGetAccountId.mockReturnValue(null);
|
|
275
|
+
await expect(buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false)).rejects.toThrow();
|
|
276
|
+
});
|
|
277
|
+
it('should handle API errors when creating sandbox', async () => {
|
|
278
|
+
mockedCreateV2Sandbox.mockRejectedValue(new Error('test-error'));
|
|
279
|
+
await expect(buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false)).rejects.toThrow();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
224
282
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasFlag, makeYargsBuilder } from '../yargsUtils.js';
|
|
1
|
+
import { hasFlag, makeYargsBuilder, getExclusiveConflicts, } from '../yargsUtils.js';
|
|
2
2
|
import * as commonOpts from '../commonOpts.js';
|
|
3
3
|
vi.mock('../commonOpts');
|
|
4
4
|
const argvWithFlag = ['hs', 'command', '--test'];
|
|
@@ -36,4 +36,15 @@ describe('lib/yargsUtils', () => {
|
|
|
36
36
|
expect(commonOpts.addCustomHelpOutput).toHaveBeenCalled();
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
describe('getExclusiveConflicts()', () => {
|
|
40
|
+
it('should return an object where each option conflicts with all others', () => {
|
|
41
|
+
const options = ['option1', 'option2', 'option3'];
|
|
42
|
+
const result = getExclusiveConflicts(options);
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
option1: ['option2', 'option3'],
|
|
45
|
+
option2: ['option1', 'option3'],
|
|
46
|
+
option3: ['option1', 'option2'],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
39
50
|
});
|
package/lib/buildAccount.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DeveloperTestAccountConfig } from '@hubspot/local-dev-lib/types/developerTestAccounts';
|
|
2
2
|
import { Environment } from '@hubspot/local-dev-lib/types/Config';
|
|
3
3
|
import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts';
|
|
4
|
-
import { SandboxResponse } from '@hubspot/local-dev-lib/types/Sandbox';
|
|
4
|
+
import { SandboxResponse, V2Sandbox } from '@hubspot/local-dev-lib/types/Sandbox';
|
|
5
5
|
import { SandboxAccountType } from '../types/Sandboxes.js';
|
|
6
6
|
export declare function saveAccountToConfig(accountId: number | undefined, accountName: string, env: Environment, personalAccessKey?: string, force?: boolean): Promise<string>;
|
|
7
7
|
export declare function createDeveloperTestAccountV3(parentAccountId: number, testAccountConfig: DeveloperTestAccountConfig): Promise<{
|
|
@@ -14,4 +14,7 @@ type SandboxAccount = SandboxResponse & {
|
|
|
14
14
|
name: string;
|
|
15
15
|
};
|
|
16
16
|
export declare function buildSandbox(sandboxName: string, parentAccountConfig: CLIAccount, sandboxType: SandboxAccountType, env: Environment, force?: boolean): Promise<SandboxAccount>;
|
|
17
|
+
export declare function buildV2Sandbox(sandboxName: string, parentAccountConfig: CLIAccount, sandboxType: SandboxAccountType, syncObjectRecords: boolean, env: Environment, force?: boolean): Promise<{
|
|
18
|
+
sandbox: V2Sandbox;
|
|
19
|
+
}>;
|
|
17
20
|
export {};
|
package/lib/buildAccount.js
CHANGED
|
@@ -4,14 +4,14 @@ import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountId
|
|
|
4
4
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
5
5
|
import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
|
|
6
6
|
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
|
|
7
|
-
import { createSandbox } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
7
|
+
import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
8
8
|
import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
|
|
9
9
|
import { createDeveloperTestAccountConfigPrompt } from './prompts/createDeveloperTestAccountConfigPrompt.js';
|
|
10
10
|
import { i18n } from './lang.js';
|
|
11
11
|
import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
|
|
12
12
|
import SpinniesManager from './ui/SpinniesManager.js';
|
|
13
13
|
import { debugError, logError } from './errorHandlers/index.js';
|
|
14
|
-
import { SANDBOX_API_TYPE_MAP, handleSandboxCreateError } from './sandboxes.js';
|
|
14
|
+
import { SANDBOX_API_TYPE_MAP, SANDBOX_TYPE_MAP_V2, handleSandboxCreateError, } from './sandboxes.js';
|
|
15
15
|
import { handleDeveloperTestAccountCreateError } from './developerTestAccounts.js';
|
|
16
16
|
import { lib } from '../lang/en.js';
|
|
17
17
|
import { poll } from './polling.js';
|
|
@@ -199,3 +199,58 @@ export async function buildSandbox(sandboxName, parentAccountConfig, sandboxType
|
|
|
199
199
|
}
|
|
200
200
|
return sandbox;
|
|
201
201
|
}
|
|
202
|
+
export async function buildV2Sandbox(sandboxName, parentAccountConfig, sandboxType, syncObjectRecords, env, force = false) {
|
|
203
|
+
let i18nKey;
|
|
204
|
+
if (sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX) {
|
|
205
|
+
i18nKey = 'lib.sandbox.create.loading.standard';
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
i18nKey = 'lib.sandbox.create.loading.developer';
|
|
209
|
+
}
|
|
210
|
+
const id = getAccountIdentifier(parentAccountConfig);
|
|
211
|
+
const parentAccountId = getAccountId(id);
|
|
212
|
+
if (!parentAccountId) {
|
|
213
|
+
throw new Error(i18n(`${i18nKey}.fail`));
|
|
214
|
+
}
|
|
215
|
+
SpinniesManager.init({
|
|
216
|
+
succeedColor: 'white',
|
|
217
|
+
});
|
|
218
|
+
logger.log('');
|
|
219
|
+
SpinniesManager.add('buildV2Sandbox', {
|
|
220
|
+
text: i18n(`${i18nKey}.add`, {
|
|
221
|
+
accountName: sandboxName,
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
let sandbox;
|
|
225
|
+
let pak;
|
|
226
|
+
try {
|
|
227
|
+
const sandboxTypeV2 = SANDBOX_TYPE_MAP_V2[sandboxType];
|
|
228
|
+
const { data } = await createV2Sandbox(parentAccountId, sandboxName, sandboxTypeV2, syncObjectRecords);
|
|
229
|
+
sandbox = { ...data };
|
|
230
|
+
const { data: { personalAccessKey }, } = await getSandboxPersonalAccessKey(parentAccountId, sandbox.sandboxHubId);
|
|
231
|
+
pak = personalAccessKey.encodedOAuthRefreshToken;
|
|
232
|
+
SpinniesManager.succeed('buildV2Sandbox', {
|
|
233
|
+
text: i18n(`${i18nKey}.succeed`, {
|
|
234
|
+
accountName: sandboxName,
|
|
235
|
+
accountId: sandbox.sandboxHubId,
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
debugError(e);
|
|
241
|
+
SpinniesManager.fail('buildV2Sandbox', {
|
|
242
|
+
text: i18n(`${i18nKey}.fail`, {
|
|
243
|
+
accountName: sandboxName,
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
handleSandboxCreateError(e, env, sandboxName, parentAccountId);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await saveAccountToConfig(sandbox.sandboxHubId, sandboxName, env, pak, force);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
logError(err);
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
return { sandbox };
|
|
256
|
+
}
|
package/lib/commonOpts.js
CHANGED
|
@@ -7,6 +7,7 @@ import { debugError } from './errorHandlers/index.js';
|
|
|
7
7
|
import { EXIT_CODES } from './enums/exitCodes.js';
|
|
8
8
|
import { uiCommandReference } from './ui/index.js';
|
|
9
9
|
import { i18n } from './lang.js';
|
|
10
|
+
import { getTerminalUISupport, UI_COLORS } from './ui/index.js';
|
|
10
11
|
export function addGlobalOptions(yargs) {
|
|
11
12
|
yargs.version(false);
|
|
12
13
|
yargs.option('debug', {
|
|
@@ -76,8 +77,32 @@ export function addJSONOutputOptions(yargs) {
|
|
|
76
77
|
hidden: true,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
80
|
+
// Remove this once we've upgraded to yargs 18.0.0
|
|
81
|
+
function uiBetaTagWithColor(message) {
|
|
82
|
+
const terminalUISupport = getTerminalUISupport();
|
|
83
|
+
const tag = i18n(`lib.ui.betaTagWithStyle`);
|
|
84
|
+
const result = `${terminalUISupport.color ? chalk.hex(UI_COLORS.SORBET)(tag) : tag} ${message}`;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
// Remove this once we've upgraded to yargs 18.0.0
|
|
88
|
+
function uiDeprecatedTagWithColor(message) {
|
|
89
|
+
const terminalUISupport = getTerminalUISupport();
|
|
90
|
+
const tag = i18n(`lib.ui.deprecatedTagWithStyle`);
|
|
91
|
+
const result = `${terminalUISupport.color ? chalk.yellow(tag) : tag} ${message}`;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
79
94
|
export async function addCustomHelpOutput(yargs, command, describe) {
|
|
80
95
|
try {
|
|
96
|
+
// Remove this once we've upgraded to yargs 18.0.0
|
|
97
|
+
if (describe && describe.includes(i18n(`lib.ui.betaTag`))) {
|
|
98
|
+
describe = describe.replace(i18n(`lib.ui.betaTag`) + ' ', '');
|
|
99
|
+
describe = uiBetaTagWithColor(describe);
|
|
100
|
+
}
|
|
101
|
+
// Remove this once we've upgraded to yargs 18.0.0
|
|
102
|
+
if (describe && describe.includes(i18n(`lib.ui.deprecatedTag`))) {
|
|
103
|
+
describe = describe.replace(i18n(`lib.ui.deprecatedTag`) + ' ', '');
|
|
104
|
+
describe = uiDeprecatedTagWithColor(describe);
|
|
105
|
+
}
|
|
81
106
|
const parsedArgv = yargsParser(process.argv.slice(2));
|
|
82
107
|
if (parsedArgv && parsedArgv.help) {
|
|
83
108
|
const commandBase = `hs ${parsedArgv._.slice(0, -1).join(' ')}`;
|
package/lib/configOptions.d.ts
CHANGED
|
@@ -15,3 +15,8 @@ export declare function setHttpTimeout({ accountId, httpTimeout, }: {
|
|
|
15
15
|
accountId: number;
|
|
16
16
|
httpTimeout?: string;
|
|
17
17
|
}): Promise<void>;
|
|
18
|
+
export declare function setAutoOpenBrowser({ accountId, autoOpenBrowser, }: {
|
|
19
|
+
accountId: number;
|
|
20
|
+
autoOpenBrowser: boolean;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export declare function isAutoOpenBrowserEnabled(): boolean;
|
package/lib/configOptions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { updateAllowUsageTracking, updateAllowAutoUpdates, updateDefaultCmsPublishMode, updateHttpTimeout, } from '@hubspot/local-dev-lib/config';
|
|
1
|
+
import { updateAllowUsageTracking, updateAllowAutoUpdates, updateDefaultCmsPublishMode, updateHttpTimeout, isConfigFlagEnabled, updateAutoOpenBrowser, } from '@hubspot/local-dev-lib/config';
|
|
2
2
|
import { CMS_PUBLISH_MODE } from '@hubspot/local-dev-lib/constants/files';
|
|
3
3
|
import { commaSeparatedValues } from '@hubspot/local-dev-lib/text';
|
|
4
4
|
import { trackCommandUsage } from './usageTracking.js';
|
|
@@ -95,3 +95,13 @@ export async function setHttpTimeout({ accountId, httpTimeout, }) {
|
|
|
95
95
|
updateHttpTimeout(newHttpTimeout);
|
|
96
96
|
uiLogger.success(lib.configOptions.setHttpTimeout.success(newHttpTimeout));
|
|
97
97
|
}
|
|
98
|
+
export async function setAutoOpenBrowser({ accountId, autoOpenBrowser, }) {
|
|
99
|
+
trackCommandUsage('config-set-auto-open-browser', undefined, accountId);
|
|
100
|
+
updateAutoOpenBrowser(autoOpenBrowser);
|
|
101
|
+
uiLogger.success(autoOpenBrowser
|
|
102
|
+
? lib.configOptions.setAutoOpenBrowser.enabled
|
|
103
|
+
: lib.configOptions.setAutoOpenBrowser.disabled);
|
|
104
|
+
}
|
|
105
|
+
export function isAutoOpenBrowserEnabled() {
|
|
106
|
+
return isConfigFlagEnabled('autoOpenBrowser', true);
|
|
107
|
+
}
|
package/lib/constants.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export declare const APP_AUTH_TYPES: {
|
|
|
78
78
|
export declare const FEATURES: {
|
|
79
79
|
readonly UNIFIED_THEME_PREVIEW: "cms:react:unifiedThemePreview";
|
|
80
80
|
readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
|
|
81
|
+
readonly SANDBOXES_V2: "sandboxes:v2:enabled";
|
|
82
|
+
readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
|
|
81
83
|
};
|
|
82
84
|
export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
83
85
|
UPLOAD_SUCCESS: string;
|
|
@@ -88,6 +90,7 @@ export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
|
88
90
|
};
|
|
89
91
|
export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
|
|
90
92
|
UPLOAD: string;
|
|
93
|
+
VIEWED_WELCOME_SCREEN: string;
|
|
91
94
|
};
|
|
92
95
|
export declare const APP_INSTALLATION_STATES: {
|
|
93
96
|
readonly NOT_INSTALLED: "NOT_INSTALLED";
|
|
@@ -107,3 +110,8 @@ export declare const LOCAL_DEV_SERVER_MESSAGE_TYPES: {
|
|
|
107
110
|
readonly INITIAL: "INITIAL";
|
|
108
111
|
readonly WEBSOCKET_SERVER_CONNECTED: "WEBSOCKET_SERVER_CONNECTED";
|
|
109
112
|
};
|
|
113
|
+
export declare const CONFIG_LOCAL_STATE_FLAGS: {
|
|
114
|
+
readonly LOCAL_DEV_UI_WELCOME: "LOCAL_DEV_UI_WELCOME";
|
|
115
|
+
};
|
|
116
|
+
export declare const EMPTY_PROJECT = "empty";
|
|
117
|
+
export declare const PROJECT_WITH_APP = "app";
|
package/lib/constants.js
CHANGED
|
@@ -70,6 +70,8 @@ export const APP_AUTH_TYPES = {
|
|
|
70
70
|
export const FEATURES = {
|
|
71
71
|
UNIFIED_THEME_PREVIEW: 'cms:react:unifiedThemePreview',
|
|
72
72
|
UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
|
|
73
|
+
SANDBOXES_V2: 'sandboxes:v2:enabled',
|
|
74
|
+
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
73
75
|
};
|
|
74
76
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
75
77
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
|
@@ -80,6 +82,7 @@ export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
|
80
82
|
};
|
|
81
83
|
export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
|
|
82
84
|
UPLOAD: 'client:upload',
|
|
85
|
+
VIEWED_WELCOME_SCREEN: 'client:viewedWelcomeScreen',
|
|
83
86
|
};
|
|
84
87
|
export const APP_INSTALLATION_STATES = {
|
|
85
88
|
NOT_INSTALLED: 'NOT_INSTALLED',
|
|
@@ -99,3 +102,8 @@ export const LOCAL_DEV_SERVER_MESSAGE_TYPES = {
|
|
|
99
102
|
INITIAL: 'INITIAL',
|
|
100
103
|
WEBSOCKET_SERVER_CONNECTED: 'WEBSOCKET_SERVER_CONNECTED',
|
|
101
104
|
};
|
|
105
|
+
export const CONFIG_LOCAL_STATE_FLAGS = {
|
|
106
|
+
LOCAL_DEV_UI_WELCOME: 'LOCAL_DEV_UI_WELCOME',
|
|
107
|
+
};
|
|
108
|
+
export const EMPTY_PROJECT = 'empty';
|
|
109
|
+
export const PROJECT_WITH_APP = 'app';
|
|
@@ -5,6 +5,7 @@ import { shouldSuppressError } from './suppressError.js';
|
|
|
5
5
|
import { i18n } from '../lang.js';
|
|
6
6
|
import util from 'util';
|
|
7
7
|
import { uiCommandReference } from '../ui/index.js';
|
|
8
|
+
import { isProjectValidationError } from '../errors/ProjectValidationError.js';
|
|
8
9
|
export function logError(error, context) {
|
|
9
10
|
debugError(error, context);
|
|
10
11
|
if (isProjectValidationError(error)) {
|
|
@@ -84,9 +85,6 @@ export class ApiErrorContext {
|
|
|
84
85
|
this.projectName = props.projectName || '';
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
function isProjectValidationError(error) {
|
|
88
|
-
return error instanceof Error && error.name === 'ProjectValidationError';
|
|
89
|
-
}
|
|
90
88
|
function isErrorWithMessageOrReason(error) {
|
|
91
89
|
return (typeof error === 'object' &&
|
|
92
90
|
error !== null &&
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default class ProjectValidationError extends Error {
|
|
2
|
+
constructor(message, options) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = 'ProjectValidationError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isProjectValidationError(err) {
|
|
8
|
+
return err instanceof ProjectValidationError;
|
|
9
|
+
}
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export declare const supportedTools: ({
|
|
|
7
7
|
} | {
|
|
8
8
|
name: "Windsurf";
|
|
9
9
|
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
name: "VSCode";
|
|
12
|
+
value: string;
|
|
10
13
|
})[];
|
|
11
14
|
interface McpCommand {
|
|
12
15
|
command: string;
|
|
@@ -15,6 +18,7 @@ interface McpCommand {
|
|
|
15
18
|
export declare function addMintlifyMcpServer(installTargets: string[]): Promise<void>;
|
|
16
19
|
export declare function setupMintlify(derivedTargets?: string[]): Promise<boolean>;
|
|
17
20
|
export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
|
|
21
|
+
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
18
22
|
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
19
23
|
export declare function setupCursor(mcpCommand?: McpCommand): boolean;
|
|
20
24
|
export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
|
package/lib/mcp/setup.js
CHANGED
|
@@ -13,11 +13,13 @@ const mcpServerName = 'hubspot-cli-mcp';
|
|
|
13
13
|
const claudeCode = 'claude';
|
|
14
14
|
const windsurf = 'windsurf';
|
|
15
15
|
const cursor = 'cursor';
|
|
16
|
+
const vscode = 'vscode';
|
|
16
17
|
const supportedMintlifyClients = [windsurf, cursor];
|
|
17
18
|
export const supportedTools = [
|
|
18
19
|
{ name: commands.mcp.setup.claudeCode, value: claudeCode },
|
|
19
20
|
{ name: commands.mcp.setup.cursor, value: cursor },
|
|
20
21
|
{ name: commands.mcp.setup.windsurf, value: windsurf },
|
|
22
|
+
{ name: commands.mcp.setup.vsCode, value: vscode },
|
|
21
23
|
];
|
|
22
24
|
const defaultMcpCommand = {
|
|
23
25
|
command: 'hs',
|
|
@@ -75,6 +77,9 @@ export async function addMcpServerToConfig(targets) {
|
|
|
75
77
|
if (derivedTargets.includes(windsurf)) {
|
|
76
78
|
await runSetupFunction(setupWindsurf);
|
|
77
79
|
}
|
|
80
|
+
if (derivedTargets.includes(vscode)) {
|
|
81
|
+
await runSetupFunction(setupVsCode);
|
|
82
|
+
}
|
|
78
83
|
uiLogger.info(commands.mcp.setup.success(derivedTargets));
|
|
79
84
|
return derivedTargets;
|
|
80
85
|
}
|
|
@@ -150,6 +155,37 @@ function setupMcpConfigFile(config) {
|
|
|
150
155
|
return false;
|
|
151
156
|
}
|
|
152
157
|
}
|
|
158
|
+
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
159
|
+
try {
|
|
160
|
+
SpinniesManager.add('vsCode', {
|
|
161
|
+
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
162
|
+
});
|
|
163
|
+
const mcpConfig = JSON.stringify({
|
|
164
|
+
name: mcpServerName,
|
|
165
|
+
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
166
|
+
});
|
|
167
|
+
await execAsync(`code --add-mcp '${mcpConfig}'`);
|
|
168
|
+
SpinniesManager.succeed('vsCode', {
|
|
169
|
+
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error instanceof Error &&
|
|
175
|
+
error.message.includes('code: command not found')) {
|
|
176
|
+
SpinniesManager.fail('vsCode', {
|
|
177
|
+
text: commands.mcp.setup.spinners.vsCodeNotFound,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
SpinniesManager.fail('vsCode', {
|
|
182
|
+
text: commands.mcp.setup.spinners.failedToConfigureVsCode,
|
|
183
|
+
});
|
|
184
|
+
logError(error);
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
153
189
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
154
190
|
try {
|
|
155
191
|
SpinniesManager.add('claudeCode', {
|
|
@@ -16,6 +16,7 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => ({
|
|
|
16
16
|
cleanup: vi.fn().mockResolvedValue(undefined),
|
|
17
17
|
},
|
|
18
18
|
}));
|
|
19
|
+
vi.mock('open');
|
|
19
20
|
vi.mock('@hubspot/project-parsing-lib');
|
|
20
21
|
vi.mock('../upload');
|
|
21
22
|
vi.mock('../config');
|
|
@@ -306,4 +307,38 @@ describe('LocalDevProcess', () => {
|
|
|
306
307
|
expect(listener).toHaveBeenCalledWith(process.projectNodes);
|
|
307
308
|
});
|
|
308
309
|
});
|
|
310
|
+
describe('removeStateListener()', () => {
|
|
311
|
+
it('should remove state listener', () => {
|
|
312
|
+
const listener = vi.fn();
|
|
313
|
+
const key = 'projectNodes';
|
|
314
|
+
// Add the listener first
|
|
315
|
+
process.addStateListener(key, listener);
|
|
316
|
+
// Trigger state change to verify listener is called
|
|
317
|
+
// @ts-expect-error
|
|
318
|
+
process.state.projectNodes = {};
|
|
319
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
320
|
+
// Remove the listener
|
|
321
|
+
process.removeStateListener(key, listener);
|
|
322
|
+
// Trigger state change again to verify listener is no longer called
|
|
323
|
+
// @ts-expect-error
|
|
324
|
+
process.state.projectNodes = { newNode: { uid: 'newNode' } };
|
|
325
|
+
expect(listener).toHaveBeenCalledTimes(1); // Should still be 1, not 2
|
|
326
|
+
});
|
|
327
|
+
it('should not affect other listeners when removing one', () => {
|
|
328
|
+
const listener1 = vi.fn();
|
|
329
|
+
const listener2 = vi.fn();
|
|
330
|
+
const key = 'projectNodes';
|
|
331
|
+
// Add two listeners
|
|
332
|
+
process.addStateListener(key, listener1);
|
|
333
|
+
process.addStateListener(key, listener2);
|
|
334
|
+
// Remove only the first listener
|
|
335
|
+
process.removeStateListener(key, listener1);
|
|
336
|
+
// Trigger state change
|
|
337
|
+
// @ts-expect-error
|
|
338
|
+
process.state.projectNodes = {};
|
|
339
|
+
// Only listener2 should be called
|
|
340
|
+
expect(listener1).not.toHaveBeenCalled();
|
|
341
|
+
expect(listener2).toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
309
344
|
});
|