@hubspot/cli 7.7.29-experimental.0 → 7.7.31-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/app/__tests__/install.test.d.ts +1 -0
- package/commands/app/__tests__/install.test.js +47 -0
- package/commands/app/install.d.ts +8 -0
- package/commands/app/install.js +122 -0
- package/commands/app.js +6 -1
- package/commands/project/__tests__/add.test.js +66 -0
- package/commands/project/add.d.ts +1 -1
- package/commands/project/add.js +1 -1
- package/commands/project/create.js +1 -1
- package/commands/project/upload.d.ts +2 -2
- package/commands/project/upload.js +1 -1
- package/commands/testAccount/create.js +3 -0
- package/lang/en.d.ts +24 -2
- package/lang/en.js +25 -3
- package/lib/mcp/setup.d.ts +3 -3
- package/lib/mcp/setup.js +113 -38
- package/lib/projectProfiles.d.ts +1 -1
- package/lib/projectProfiles.js +10 -2
- package/lib/projects/__tests__/AppDevModeInterface.test.js +10 -0
- package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -7
- package/lib/projects/add/v3AddComponent.d.ts +1 -1
- package/lib/projects/add/v3AddComponent.js +6 -1
- package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -1
- package/lib/projects/localDev/AppDevModeInterface.js +33 -10
- package/lib/projects/structure.d.ts +2 -2
- package/lib/projects/upload.d.ts +2 -1
- package/lib/projects/upload.js +1 -0
- package/package.json +1 -1
- package/types/Yargs.d.ts +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { addAccountOptions, addConfigOptions, addUseEnvironmentOptions, addGlobalOptions, addJSONOutputOptions, } from '../../../lib/commonOpts.js';
|
|
3
|
+
import installCommand from '../install.js';
|
|
4
|
+
vi.mock('../../../lib/commonOpts');
|
|
5
|
+
describe('commands/app/install', () => {
|
|
6
|
+
const yargsMock = yargs;
|
|
7
|
+
describe('command', () => {
|
|
8
|
+
it('should have the correct command structure', () => {
|
|
9
|
+
expect(installCommand.command).toEqual('install <test-account-id>');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
describe('describe', () => {
|
|
13
|
+
it('should provide a description', () => {
|
|
14
|
+
expect(installCommand.describe).not.toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('builder', () => {
|
|
18
|
+
it('should support the correct options', () => {
|
|
19
|
+
installCommand.builder(yargsMock);
|
|
20
|
+
expect(addGlobalOptions).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(addGlobalOptions).toHaveBeenCalledWith(yargsMock);
|
|
22
|
+
expect(addAccountOptions).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(addAccountOptions).toHaveBeenCalledWith(yargsMock);
|
|
24
|
+
expect(addConfigOptions).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(addConfigOptions).toHaveBeenCalledWith(yargsMock);
|
|
26
|
+
expect(addUseEnvironmentOptions).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(addUseEnvironmentOptions).toHaveBeenCalledWith(yargsMock);
|
|
28
|
+
expect(addJSONOutputOptions).toHaveBeenCalledTimes(1);
|
|
29
|
+
expect(addJSONOutputOptions).toHaveBeenCalledWith(yargsMock);
|
|
30
|
+
expect(yargsMock.positional).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(yargsMock.positional).toHaveBeenCalledWith('test-account-id', expect.objectContaining({
|
|
32
|
+
type: 'number',
|
|
33
|
+
required: true,
|
|
34
|
+
describe: expect.any(String),
|
|
35
|
+
}));
|
|
36
|
+
expect(yargsMock.option).toHaveBeenCalledTimes(2);
|
|
37
|
+
expect(yargsMock.option).toHaveBeenCalledWith('app-uid', expect.objectContaining({
|
|
38
|
+
type: 'string',
|
|
39
|
+
describe: expect.any(String),
|
|
40
|
+
}));
|
|
41
|
+
expect(yargsMock.option).toHaveBeenCalledWith('project-name', expect.objectContaining({
|
|
42
|
+
type: 'string',
|
|
43
|
+
describe: expect.any(String),
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, YargsCommandModule, JSONOutputArgs } from '../../types/Yargs.js';
|
|
2
|
+
type InstallAppArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs & {
|
|
3
|
+
appUid?: string;
|
|
4
|
+
projectName?: string;
|
|
5
|
+
testAccountId: number;
|
|
6
|
+
};
|
|
7
|
+
declare const installAppCommand: YargsCommandModule<unknown, InstallAppArgs>;
|
|
8
|
+
export default installAppCommand;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { fetchDeveloperTestAccountOauthAppInstallStatus, installOauthAppIntoDeveloperTestAccount, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
|
|
2
|
+
import { trackCommandUsage } from '../../lib/usageTracking.js';
|
|
3
|
+
import { commands } from '../../lang/en.js';
|
|
4
|
+
import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
|
|
5
|
+
import { makeYargsBuilder } from '../../lib/yargsUtils.js';
|
|
6
|
+
import { APP_AUTH_TYPES } from '../../lib/constants.js';
|
|
7
|
+
import { uiLogger } from '../../lib/ui/logger.js';
|
|
8
|
+
import SpinniesManager from '../../lib/ui/SpinniesManager.js';
|
|
9
|
+
import { poll } from '../../lib/polling.js';
|
|
10
|
+
import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
|
|
11
|
+
import { handleTranslate } from '../../lib/projects/upload.js';
|
|
12
|
+
import { isAppIRNode } from '../../lib/projects/structure.js';
|
|
13
|
+
import { logError } from '../../lib/errorHandlers/index.js';
|
|
14
|
+
const command = 'install <test-account-id>';
|
|
15
|
+
const describe = undefined; // commands.app.subcommands.install.describe;
|
|
16
|
+
async function handler(args) {
|
|
17
|
+
const { derivedAccountId, appUid, projectName, testAccountId, formatOutputAsJson, } = args;
|
|
18
|
+
trackCommandUsage('app-install', {}, derivedAccountId);
|
|
19
|
+
const jsonOutput = {};
|
|
20
|
+
let targetProjectName = projectName;
|
|
21
|
+
let targetAppUid = appUid;
|
|
22
|
+
const { projectConfig, projectDir } = await getProjectConfig();
|
|
23
|
+
if (!targetProjectName) {
|
|
24
|
+
validateProjectConfig(projectConfig, projectDir);
|
|
25
|
+
targetProjectName = projectConfig?.name;
|
|
26
|
+
}
|
|
27
|
+
if (!targetProjectName) {
|
|
28
|
+
uiLogger.error(commands.app.subcommands.install.errors.mustSpecifyProjectName);
|
|
29
|
+
process.exit(EXIT_CODES.ERROR);
|
|
30
|
+
}
|
|
31
|
+
let isAppOauth = true;
|
|
32
|
+
if (!targetAppUid) {
|
|
33
|
+
const intermediateRepresentation = await handleTranslate(projectDir, projectConfig, derivedAccountId, true, undefined);
|
|
34
|
+
if (intermediateRepresentation) {
|
|
35
|
+
Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).forEach(node => {
|
|
36
|
+
if (isAppIRNode(node)) {
|
|
37
|
+
targetAppUid = node.uid;
|
|
38
|
+
isAppOauth = node.config.auth.type === APP_AUTH_TYPES.OAUTH;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!targetAppUid) {
|
|
44
|
+
uiLogger.error(commands.app.subcommands.install.errors.noAppUidFound);
|
|
45
|
+
process.exit(EXIT_CODES.ERROR);
|
|
46
|
+
}
|
|
47
|
+
if (!isAppOauth) {
|
|
48
|
+
uiLogger.error(commands.app.subcommands.install.errors.appMustBeOauth);
|
|
49
|
+
process.exit(EXIT_CODES.ERROR);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { data } = await installOauthAppIntoDeveloperTestAccount(derivedAccountId, testAccountId, targetProjectName, targetAppUid);
|
|
53
|
+
if (data?.authCodes.length > 0) {
|
|
54
|
+
jsonOutput.authCode = data.authCodes[0].authCode;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
logError(err);
|
|
59
|
+
process.exit(EXIT_CODES.ERROR);
|
|
60
|
+
}
|
|
61
|
+
SpinniesManager.init({
|
|
62
|
+
succeedColor: 'white',
|
|
63
|
+
});
|
|
64
|
+
SpinniesManager.add('installApp', {
|
|
65
|
+
text: commands.app.subcommands.install.polling.start,
|
|
66
|
+
});
|
|
67
|
+
let appInstallSucceeded = false;
|
|
68
|
+
try {
|
|
69
|
+
await poll(() => fetchDeveloperTestAccountOauthAppInstallStatus(derivedAccountId, targetProjectName, targetAppUid), {
|
|
70
|
+
successStates: ['SUCCESS'],
|
|
71
|
+
errorStates: [],
|
|
72
|
+
});
|
|
73
|
+
appInstallSucceeded = true;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
SpinniesManager.fail('installApp');
|
|
77
|
+
logError(err);
|
|
78
|
+
process.exit(EXIT_CODES.ERROR);
|
|
79
|
+
}
|
|
80
|
+
if (!appInstallSucceeded) {
|
|
81
|
+
SpinniesManager.fail('installApp');
|
|
82
|
+
process.exit(EXIT_CODES.ERROR);
|
|
83
|
+
}
|
|
84
|
+
SpinniesManager.succeed('installApp', {
|
|
85
|
+
text: commands.app.subcommands.install.polling.success,
|
|
86
|
+
});
|
|
87
|
+
if (formatOutputAsJson) {
|
|
88
|
+
uiLogger.json(jsonOutput);
|
|
89
|
+
}
|
|
90
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
91
|
+
}
|
|
92
|
+
function installAppBuilder(yargs) {
|
|
93
|
+
yargs.positional('test-account-id', {
|
|
94
|
+
describe: commands.app.subcommands.install.positionals.testAccountId,
|
|
95
|
+
required: true,
|
|
96
|
+
type: 'number',
|
|
97
|
+
});
|
|
98
|
+
yargs.option('app-uid', {
|
|
99
|
+
describe: commands.app.subcommands.install.options.appUid,
|
|
100
|
+
type: 'string',
|
|
101
|
+
});
|
|
102
|
+
yargs.option('project-name', {
|
|
103
|
+
describe: commands.app.subcommands.install.options.projectName,
|
|
104
|
+
type: 'string',
|
|
105
|
+
});
|
|
106
|
+
yargs.example('install 1234567890 --app-uid=my-app-uid --project-name=my-project', commands.app.subcommands.install.example);
|
|
107
|
+
return yargs;
|
|
108
|
+
}
|
|
109
|
+
const builder = makeYargsBuilder(installAppBuilder, command, commands.app.subcommands.install.describe, {
|
|
110
|
+
useGlobalOptions: true,
|
|
111
|
+
useAccountOptions: true,
|
|
112
|
+
useConfigOptions: true,
|
|
113
|
+
useEnvironmentOptions: true,
|
|
114
|
+
useJSONOutputOptions: true,
|
|
115
|
+
});
|
|
116
|
+
const installAppCommand = {
|
|
117
|
+
command,
|
|
118
|
+
describe,
|
|
119
|
+
handler,
|
|
120
|
+
builder,
|
|
121
|
+
};
|
|
122
|
+
export default installAppCommand;
|
package/commands/app.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import migrateCommand from './app/migrate.js';
|
|
2
2
|
import appSecretCommand from './app/secret.js';
|
|
3
|
+
import installAppCommand from './app/install.js';
|
|
3
4
|
import { makeYargsBuilder } from '../lib/yargsUtils.js';
|
|
4
5
|
const command = ['app', 'apps'];
|
|
5
6
|
// Keep the command hidden for now
|
|
6
7
|
const describe = undefined;
|
|
7
8
|
function appBuilder(yargs) {
|
|
8
|
-
yargs
|
|
9
|
+
yargs
|
|
10
|
+
.command(migrateCommand)
|
|
11
|
+
.command(appSecretCommand)
|
|
12
|
+
.command(installAppCommand)
|
|
13
|
+
.demandCommand(1, '');
|
|
9
14
|
return yargs;
|
|
10
15
|
}
|
|
11
16
|
const builder = makeYargsBuilder(appBuilder, command, describe);
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import yargs from 'yargs';
|
|
2
2
|
import projectAddCommand from '../add.js';
|
|
3
3
|
import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../../lib/constants.js';
|
|
4
|
+
import { v3AddComponent } from '../../../lib/projects/add/v3AddComponent.js';
|
|
5
|
+
import { legacyAddComponent } from '../../../lib/projects/add/legacyAddComponent.js';
|
|
6
|
+
import { getProjectConfig } from '../../../lib/projects/config.js';
|
|
7
|
+
import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
|
|
8
|
+
import { trackCommandUsage } from '../../../lib/usageTracking.js';
|
|
4
9
|
vi.mock('../../../lib/commonOpts');
|
|
10
|
+
vi.mock('../../../lib/projects/add/v3AddComponent');
|
|
11
|
+
vi.mock('../../../lib/projects/add/legacyAddComponent');
|
|
12
|
+
vi.mock('../../../lib/projects/config');
|
|
13
|
+
vi.mock('../../../lib/projects/buildAndDeploy');
|
|
14
|
+
vi.mock('../../../lib/usageTracking');
|
|
15
|
+
const mockedV3AddComponent = vi.mocked(v3AddComponent);
|
|
16
|
+
const mockedLegacyAddComponent = vi.mocked(legacyAddComponent);
|
|
17
|
+
const mockedGetProjectConfig = vi.mocked(getProjectConfig);
|
|
18
|
+
const mockedUseV3Api = vi.mocked(useV3Api);
|
|
19
|
+
const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
|
|
5
20
|
describe('commands/project/add', () => {
|
|
6
21
|
const yargsMock = yargs;
|
|
7
22
|
describe('command', () => {
|
|
@@ -40,4 +55,55 @@ describe('commands/project/add', () => {
|
|
|
40
55
|
});
|
|
41
56
|
});
|
|
42
57
|
});
|
|
58
|
+
describe('handler', () => {
|
|
59
|
+
const mockProjectConfig = {
|
|
60
|
+
name: 'test-project',
|
|
61
|
+
srcDir: 'src',
|
|
62
|
+
platformVersion: 'v3',
|
|
63
|
+
};
|
|
64
|
+
const mockProjectDir = '/path/to/project';
|
|
65
|
+
const mockArgs = {
|
|
66
|
+
derivedAccountId: 123,
|
|
67
|
+
name: 'test-component',
|
|
68
|
+
type: 'module',
|
|
69
|
+
};
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockedGetProjectConfig.mockResolvedValue({
|
|
72
|
+
projectConfig: mockProjectConfig,
|
|
73
|
+
projectDir: mockProjectDir,
|
|
74
|
+
});
|
|
75
|
+
mockedTrackCommandUsage.mockResolvedValue();
|
|
76
|
+
mockedV3AddComponent.mockResolvedValue();
|
|
77
|
+
mockedLegacyAddComponent.mockResolvedValue();
|
|
78
|
+
vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
79
|
+
throw new Error('process.exit called');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('should call v3AddComponent with accountId for v3 projects', async () => {
|
|
83
|
+
mockedUseV3Api.mockReturnValue(true);
|
|
84
|
+
await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
|
|
85
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
|
|
86
|
+
expect(mockedV3AddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
|
|
87
|
+
expect(mockedLegacyAddComponent).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
it('should call legacyAddComponent for non-v3 projects', async () => {
|
|
90
|
+
mockedUseV3Api.mockReturnValue(false);
|
|
91
|
+
await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
|
|
92
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
|
|
93
|
+
expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig);
|
|
94
|
+
expect(mockedV3AddComponent).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
it('should exit with error when project config is not found', async () => {
|
|
97
|
+
mockedGetProjectConfig.mockResolvedValue({
|
|
98
|
+
projectConfig: null,
|
|
99
|
+
projectDir: null,
|
|
100
|
+
});
|
|
101
|
+
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
102
|
+
throw new Error('process.exit called');
|
|
103
|
+
});
|
|
104
|
+
await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
|
|
105
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
106
|
+
mockExit.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
43
109
|
});
|
package/commands/project/add.js
CHANGED
|
@@ -23,7 +23,7 @@ async function handler(args) {
|
|
|
23
23
|
}
|
|
24
24
|
const isV3ProjectCreate = useV3Api(projectConfig.platformVersion);
|
|
25
25
|
if (isV3ProjectCreate) {
|
|
26
|
-
await v3AddComponent(args, projectDir, projectConfig);
|
|
26
|
+
await v3AddComponent(args, projectDir, projectConfig, derivedAccountId);
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
29
|
await legacyAddComponent(args, projectDir, projectConfig);
|
|
@@ -41,7 +41,7 @@ async function handler(args) {
|
|
|
41
41
|
type: selectProjectTemplatePromptResponse.projectTemplate?.name ||
|
|
42
42
|
(selectProjectTemplatePromptResponse.componentTemplates || [])
|
|
43
43
|
// @ts-expect-error
|
|
44
|
-
.map((item) => item.
|
|
44
|
+
.map((item) => item.type)
|
|
45
45
|
.join(','),
|
|
46
46
|
}, derivedAccountId);
|
|
47
47
|
const projectDest = path.resolve(getCwd(), projectNameAndDestPromptResponse.dest);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CommonArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
|
|
2
|
-
type ProjectUploadArgs = CommonArgs & JSONOutputArgs & {
|
|
1
|
+
import { CommonArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
|
|
2
|
+
type ProjectUploadArgs = CommonArgs & JSONOutputArgs & EnvironmentArgs & {
|
|
3
3
|
forceCreate: boolean;
|
|
4
4
|
message: string;
|
|
5
5
|
m: string;
|
|
@@ -25,7 +25,7 @@ async function handler(args) {
|
|
|
25
25
|
validateProjectConfig(projectConfig, projectDir);
|
|
26
26
|
let targetAccountId;
|
|
27
27
|
if (useV3Api(projectConfig.platformVersion)) {
|
|
28
|
-
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
|
|
28
|
+
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile, args.useEnv);
|
|
29
29
|
}
|
|
30
30
|
targetAccountId = targetAccountId || derivedAccountId;
|
|
31
31
|
const accountConfig = getAccountConfig(targetAccountId);
|
|
@@ -75,6 +75,9 @@ async function handler(args) {
|
|
|
75
75
|
resultJson.personalAccessKey = createResult.personalAccessKey;
|
|
76
76
|
}
|
|
77
77
|
catch (err) {
|
|
78
|
+
SpinniesManager.fail('createTestAccount', {
|
|
79
|
+
text: commands.testAccount.create.polling.createFailure,
|
|
80
|
+
});
|
|
78
81
|
logError(err);
|
|
79
82
|
SpinniesManager.fail('createTestAccount', {
|
|
80
83
|
text: commands.testAccount.create.polling.createFailure,
|
package/lang/en.d.ts
CHANGED
|
@@ -884,12 +884,10 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
|
|
|
884
884
|
readonly configuringCursor: "Configuring Cursor...";
|
|
885
885
|
readonly failedToConfigureCursor: "Failed to configure Cursor";
|
|
886
886
|
readonly configuredCursor: "Configured Cursor";
|
|
887
|
-
readonly cursorNotFound: "Cursor not found - skipping configuration";
|
|
888
887
|
readonly alreadyInstalled: "HubSpot CLI mcp server already installed, reinstalling";
|
|
889
888
|
readonly configuringWindsurf: "Configuring Windsurf...";
|
|
890
889
|
readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
|
|
891
890
|
readonly configuredWindsurf: "Configured Windsurf";
|
|
892
|
-
readonly windsurfNotFound: "Windsurf not found - skipping configuration";
|
|
893
891
|
readonly configuringVsCode: "Configuring VSCode...";
|
|
894
892
|
readonly failedToConfigureVsCode: "Failed to configure VSCode";
|
|
895
893
|
readonly configuredVsCode: "Configured VSCode";
|
|
@@ -1552,6 +1550,28 @@ ${string}`;
|
|
|
1552
1550
|
readonly app: {
|
|
1553
1551
|
readonly describe: "Commands for managing apps.";
|
|
1554
1552
|
readonly subcommands: {
|
|
1553
|
+
readonly install: {
|
|
1554
|
+
readonly describe: "Install an OAuth app into a test account.";
|
|
1555
|
+
readonly options: {
|
|
1556
|
+
readonly appUid: "The uid of the app to install";
|
|
1557
|
+
readonly projectName: "The name of the project that contains the app";
|
|
1558
|
+
};
|
|
1559
|
+
readonly positionals: {
|
|
1560
|
+
readonly testAccountId: "The id of the test account to install the app into";
|
|
1561
|
+
};
|
|
1562
|
+
readonly errors: {
|
|
1563
|
+
readonly mustSpecifyProjectName: `You must specify a project name. Use the ${string} flag to specify the project name or run this command from within a project directory.`;
|
|
1564
|
+
readonly noAppUidFound: `No app uid found. Please specify the app uid with the ${string} flag or run this command from within a project that contains an app.`;
|
|
1565
|
+
readonly appMustBeOauth: "This command only supports installing oauth apps. Please specify an app with oauth auth type.";
|
|
1566
|
+
};
|
|
1567
|
+
readonly polling: {
|
|
1568
|
+
readonly start: "Installing app...";
|
|
1569
|
+
readonly success: "App installed successfully";
|
|
1570
|
+
readonly failure: "App installation failed";
|
|
1571
|
+
readonly error: "Error installing app";
|
|
1572
|
+
};
|
|
1573
|
+
readonly example: "Install the app with uid my-app-uid from the project named \"my-project\" into the target account with id 1234567890";
|
|
1574
|
+
};
|
|
1555
1575
|
readonly secret: {
|
|
1556
1576
|
readonly describe: "Commands for managing secrets.";
|
|
1557
1577
|
readonly subcommands: {
|
|
@@ -2571,6 +2591,8 @@ export declare const lib: {
|
|
|
2571
2591
|
readonly activeInstallations: (appName: string, installCount: number) => string;
|
|
2572
2592
|
readonly error: "An error occurred while checking installations for your app";
|
|
2573
2593
|
};
|
|
2594
|
+
readonly distributionChanged: `Your app's distribution type has been changed from private to marketplace. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${string} change your app's distribution type. This will uninstall your app from all accounts.`;
|
|
2595
|
+
readonly authTypeChanged: `Your app's auth type has been changed from static to oauth. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${string} change your app's auth type. This will uninstall your app from all accounts.`;
|
|
2574
2596
|
};
|
|
2575
2597
|
readonly LocalDevWebsocketServer: {
|
|
2576
2598
|
readonly errors: {
|
package/lang/en.js
CHANGED
|
@@ -5,7 +5,7 @@ import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constant
|
|
|
5
5
|
import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
|
|
6
6
|
import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
|
|
7
7
|
import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, } from '../lib/projects/urls.js';
|
|
8
|
-
import { PROJECT_CONFIG_FILE, PROJECT_WITH_APP } from '../lib/constants.js';
|
|
8
|
+
import { APP_DISTRIBUTION_TYPES, APP_AUTH_TYPES, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
|
|
9
9
|
import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
|
|
10
10
|
export const commands = {
|
|
11
11
|
generalErrors: {
|
|
@@ -889,13 +889,11 @@ export const commands = {
|
|
|
889
889
|
configuringCursor: 'Configuring Cursor...',
|
|
890
890
|
failedToConfigureCursor: 'Failed to configure Cursor',
|
|
891
891
|
configuredCursor: 'Configured Cursor',
|
|
892
|
-
cursorNotFound: 'Cursor not found - skipping configuration',
|
|
893
892
|
alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
|
|
894
893
|
// Windsurf
|
|
895
894
|
configuringWindsurf: 'Configuring Windsurf...',
|
|
896
895
|
failedToConfigureWindsurf: 'Failed to configure Windsurf',
|
|
897
896
|
configuredWindsurf: 'Configured Windsurf',
|
|
898
|
-
windsurfNotFound: 'Windsurf not found - skipping configuration',
|
|
899
897
|
// VS Code
|
|
900
898
|
configuringVsCode: 'Configuring VSCode...',
|
|
901
899
|
failedToConfigureVsCode: 'Failed to configure VSCode',
|
|
@@ -1549,6 +1547,28 @@ export const commands = {
|
|
|
1549
1547
|
app: {
|
|
1550
1548
|
describe: 'Commands for managing apps.',
|
|
1551
1549
|
subcommands: {
|
|
1550
|
+
install: {
|
|
1551
|
+
describe: 'Install an OAuth app into a test account.',
|
|
1552
|
+
options: {
|
|
1553
|
+
appUid: 'The uid of the app to install',
|
|
1554
|
+
projectName: 'The name of the project that contains the app',
|
|
1555
|
+
},
|
|
1556
|
+
positionals: {
|
|
1557
|
+
testAccountId: 'The id of the test account to install the app into',
|
|
1558
|
+
},
|
|
1559
|
+
errors: {
|
|
1560
|
+
mustSpecifyProjectName: `You must specify a project name. Use the ${uiCommandReference('--project-name')} flag to specify the project name or run this command from within a project directory.`,
|
|
1561
|
+
noAppUidFound: `No app uid found. Please specify the app uid with the ${uiCommandReference('--app-uid')} flag or run this command from within a project that contains an app.`,
|
|
1562
|
+
appMustBeOauth: 'This command only supports installing oauth apps. Please specify an app with oauth auth type.',
|
|
1563
|
+
},
|
|
1564
|
+
polling: {
|
|
1565
|
+
start: 'Installing app...',
|
|
1566
|
+
success: 'App installed successfully',
|
|
1567
|
+
failure: 'App installation failed',
|
|
1568
|
+
error: 'Error installing app',
|
|
1569
|
+
},
|
|
1570
|
+
example: 'Install the app with uid my-app-uid from the project named "my-project" into the target account with id 1234567890',
|
|
1571
|
+
},
|
|
1552
1572
|
secret: {
|
|
1553
1573
|
describe: 'Commands for managing secrets.',
|
|
1554
1574
|
subcommands: {
|
|
@@ -2568,6 +2588,8 @@ export const lib = {
|
|
|
2568
2588
|
activeInstallations: (appName, installCount) => `[WARNING] Your app ${chalk.bold(appName)} is installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}`,
|
|
2569
2589
|
error: 'An error occurred while checking installations for your app',
|
|
2570
2590
|
},
|
|
2591
|
+
distributionChanged: `Your app's distribution type has been changed from ${APP_DISTRIBUTION_TYPES.PRIVATE} to ${APP_DISTRIBUTION_TYPES.MARKETPLACE}. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${chalk.bold('permanantly')} change your app's distribution type. This will uninstall your app from all accounts.`,
|
|
2592
|
+
authTypeChanged: `Your app's auth type has been changed from ${APP_AUTH_TYPES.STATIC} to ${APP_AUTH_TYPES.OAUTH}. Once uploaded, this change cannot be reversed. Before uploading your project, confirm that you want to ${chalk.bold('permanantly')} change your app's auth type. This will uninstall your app from all accounts.`,
|
|
2571
2593
|
},
|
|
2572
2594
|
LocalDevWebsocketServer: {
|
|
2573
2595
|
errors: {
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ interface McpCommand {
|
|
|
16
16
|
args: string[];
|
|
17
17
|
}
|
|
18
18
|
export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
|
|
19
|
-
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
20
19
|
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function
|
|
20
|
+
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
|
+
export declare function setupCursor(mcpCommand?: McpCommand): boolean;
|
|
22
|
+
export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
|
|
23
23
|
export {};
|
package/lib/mcp/setup.js
CHANGED
|
@@ -4,6 +4,10 @@ import { promptUser } from '../prompts/promptUtils.js';
|
|
|
4
4
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
5
5
|
import { logError } from '../errorHandlers/index.js';
|
|
6
6
|
import { execAsync } from '../../mcp-server/utils/command.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
7
11
|
const mcpServerName = 'hubspot-cli-mcp';
|
|
8
12
|
const claudeCode = 'claude';
|
|
9
13
|
const windsurf = 'windsurf';
|
|
@@ -68,6 +72,96 @@ async function runSetupFunction(func) {
|
|
|
68
72
|
throw new Error();
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
function setupMcpConfigFile(config) {
|
|
76
|
+
try {
|
|
77
|
+
SpinniesManager.add('spinner', {
|
|
78
|
+
text: config.configuringMessage,
|
|
79
|
+
});
|
|
80
|
+
if (!existsSync(config.configPath)) {
|
|
81
|
+
fs.writeFileSync(config.configPath, JSON.stringify({}, null, 2));
|
|
82
|
+
}
|
|
83
|
+
let mcpConfig = {};
|
|
84
|
+
let configContent;
|
|
85
|
+
try {
|
|
86
|
+
configContent = fs.readFileSync(config.configPath, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
SpinniesManager.fail('spinner', {
|
|
90
|
+
text: config.failedMessage,
|
|
91
|
+
});
|
|
92
|
+
logError(error);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
// In the event the file exists, but is empty, initialize it to and empty object
|
|
97
|
+
if (configContent.trim() === '') {
|
|
98
|
+
mcpConfig = {};
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
mcpConfig = JSON.parse(configContent);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
SpinniesManager.fail('spinner', {
|
|
106
|
+
text: config.failedMessage,
|
|
107
|
+
});
|
|
108
|
+
uiLogger.error(commands.mcp.setup.errors.errorParsingJsonFIle(config.configPath, error instanceof Error ? error.message : `${error}`));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
// Initialize mcpServers if it doesn't exist
|
|
112
|
+
if (!mcpConfig.mcpServers) {
|
|
113
|
+
mcpConfig.mcpServers = {};
|
|
114
|
+
}
|
|
115
|
+
// Add or update HubSpot CLI MCP server
|
|
116
|
+
mcpConfig.mcpServers[mcpServerName] = {
|
|
117
|
+
...config.mcpCommand,
|
|
118
|
+
};
|
|
119
|
+
// Write the updated config
|
|
120
|
+
fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
|
|
121
|
+
SpinniesManager.succeed('spinner', {
|
|
122
|
+
text: config.configuredMessage,
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
SpinniesManager.fail('spinner', {
|
|
128
|
+
text: config.failedMessage,
|
|
129
|
+
});
|
|
130
|
+
logError(error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
135
|
+
try {
|
|
136
|
+
SpinniesManager.add('vsCode', {
|
|
137
|
+
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
138
|
+
});
|
|
139
|
+
const mcpConfig = JSON.stringify({
|
|
140
|
+
name: mcpServerName,
|
|
141
|
+
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
142
|
+
});
|
|
143
|
+
await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
144
|
+
SpinniesManager.succeed('vsCode', {
|
|
145
|
+
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
146
|
+
});
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof Error &&
|
|
151
|
+
error.message.includes('code: command not found')) {
|
|
152
|
+
SpinniesManager.fail('vsCode', {
|
|
153
|
+
text: commands.mcp.setup.spinners.vsCodeNotFound,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
SpinniesManager.fail('vsCode', {
|
|
158
|
+
text: commands.mcp.setup.spinners.failedToConfigureVsCode,
|
|
159
|
+
});
|
|
160
|
+
logError(error);
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
71
165
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
72
166
|
try {
|
|
73
167
|
SpinniesManager.add('claudeCode', {
|
|
@@ -88,7 +182,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
88
182
|
});
|
|
89
183
|
await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
|
|
90
184
|
}
|
|
91
|
-
await execAsync(`claude mcp add-json "${mcpServerName}" ${
|
|
185
|
+
await execAsync(`claude mcp add-json "${mcpServerName}" '${mcpConfig}' --scope user`);
|
|
92
186
|
SpinniesManager.succeed('claudeCode', {
|
|
93
187
|
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
94
188
|
});
|
|
@@ -117,44 +211,25 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
117
211
|
return false;
|
|
118
212
|
}
|
|
119
213
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await execAsync(`${commandName} --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
130
|
-
SpinniesManager.succeed(commandName, {
|
|
131
|
-
text: configuredText,
|
|
132
|
-
});
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
if (error instanceof Error && error.message.includes(commandName)) {
|
|
137
|
-
SpinniesManager.fail(commandName, {
|
|
138
|
-
text: notFoundText,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
SpinniesManager.fail(commandName, {
|
|
143
|
-
text: failedText,
|
|
144
|
-
});
|
|
145
|
-
logError(error);
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
151
|
-
return setupVsCodeBasedIntegration('code', commands.mcp.setup.spinners.configuringVsCode, commands.mcp.setup.spinners.configuredVsCode, commands.mcp.setup.spinners.vsCodeNotFound, commands.mcp.setup.spinners.failedToConfigureVsCode, mcpCommand);
|
|
152
|
-
}
|
|
153
|
-
export async function setupCursor(mcpCommand = defaultMcpCommand) {
|
|
154
|
-
return setupVsCodeBasedIntegration('cursor', commands.mcp.setup.spinners.configuringCursor, commands.mcp.setup.spinners.configuredCursor, commands.mcp.setup.spinners.cursorNotFound, commands.mcp.setup.spinners.failedToConfigureCursor, mcpCommand);
|
|
214
|
+
export function setupCursor(mcpCommand = defaultMcpCommand) {
|
|
215
|
+
const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
216
|
+
return setupMcpConfigFile({
|
|
217
|
+
configPath: cursorConfigPath,
|
|
218
|
+
configuringMessage: commands.mcp.setup.spinners.configuringCursor,
|
|
219
|
+
configuredMessage: commands.mcp.setup.spinners.configuredCursor,
|
|
220
|
+
failedMessage: commands.mcp.setup.spinners.failedToConfigureCursor,
|
|
221
|
+
mcpCommand: buildCommandWithAgentString(mcpCommand, cursor),
|
|
222
|
+
});
|
|
155
223
|
}
|
|
156
|
-
export
|
|
157
|
-
|
|
224
|
+
export function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
225
|
+
const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
226
|
+
return setupMcpConfigFile({
|
|
227
|
+
configPath: windsurfConfigPath,
|
|
228
|
+
configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
|
|
229
|
+
configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
|
|
230
|
+
failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
|
|
231
|
+
mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
|
|
232
|
+
});
|
|
158
233
|
}
|
|
159
234
|
function buildCommandWithAgentString(mcpCommand, agent) {
|
|
160
235
|
const mcpCommandCopy = structuredClone(mcpCommand);
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
|
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
5
|
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
|
|
6
6
|
export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined, useEnv?: boolean): Promise<number | undefined>;
|
package/lib/projectProfiles.js
CHANGED
|
@@ -38,7 +38,12 @@ export function loadProfile(projectConfig, projectDir, profileName) {
|
|
|
38
38
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
41
|
+
return {
|
|
42
|
+
...profile,
|
|
43
|
+
accountId: process.env.HUBSPOT_ACCOUNT_ID
|
|
44
|
+
? Number(process.env.HUBSPOT_ACCOUNT_ID)
|
|
45
|
+
: profile.accountId,
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
48
|
catch (e) {
|
|
44
49
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
|
|
@@ -54,7 +59,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
|
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
|
-
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
|
|
62
|
+
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile, useEnv = false) {
|
|
58
63
|
if (argsProfile) {
|
|
59
64
|
logProfileHeader(argsProfile);
|
|
60
65
|
const profile = loadProfile(projectConfig, projectDir, argsProfile);
|
|
@@ -63,6 +68,9 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
|
|
|
63
68
|
process.exit(EXIT_CODES.ERROR);
|
|
64
69
|
}
|
|
65
70
|
logProfileFooter(profile, true);
|
|
71
|
+
if (useEnv) {
|
|
72
|
+
return Number(process.env.HUBSPOT_ACCOUNT_ID);
|
|
73
|
+
}
|
|
66
74
|
return profile.accountId;
|
|
67
75
|
}
|
|
68
76
|
else {
|
|
@@ -100,6 +100,7 @@ describe('AppDevModeInterface', () => {
|
|
|
100
100
|
setAppDataForUid: vi.fn(),
|
|
101
101
|
addListener: vi.fn(),
|
|
102
102
|
addUploadWarning: vi.fn(),
|
|
103
|
+
removeListener: vi.fn(),
|
|
103
104
|
};
|
|
104
105
|
mockLocalDevLogger = {};
|
|
105
106
|
// Mock constructors
|
|
@@ -387,6 +388,15 @@ describe('AppDevModeInterface', () => {
|
|
|
387
388
|
await appDevModeInterface.cleanup();
|
|
388
389
|
expect(UIEDevModeInterface.cleanup).toHaveBeenCalled();
|
|
389
390
|
});
|
|
391
|
+
it('should remove state listeners', async () => {
|
|
392
|
+
await appDevModeInterface.cleanup();
|
|
393
|
+
expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('devServerMessage',
|
|
394
|
+
// @ts-expect-error access private method for testing
|
|
395
|
+
appDevModeInterface.onDevServerMessage);
|
|
396
|
+
expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('projectNodes',
|
|
397
|
+
// @ts-expect-error
|
|
398
|
+
appDevModeInterface.onChangeProjectNodes);
|
|
399
|
+
});
|
|
390
400
|
});
|
|
391
401
|
describe('isAutomaticallyInstallable()', () => {
|
|
392
402
|
it('should return true for static auth app on test account with correct parent', () => {
|
|
@@ -7,6 +7,7 @@ import { projectAddPromptV3 } from '../../../prompts/projectAddPrompt.js';
|
|
|
7
7
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
8
8
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
9
9
|
import { getProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
|
|
10
|
+
import { trackCommandUsage } from '../../../usageTracking.js';
|
|
10
11
|
import { commands } from '../../../../lang/en.js';
|
|
11
12
|
vi.mock('fs');
|
|
12
13
|
vi.mock('../../../prompts/promptUtils');
|
|
@@ -16,6 +17,7 @@ vi.mock('../../../prompts/projectAddPrompt');
|
|
|
16
17
|
vi.mock('@hubspot/local-dev-lib/github');
|
|
17
18
|
vi.mock('@hubspot/local-dev-lib/logger');
|
|
18
19
|
vi.mock('@hubspot/project-parsing-lib/src/lib/project');
|
|
20
|
+
vi.mock('../../../usageTracking');
|
|
19
21
|
const mockedFs = vi.mocked(fs);
|
|
20
22
|
const mockedGetConfigForPlatformVersion = vi.mocked(getConfigForPlatformVersion);
|
|
21
23
|
const mockedConfirmPrompt = vi.mocked(confirmPrompt);
|
|
@@ -24,6 +26,7 @@ const mockedProjectAddPromptV3 = vi.mocked(projectAddPromptV3);
|
|
|
24
26
|
const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
|
|
25
27
|
const mockedLogger = vi.mocked(logger);
|
|
26
28
|
const mockedGetProjectMetadata = vi.mocked(getProjectMetadata);
|
|
29
|
+
const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
|
|
27
30
|
describe('lib/projects/add/v3AddComponent', () => {
|
|
28
31
|
const mockProjectConfig = {
|
|
29
32
|
name: 'test-project',
|
|
@@ -32,6 +35,7 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
32
35
|
};
|
|
33
36
|
const mockArgs = { name: 'test-component', type: 'module' };
|
|
34
37
|
const projectDir = '/path/to/project';
|
|
38
|
+
const mockAccountId = 123;
|
|
35
39
|
const mockComponentTemplate = {
|
|
36
40
|
label: 'Test Component',
|
|
37
41
|
path: 'test-component',
|
|
@@ -62,6 +66,7 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
62
66
|
authType: 'oauth',
|
|
63
67
|
distribution: 'private',
|
|
64
68
|
});
|
|
69
|
+
mockedTrackCommandUsage.mockResolvedValue();
|
|
65
70
|
});
|
|
66
71
|
describe('v3AddComponent()', () => {
|
|
67
72
|
it('successfully adds a component when app already exists', async () => {
|
|
@@ -79,10 +84,13 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
79
84
|
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
80
85
|
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
81
86
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
82
|
-
await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
|
|
87
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
83
88
|
expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('v3');
|
|
84
89
|
expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
|
|
85
90
|
expect(mockedProjectAddPromptV3).toHaveBeenCalled();
|
|
91
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
92
|
+
type: 'module',
|
|
93
|
+
}, mockAccountId);
|
|
86
94
|
expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
|
|
87
95
|
sourceDir: ['v3/test-component'],
|
|
88
96
|
hideLogs: true,
|
|
@@ -106,8 +114,11 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
106
114
|
mockedConfirmPrompt.mockResolvedValue(true);
|
|
107
115
|
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
108
116
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
109
|
-
await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
|
|
117
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
110
118
|
expect(mockedCreateV3App).toHaveBeenCalled();
|
|
119
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
120
|
+
type: 'module',
|
|
121
|
+
}, mockAccountId);
|
|
111
122
|
expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
|
|
112
123
|
sourceDir: ['v3/test-component', 'v3/app-template'],
|
|
113
124
|
}));
|
|
@@ -132,8 +143,11 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
132
143
|
mockedConfirmPrompt.mockResolvedValue(true);
|
|
133
144
|
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
134
145
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
135
|
-
await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
|
|
146
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
136
147
|
expect(mockedCreateV3App).not.toHaveBeenCalled();
|
|
148
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
149
|
+
type: '',
|
|
150
|
+
}, mockAccountId);
|
|
137
151
|
expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
|
|
138
152
|
});
|
|
139
153
|
it('throws an error when app count exceeds maximum', async () => {
|
|
@@ -146,7 +160,7 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
146
160
|
};
|
|
147
161
|
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
148
162
|
mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataMaxApps);
|
|
149
|
-
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('This project currently has the maximum number of apps: 1');
|
|
163
|
+
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
|
|
150
164
|
});
|
|
151
165
|
it('throws an error when components list is empty', async () => {
|
|
152
166
|
const mockEmptyConfig = {
|
|
@@ -154,7 +168,7 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
154
168
|
parentComponents: [],
|
|
155
169
|
};
|
|
156
170
|
mockedGetConfigForPlatformVersion.mockResolvedValue(mockEmptyConfig);
|
|
157
|
-
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
171
|
+
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
158
172
|
});
|
|
159
173
|
it('throws an error when app meta file cannot be parsed', async () => {
|
|
160
174
|
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
@@ -162,7 +176,7 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
162
176
|
mockedFs.readFileSync.mockImplementation(() => {
|
|
163
177
|
throw new Error('File read error');
|
|
164
178
|
});
|
|
165
|
-
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('Unable to parse app file');
|
|
179
|
+
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
|
|
166
180
|
});
|
|
167
181
|
it('throws an error when cloning fails', async () => {
|
|
168
182
|
const mockAppMeta = {
|
|
@@ -179,7 +193,57 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
179
193
|
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
180
194
|
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
181
195
|
mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
|
|
182
|
-
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
|
|
196
|
+
await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
|
|
197
|
+
});
|
|
198
|
+
it('should track usage with multiple component types', async () => {
|
|
199
|
+
const mockAppMeta = {
|
|
200
|
+
config: {
|
|
201
|
+
distribution: 'private',
|
|
202
|
+
auth: { type: 'oauth' },
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const mockSecondComponentTemplate = {
|
|
206
|
+
label: 'Test Card',
|
|
207
|
+
path: 'test-card',
|
|
208
|
+
type: 'card',
|
|
209
|
+
supportedAuthTypes: ['oauth'],
|
|
210
|
+
supportedDistributions: ['private'],
|
|
211
|
+
};
|
|
212
|
+
const mockPromptResponse = {
|
|
213
|
+
componentTemplate: [mockComponentTemplate, mockSecondComponentTemplate],
|
|
214
|
+
};
|
|
215
|
+
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
216
|
+
mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
|
|
217
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
218
|
+
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
219
|
+
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
220
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
221
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
222
|
+
type: 'module,card',
|
|
223
|
+
}, mockAccountId);
|
|
224
|
+
});
|
|
225
|
+
it('should track usage with empty type when no components are selected', async () => {
|
|
226
|
+
const mockProjectMetadataNoApps = {
|
|
227
|
+
hsMetaFiles: [],
|
|
228
|
+
components: {
|
|
229
|
+
app: {
|
|
230
|
+
count: 1,
|
|
231
|
+
maxCount: 1,
|
|
232
|
+
hsMetaFiles: ['/path/to/app.meta.json'],
|
|
233
|
+
},
|
|
234
|
+
module: { count: 0, maxCount: 5, hsMetaFiles: [] },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const mockPromptResponse = {
|
|
238
|
+
componentTemplate: [],
|
|
239
|
+
};
|
|
240
|
+
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
241
|
+
mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
|
|
242
|
+
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
243
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
244
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
245
|
+
type: '',
|
|
246
|
+
}, mockAccountId);
|
|
183
247
|
});
|
|
184
248
|
});
|
|
185
249
|
});
|
|
@@ -12,7 +12,8 @@ import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
|
|
|
12
12
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
13
13
|
import { debugError } from '../../errorHandlers/index.js';
|
|
14
14
|
import { uiLogger } from '../../ui/logger.js';
|
|
15
|
-
|
|
15
|
+
import { trackCommandUsage } from '../../usageTracking.js';
|
|
16
|
+
export async function v3AddComponent(args, projectDir, projectConfig, accountId) {
|
|
16
17
|
uiLogger.log(commands.project.add.creatingComponent(projectConfig.name));
|
|
17
18
|
const config = await getConfigForPlatformVersion(projectConfig.platformVersion);
|
|
18
19
|
const { components, parentComponents } = config;
|
|
@@ -47,6 +48,10 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
|
|
|
47
48
|
}
|
|
48
49
|
const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
|
|
49
50
|
const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
|
|
51
|
+
const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.type);
|
|
52
|
+
await trackCommandUsage('project-add', {
|
|
53
|
+
type: componentTypes?.join(','),
|
|
54
|
+
}, accountId);
|
|
50
55
|
try {
|
|
51
56
|
const components = projectAddPromptResponse.componentTemplate?.map((componentTemplate) => {
|
|
52
57
|
return path.join(projectConfig.platformVersion, componentTemplate.path);
|
|
@@ -11,6 +11,7 @@ declare class AppDevModeInterface {
|
|
|
11
11
|
_appNode?: AppIRNode | null;
|
|
12
12
|
marketplaceAppInstalls?: number;
|
|
13
13
|
constructor(options: AppDevModeInterfaceConstructorOptions);
|
|
14
|
+
private getAppNodeFromProjectNodes;
|
|
14
15
|
private get appNode();
|
|
15
16
|
private get appData();
|
|
16
17
|
private set appData(value);
|
|
@@ -21,7 +22,10 @@ declare class AppDevModeInterface {
|
|
|
21
22
|
private autoInstallStaticAuthApp;
|
|
22
23
|
private installAppOrOpenInstallUrl;
|
|
23
24
|
private checkTestAccountAppInstallation;
|
|
24
|
-
private
|
|
25
|
+
private onDevServerMessage;
|
|
26
|
+
private onChangeProjectNodes;
|
|
27
|
+
private setUpStateListeners;
|
|
28
|
+
private removeStateListeners;
|
|
25
29
|
setup(args: any): Promise<void>;
|
|
26
30
|
start(): Promise<void>;
|
|
27
31
|
fileChange(filePath: string, event: string): Promise<void>;
|
|
@@ -30,12 +30,13 @@ class AppDevModeInterface {
|
|
|
30
30
|
process.exit(EXIT_CODES.ERROR);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
getAppNodeFromProjectNodes(projectNodes) {
|
|
34
|
+
return Object.values(projectNodes).find(isAppIRNode) || null;
|
|
35
|
+
}
|
|
33
36
|
// Assumes only one app per project
|
|
34
37
|
get appNode() {
|
|
35
38
|
if (this._appNode === undefined) {
|
|
36
|
-
this._appNode =
|
|
37
|
-
Object.values(this.localDevState.projectNodes).find(isAppIRNode) ||
|
|
38
|
-
null;
|
|
39
|
+
this._appNode = this.getAppNodeFromProjectNodes(this.localDevState.projectNodes);
|
|
39
40
|
}
|
|
40
41
|
return this._appNode;
|
|
41
42
|
}
|
|
@@ -175,12 +176,33 @@ class AppDevModeInterface {
|
|
|
175
176
|
}
|
|
176
177
|
return { needsInstall: !isInstalledWithScopeGroups, isReinstall };
|
|
177
178
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
onDevServerMessage = (message) => {
|
|
180
|
+
if (message === LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED) {
|
|
181
|
+
this.checkTestAccountAppInstallation();
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
onChangeProjectNodes = (nodes) => {
|
|
185
|
+
const newAppNode = this.getAppNodeFromProjectNodes(nodes);
|
|
186
|
+
const oldDistribution = this.appNode?.config.distribution;
|
|
187
|
+
const newDistribution = newAppNode?.config.distribution;
|
|
188
|
+
const oldAuthType = this.appNode?.config.auth.type;
|
|
189
|
+
const newAuthType = newAppNode?.config.auth.type;
|
|
190
|
+
if (newDistribution === APP_DISTRIBUTION_TYPES.MARKETPLACE &&
|
|
191
|
+
oldDistribution !== APP_DISTRIBUTION_TYPES.MARKETPLACE) {
|
|
192
|
+
this.localDevState.addUploadWarning(lib.AppDevModeInterface.distributionChanged);
|
|
193
|
+
}
|
|
194
|
+
else if (newAuthType === APP_AUTH_TYPES.OAUTH &&
|
|
195
|
+
oldAuthType !== APP_AUTH_TYPES.OAUTH) {
|
|
196
|
+
this.localDevState.addUploadWarning(lib.AppDevModeInterface.authTypeChanged);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
setUpStateListeners() {
|
|
200
|
+
this.localDevState.addListener('devServerMessage', this.onDevServerMessage);
|
|
201
|
+
this.localDevState.addListener('projectNodes', this.onChangeProjectNodes);
|
|
202
|
+
}
|
|
203
|
+
removeStateListeners() {
|
|
204
|
+
this.localDevState.removeListener('devServerMessage', this.onDevServerMessage);
|
|
205
|
+
this.localDevState.removeListener('projectNodes', this.onChangeProjectNodes);
|
|
184
206
|
}
|
|
185
207
|
// @ts-expect-error TODO: reconcile types between CLI and UIE Dev Server
|
|
186
208
|
// In the future, update UIE Dev Server to use LocalDevState
|
|
@@ -215,7 +237,7 @@ class AppDevModeInterface {
|
|
|
215
237
|
catch (e) {
|
|
216
238
|
logError(e);
|
|
217
239
|
}
|
|
218
|
-
this.
|
|
240
|
+
this.setUpStateListeners();
|
|
219
241
|
return UIEDevModeInterface.setup(args);
|
|
220
242
|
}
|
|
221
243
|
async start() {
|
|
@@ -239,6 +261,7 @@ class AppDevModeInterface {
|
|
|
239
261
|
if (!this.appNode) {
|
|
240
262
|
return;
|
|
241
263
|
}
|
|
264
|
+
this.removeStateListeners();
|
|
242
265
|
return UIEDevModeInterface.cleanup();
|
|
243
266
|
}
|
|
244
267
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentTypes, Component, GenericComponentConfig, PublicAppComponentConfig, PrivateAppComponentConfig, AppCardComponentConfig } from '../../types/Projects.js';
|
|
2
|
-
import { IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/src/lib/types.js';
|
|
2
|
+
import { IntermediateRepresentationNode, IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/src/lib/types.js';
|
|
3
3
|
import { AppIRNode } from '../../types/ProjectComponents.js';
|
|
4
4
|
export declare const CONFIG_FILES: {
|
|
5
5
|
[k in ComponentTypes]: string;
|
|
@@ -15,4 +15,4 @@ export declare function getProjectComponentTypes(components: Array<Component>):
|
|
|
15
15
|
export declare function getComponentUid(component?: Component | null): string | null;
|
|
16
16
|
export declare function componentIsApp(component?: Component | null): component is Component<PublicAppComponentConfig | PrivateAppComponentConfig>;
|
|
17
17
|
export declare function componentIsPublicApp(component?: Component | null): component is Component<PublicAppComponentConfig>;
|
|
18
|
-
export declare function isAppIRNode(component: IntermediateRepresentationNodeLocalDev): component is AppIRNode;
|
|
18
|
+
export declare function isAppIRNode(component: IntermediateRepresentationNodeLocalDev | IntermediateRepresentationNode): component is AppIRNode;
|
package/lib/projects/upload.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FileResult } from 'tmp';
|
|
2
|
+
import { IntermediateRepresentation } from '@hubspot/project-parsing-lib';
|
|
2
3
|
import { ProjectConfig } from '../../types/Projects.js';
|
|
3
4
|
type ProjectUploadCallbackFunction<T> = (accountId: number, projectConfig: ProjectConfig, tempFile: FileResult, buildId: number) => Promise<T>;
|
|
4
5
|
type ProjectUploadResult<T> = {
|
|
@@ -20,5 +21,5 @@ type HandleProjectUploadArg<T> = {
|
|
|
20
21
|
export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
|
|
21
22
|
export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig): void;
|
|
22
23
|
export declare function validateNoHSMetaMismatch(srcDir: string, projectConfig: ProjectConfig): Promise<void>;
|
|
23
|
-
export declare function handleTranslate(projectDir: string, projectConfig: ProjectConfig, accountId: number, skipValidation: boolean, profile: string | undefined): Promise<
|
|
24
|
+
export declare function handleTranslate(projectDir: string, projectConfig: ProjectConfig, accountId: number, skipValidation: boolean, profile: string | undefined): Promise<IntermediateRepresentation | undefined>;
|
|
24
25
|
export {};
|
package/lib/projects/upload.js
CHANGED
package/package.json
CHANGED