@hubspot/cli 7.8.0-experimental.0 → 7.8.1-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +0 -2
- package/commands/getStarted.d.ts +1 -1
- package/commands/getStarted.js +64 -16
- package/commands/mcp/setup.js +8 -0
- package/commands/project/dev/unifiedFlow.js +1 -1
- package/commands/project/migrate.js +30 -21
- package/lang/en.d.ts +4 -1
- package/lang/en.js +5 -1
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/app/__tests__/migrate.test.js +14 -51
- package/lib/app/migrate.d.ts +2 -8
- package/lib/app/migrate.js +5 -80
- package/lib/constants.d.ts +3 -0
- package/lib/constants.js +3 -0
- package/lib/dependencyManagement.d.ts +0 -5
- package/lib/dependencyManagement.js +0 -9
- package/lib/hasFeature.js +6 -0
- package/lib/links.d.ts +1 -0
- package/lib/links.js +10 -3
- package/lib/mcp/setup.js +1 -1
- package/lib/projects/create/v3.js +3 -2
- package/lib/projects/localDev/helpers/project.d.ts +2 -2
- package/lib/projects/localDev/helpers/project.js +5 -6
- package/lib/theme/__tests__/migrate.test.d.ts +1 -0
- package/lib/theme/__tests__/migrate.test.js +233 -0
- package/lib/theme/migrate.d.ts +13 -0
- package/lib/theme/migrate.js +90 -0
- package/lib/ui/SpinniesManager.js +105 -8
- package/lib/usageTracking.js +2 -2
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -2
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +2 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +5 -5
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/DocFetchTool.js +2 -2
- package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocsSearchTool.js +7 -7
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +11 -5
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
- package/mcp-server/tools/project/UploadProjectTools.js +2 -2
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
- package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +9 -3
- package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
- package/mcp-server/utils/cliConfig.d.ts +1 -0
- package/mcp-server/utils/cliConfig.js +12 -0
- package/package.json +2 -7
- package/ui/components/HorizontalSelectPrompt.js +1 -1
- package/commands/getStartedV2.d.ts +0 -9
- package/commands/getStartedV2.js +0 -39
- package/ui/components/Ascii.d.ts +0 -10
- package/ui/components/Ascii.js +0 -11
- package/ui/views/GetStarted.d.ts +0 -7
- package/ui/views/GetStarted.js +0 -157
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { getProjectThemeDetails, migrateThemes, } from '@hubspot/project-parsing-lib';
|
|
2
|
+
import { confirmPrompt } from '../../prompts/promptUtils.js';
|
|
3
|
+
import { writeProjectConfig, } from '../../projects/config.js';
|
|
4
|
+
import { ensureProjectExists } from '../../projects/ensureProjectExists.js';
|
|
5
|
+
import { useV3Api } from '../../projects/platformVersion.js';
|
|
6
|
+
import { fetchMigrationApps } from '../../app/migrate.js';
|
|
7
|
+
import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration, migrateThemes2025_2, } from '../migrate.js';
|
|
8
|
+
import { lib } from '../../../lang/en.js';
|
|
9
|
+
vi.mock('@hubspot/local-dev-lib/logger');
|
|
10
|
+
vi.mock('@hubspot/project-parsing-lib');
|
|
11
|
+
vi.mock('../../prompts/promptUtils');
|
|
12
|
+
vi.mock('../../projects/config');
|
|
13
|
+
vi.mock('../../projects/ensureProjectExists');
|
|
14
|
+
vi.mock('../../projects/platformVersion');
|
|
15
|
+
vi.mock('../../app/migrate');
|
|
16
|
+
const mockedGetProjectThemeDetails = getProjectThemeDetails;
|
|
17
|
+
const mockedMigrateThemes = migrateThemes;
|
|
18
|
+
const mockedConfirmPrompt = confirmPrompt;
|
|
19
|
+
const mockedWriteProjectConfig = writeProjectConfig;
|
|
20
|
+
const mockedEnsureProjectExists = ensureProjectExists;
|
|
21
|
+
const mockedUseV3Api = useV3Api;
|
|
22
|
+
const mockedFetchMigrationApps = fetchMigrationApps;
|
|
23
|
+
const ACCOUNT_ID = 123;
|
|
24
|
+
const PROJECT_NAME = 'Test Project';
|
|
25
|
+
const PLATFORM_VERSION = '2025.2';
|
|
26
|
+
const MOCK_PROJECT_DIR = '/mock/project/dir';
|
|
27
|
+
const createLoadedProjectConfig = (name) => ({
|
|
28
|
+
projectConfig: { name, srcDir: 'src', platformVersion: '2024.1' },
|
|
29
|
+
projectDir: MOCK_PROJECT_DIR,
|
|
30
|
+
});
|
|
31
|
+
describe('lib/theme/migrate', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockedUseV3Api.mockReturnValue(false);
|
|
34
|
+
});
|
|
35
|
+
describe('getHasMigratableThemes', () => {
|
|
36
|
+
it('should return false when no projectConfig is provided', async () => {
|
|
37
|
+
const result = await getHasMigratableThemes();
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
hasMigratableThemes: false,
|
|
40
|
+
migratableThemesCount: 0,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('should return false when projectConfig is missing required properties', async () => {
|
|
44
|
+
const invalidProjectConfig = {
|
|
45
|
+
projectConfig: { name: undefined, srcDir: 'src' },
|
|
46
|
+
projectDir: undefined,
|
|
47
|
+
};
|
|
48
|
+
const result = await getHasMigratableThemes(invalidProjectConfig);
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
hasMigratableThemes: false,
|
|
51
|
+
migratableThemesCount: 0,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('should return true when there are legacy themes', async () => {
|
|
55
|
+
mockedGetProjectThemeDetails.mockResolvedValue({
|
|
56
|
+
legacyThemeDetails: [
|
|
57
|
+
{
|
|
58
|
+
configFilepath: 'src/theme.json',
|
|
59
|
+
themePath: 'src/theme',
|
|
60
|
+
themeConfig: {
|
|
61
|
+
secret_names: ['my-secret'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
legacyReactThemeDetails: [],
|
|
66
|
+
});
|
|
67
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
68
|
+
const result = await getHasMigratableThemes(projectConfig);
|
|
69
|
+
expect(result).toEqual({
|
|
70
|
+
hasMigratableThemes: true,
|
|
71
|
+
migratableThemesCount: 1,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it('should return true when there are legacy React themes', async () => {
|
|
75
|
+
mockedGetProjectThemeDetails.mockResolvedValue({
|
|
76
|
+
legacyThemeDetails: [],
|
|
77
|
+
legacyReactThemeDetails: [
|
|
78
|
+
{
|
|
79
|
+
configFilepath: 'src/react-theme.json',
|
|
80
|
+
themePath: 'src/react-theme',
|
|
81
|
+
themeConfig: {
|
|
82
|
+
secretNames: ['my-secret'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
88
|
+
const result = await getHasMigratableThemes(projectConfig);
|
|
89
|
+
expect(result).toEqual({
|
|
90
|
+
hasMigratableThemes: true,
|
|
91
|
+
migratableThemesCount: 1,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('should return true when there are both legacy and React themes', async () => {
|
|
95
|
+
mockedGetProjectThemeDetails.mockResolvedValue({
|
|
96
|
+
legacyThemeDetails: [
|
|
97
|
+
{
|
|
98
|
+
configFilepath: 'src/theme.json',
|
|
99
|
+
themePath: 'src/theme',
|
|
100
|
+
themeConfig: {
|
|
101
|
+
secret_names: ['my-secret'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
legacyReactThemeDetails: [
|
|
106
|
+
{
|
|
107
|
+
configFilepath: 'src/react-theme.json',
|
|
108
|
+
themePath: 'src/react-theme',
|
|
109
|
+
themeConfig: {
|
|
110
|
+
secretNames: ['my-secret'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
116
|
+
const result = await getHasMigratableThemes(projectConfig);
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
hasMigratableThemes: true,
|
|
119
|
+
migratableThemesCount: 2,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('validateMigrationAppsAndThemes', () => {
|
|
124
|
+
it('should throw an error when themes are already migrated (v3 API)', async () => {
|
|
125
|
+
mockedUseV3Api.mockReturnValue(true);
|
|
126
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
127
|
+
await expect(validateMigrationAppsAndThemes(0, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAlreadyMigrated);
|
|
128
|
+
});
|
|
129
|
+
it('should throw an error when apps and themes are both present', async () => {
|
|
130
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
131
|
+
await expect(validateMigrationAppsAndThemes(1, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
132
|
+
});
|
|
133
|
+
it('should throw an error when no project config is provided', async () => {
|
|
134
|
+
await expect(validateMigrationAppsAndThemes(0)).rejects.toThrow(lib.migrate.errors.project.noProjectForThemesMigration);
|
|
135
|
+
});
|
|
136
|
+
it('should not throw an error when validation passes', async () => {
|
|
137
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
138
|
+
await expect(validateMigrationAppsAndThemes(0, projectConfig)).resolves.not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('handleThemesMigration', () => {
|
|
142
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
mockedMigrateThemes.mockResolvedValue({
|
|
145
|
+
migrated: true,
|
|
146
|
+
failureReason: undefined,
|
|
147
|
+
legacyThemeDetails: [],
|
|
148
|
+
legacyReactThemeDetails: [],
|
|
149
|
+
});
|
|
150
|
+
mockedWriteProjectConfig.mockReturnValue(true);
|
|
151
|
+
});
|
|
152
|
+
it('should throw an error when project config is invalid', async () => {
|
|
153
|
+
const invalidProjectConfig = {
|
|
154
|
+
projectConfig: { name: PROJECT_NAME, srcDir: undefined },
|
|
155
|
+
projectDir: undefined,
|
|
156
|
+
};
|
|
157
|
+
await expect(handleThemesMigration(invalidProjectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
|
|
158
|
+
});
|
|
159
|
+
it('should successfully migrate themes and update project config', async () => {
|
|
160
|
+
await handleThemesMigration(projectConfig, PLATFORM_VERSION);
|
|
161
|
+
expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
|
|
162
|
+
expect(mockedWriteProjectConfig).toHaveBeenCalledWith(`${MOCK_PROJECT_DIR}/hsproject.json`, expect.objectContaining({
|
|
163
|
+
platformVersion: PLATFORM_VERSION,
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
it('should throw an error when theme migration fails', async () => {
|
|
167
|
+
mockedMigrateThemes.mockResolvedValue({
|
|
168
|
+
migrated: false,
|
|
169
|
+
failureReason: 'Migration failed',
|
|
170
|
+
legacyThemeDetails: [],
|
|
171
|
+
legacyReactThemeDetails: [],
|
|
172
|
+
});
|
|
173
|
+
await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow('Migration failed');
|
|
174
|
+
});
|
|
175
|
+
it('should throw an error when project config write fails', async () => {
|
|
176
|
+
mockedWriteProjectConfig.mockReturnValue(false);
|
|
177
|
+
await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.failedToUpdateProjectConfig);
|
|
178
|
+
});
|
|
179
|
+
it('should throw an error when migrateThemes throws an exception', async () => {
|
|
180
|
+
mockedMigrateThemes.mockRejectedValue(new Error('Unexpected error'));
|
|
181
|
+
await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.failedToMigrateThemes);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('migrateThemes2025_2', () => {
|
|
185
|
+
const options = {
|
|
186
|
+
platformVersion: PLATFORM_VERSION,
|
|
187
|
+
};
|
|
188
|
+
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
189
|
+
const themeCount = 2;
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
mockedEnsureProjectExists.mockResolvedValue({ projectExists: true });
|
|
192
|
+
mockedFetchMigrationApps.mockResolvedValue({
|
|
193
|
+
migratableApps: [],
|
|
194
|
+
unmigratableApps: [],
|
|
195
|
+
});
|
|
196
|
+
mockedConfirmPrompt.mockResolvedValue(true);
|
|
197
|
+
mockedMigrateThemes.mockResolvedValue({
|
|
198
|
+
migrated: true,
|
|
199
|
+
failureReason: undefined,
|
|
200
|
+
legacyThemeDetails: [],
|
|
201
|
+
legacyReactThemeDetails: [],
|
|
202
|
+
});
|
|
203
|
+
mockedWriteProjectConfig.mockReturnValue(true);
|
|
204
|
+
});
|
|
205
|
+
it('should throw an error when project config is invalid', async () => {
|
|
206
|
+
const invalidProjectConfig = {
|
|
207
|
+
projectConfig: undefined,
|
|
208
|
+
projectDir: MOCK_PROJECT_DIR,
|
|
209
|
+
};
|
|
210
|
+
await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, invalidProjectConfig)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
|
|
211
|
+
});
|
|
212
|
+
it('should throw an error when project does not exist', async () => {
|
|
213
|
+
mockedEnsureProjectExists.mockResolvedValue({ projectExists: false });
|
|
214
|
+
await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig)).rejects.toThrow(lib.migrate.errors.project.doesNotExist(ACCOUNT_ID));
|
|
215
|
+
});
|
|
216
|
+
it('should proceed with migration when user confirms', async () => {
|
|
217
|
+
await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
218
|
+
expect(mockedFetchMigrationApps).toHaveBeenCalledWith(ACCOUNT_ID, PLATFORM_VERSION, projectConfig);
|
|
219
|
+
expect(mockedConfirmPrompt).toHaveBeenCalledWith(lib.migrate.prompt.proceed, { defaultAnswer: false });
|
|
220
|
+
expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
|
|
221
|
+
});
|
|
222
|
+
it('should exit without migrating when user cancels', async () => {
|
|
223
|
+
mockedConfirmPrompt.mockResolvedValue(false);
|
|
224
|
+
await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
225
|
+
expect(mockedMigrateThemes).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
it('should validate migration apps and themes', async () => {
|
|
228
|
+
await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
229
|
+
// The validation is called internally, so we verify it through the error handling
|
|
230
|
+
expect(mockedFetchMigrationApps).toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ArgumentsCamelCase } from 'yargs';
|
|
2
|
+
import { LoadedProjectConfig } from '../projects/config.js';
|
|
3
|
+
import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs } from '../../types/Yargs.js';
|
|
4
|
+
export type MigrateThemesArgs = CommonArgs & AccountArgs & EnvironmentArgs & ConfigArgs & {
|
|
5
|
+
platformVersion: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function getHasMigratableThemes(projectConfig?: LoadedProjectConfig): Promise<{
|
|
8
|
+
hasMigratableThemes: boolean;
|
|
9
|
+
migratableThemesCount: number;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function validateMigrationAppsAndThemes(hasApps: number, projectConfig?: LoadedProjectConfig): Promise<void>;
|
|
12
|
+
export declare function handleThemesMigration(projectConfig: LoadedProjectConfig, platformVersion: string): Promise<void>;
|
|
13
|
+
export declare function migrateThemes2025_2(derivedAccountId: number, options: ArgumentsCamelCase<MigrateThemesArgs>, themeCount: number, projectConfig: LoadedProjectConfig): Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { migrateThemes, getProjectThemeDetails, } from '@hubspot/project-parsing-lib';
|
|
3
|
+
import { writeProjectConfig } from '../projects/config.js';
|
|
4
|
+
import { ensureProjectExists } from '../projects/ensureProjectExists.js';
|
|
5
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
6
|
+
import { lib } from '../../lang/en.js';
|
|
7
|
+
import { PROJECT_CONFIG_FILE } from '../constants.js';
|
|
8
|
+
import { uiLogger } from '../ui/logger.js';
|
|
9
|
+
import { debugError } from '../errorHandlers/index.js';
|
|
10
|
+
import { useV3Api } from '../projects/platformVersion.js';
|
|
11
|
+
import { confirmPrompt } from '../prompts/promptUtils.js';
|
|
12
|
+
import { fetchMigrationApps } from '../app/migrate.js';
|
|
13
|
+
export async function getHasMigratableThemes(projectConfig) {
|
|
14
|
+
if (!projectConfig?.projectConfig?.name || !projectConfig?.projectDir) {
|
|
15
|
+
return { hasMigratableThemes: false, migratableThemesCount: 0 };
|
|
16
|
+
}
|
|
17
|
+
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
18
|
+
const { legacyThemeDetails, legacyReactThemeDetails } = await getProjectThemeDetails(projectSrcDir);
|
|
19
|
+
return {
|
|
20
|
+
hasMigratableThemes: legacyThemeDetails.length > 0 || legacyReactThemeDetails.length > 0,
|
|
21
|
+
migratableThemesCount: legacyThemeDetails.length + legacyReactThemeDetails.length,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function validateMigrationAppsAndThemes(hasApps, projectConfig) {
|
|
25
|
+
if (useV3Api(projectConfig?.projectConfig?.platformVersion)) {
|
|
26
|
+
throw new Error(lib.migrate.errors.project.themesAlreadyMigrated);
|
|
27
|
+
}
|
|
28
|
+
if (hasApps > 0 && projectConfig) {
|
|
29
|
+
throw new Error(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
30
|
+
}
|
|
31
|
+
if (!projectConfig) {
|
|
32
|
+
throw new Error(lib.migrate.errors.project.noProjectForThemesMigration);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function handleThemesMigration(projectConfig, platformVersion) {
|
|
36
|
+
if (!projectConfig?.projectDir || !projectConfig?.projectConfig?.srcDir) {
|
|
37
|
+
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
38
|
+
}
|
|
39
|
+
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
40
|
+
let migrated = false;
|
|
41
|
+
let failureReason;
|
|
42
|
+
try {
|
|
43
|
+
const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
44
|
+
migrated = migrationResult.migrated;
|
|
45
|
+
failureReason = migrationResult.failureReason;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
debugError(error);
|
|
49
|
+
throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
|
|
50
|
+
}
|
|
51
|
+
if (!migrated) {
|
|
52
|
+
throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
|
|
53
|
+
}
|
|
54
|
+
const newProjectConfig = { ...projectConfig.projectConfig };
|
|
55
|
+
newProjectConfig.platformVersion = platformVersion;
|
|
56
|
+
const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
|
|
57
|
+
const success = writeProjectConfig(projectConfigPath, newProjectConfig);
|
|
58
|
+
if (!success) {
|
|
59
|
+
throw new Error(lib.migrate.errors.project.failedToUpdateProjectConfig);
|
|
60
|
+
}
|
|
61
|
+
uiLogger.log('');
|
|
62
|
+
uiLogger.log(lib.migrate.success.themesMigrationSuccess(platformVersion));
|
|
63
|
+
}
|
|
64
|
+
export async function migrateThemes2025_2(derivedAccountId, options, themeCount, projectConfig) {
|
|
65
|
+
SpinniesManager.init();
|
|
66
|
+
if (!projectConfig?.projectConfig || !projectConfig?.projectDir) {
|
|
67
|
+
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
68
|
+
}
|
|
69
|
+
const { projectExists } = await ensureProjectExists(derivedAccountId, projectConfig.projectConfig.name, { allowCreate: false, noLogs: true });
|
|
70
|
+
if (!projectExists) {
|
|
71
|
+
throw new Error(lib.migrate.errors.project.doesNotExist(derivedAccountId));
|
|
72
|
+
}
|
|
73
|
+
SpinniesManager.add('checkingForMigratableComponents', {
|
|
74
|
+
text: lib.migrate.spinners.checkingForMigratableComponents,
|
|
75
|
+
});
|
|
76
|
+
const { migratableApps, unmigratableApps } = await fetchMigrationApps(derivedAccountId, options.platformVersion, projectConfig);
|
|
77
|
+
const hasApps = [...migratableApps, ...unmigratableApps].length;
|
|
78
|
+
SpinniesManager.remove('checkingForMigratableComponents');
|
|
79
|
+
await validateMigrationAppsAndThemes(hasApps, projectConfig);
|
|
80
|
+
uiLogger.log(lib.migrate.prompt.themesMigration(themeCount));
|
|
81
|
+
const proceed = await confirmPrompt(lib.migrate.prompt.proceed, {
|
|
82
|
+
defaultAnswer: false,
|
|
83
|
+
});
|
|
84
|
+
if (proceed) {
|
|
85
|
+
await handleThemesMigration(projectConfig, options.platformVersion);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
uiLogger.log(lib.migrate.exitWithoutMigrating);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -7,8 +7,10 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
7
7
|
|
|
8
8
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
9
|
**/
|
|
10
|
+
import readline from 'readline';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
|
-
import
|
|
12
|
+
import cliCursor from 'cli-cursor';
|
|
13
|
+
import { breakText, cleanStream, colorOptions, getLinesLength, purgeSpinnerOptions, purgeSpinnersOptions, SPINNERS, terminalSupportsUnicode, writeStream, prefixOptions, } from './spinniesUtils.js';
|
|
12
14
|
function safeColor(text, color) {
|
|
13
15
|
const chalkFn = chalk[color];
|
|
14
16
|
if (typeof chalkFn === 'function') {
|
|
@@ -34,10 +36,14 @@ class SpinniesManager {
|
|
|
34
36
|
succeedColor: 'green',
|
|
35
37
|
failColor: 'red',
|
|
36
38
|
spinner: terminalSupportsUnicode() ? SPINNERS.dots : SPINNERS.dashes,
|
|
37
|
-
disableSpins:
|
|
39
|
+
disableSpins: false,
|
|
38
40
|
...purgeSpinnersOptions(options),
|
|
39
41
|
};
|
|
40
|
-
this.spin =
|
|
42
|
+
this.spin =
|
|
43
|
+
!this.options.disableSpins &&
|
|
44
|
+
!process.env.CI &&
|
|
45
|
+
process.stderr &&
|
|
46
|
+
process.stderr.isTTY;
|
|
41
47
|
if (!this.hasAnySpinners()) {
|
|
42
48
|
this.resetState();
|
|
43
49
|
}
|
|
@@ -135,7 +141,22 @@ class SpinniesManager {
|
|
|
135
141
|
status = status || 'spinning';
|
|
136
142
|
this.spinners[name] = { ...this.spinners[name], ...options, status };
|
|
137
143
|
}
|
|
138
|
-
updateSpinnerState() {
|
|
144
|
+
updateSpinnerState() {
|
|
145
|
+
if (this.spin) {
|
|
146
|
+
if (this.currentInterval) {
|
|
147
|
+
clearInterval(this.currentInterval);
|
|
148
|
+
}
|
|
149
|
+
this.currentInterval = this.loopStream();
|
|
150
|
+
if (!this.isCursorHidden) {
|
|
151
|
+
cliCursor.hide();
|
|
152
|
+
}
|
|
153
|
+
this.isCursorHidden = true;
|
|
154
|
+
this.checkIfActiveSpinners();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.setRawStreamOutput();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
139
160
|
loopStream() {
|
|
140
161
|
const frames = this.options.spinner?.frames || SPINNERS.dots.frames;
|
|
141
162
|
const interval = this.options.spinner?.interval || SPINNERS.dots.interval;
|
|
@@ -147,10 +168,86 @@ class SpinniesManager {
|
|
|
147
168
|
: ++this.currentFrameIndex;
|
|
148
169
|
}, interval);
|
|
149
170
|
}
|
|
150
|
-
setStreamOutput(frame = '') {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
setStreamOutput(frame = '') {
|
|
172
|
+
let output = '';
|
|
173
|
+
const linesLength = [];
|
|
174
|
+
const hasActiveSpinners = this.hasActiveSpinners();
|
|
175
|
+
Object.values(this.spinners).forEach(spinner => {
|
|
176
|
+
let { text } = spinner;
|
|
177
|
+
const { status, color, spinnerColor, succeedColor, failColor, indent = 0, succeedPrefix = prefixOptions(this.options).succeedPrefix, failPrefix = prefixOptions(this.options).failPrefix, } = spinner;
|
|
178
|
+
let line;
|
|
179
|
+
let prefixLength = indent;
|
|
180
|
+
text = text ?? '';
|
|
181
|
+
if (status === 'spinning') {
|
|
182
|
+
prefixLength += frame.length + 1;
|
|
183
|
+
text = breakText(text, prefixLength);
|
|
184
|
+
const colorizedFrame = safeColor(frame, spinnerColor);
|
|
185
|
+
const colorizedText = safeColor(text, color);
|
|
186
|
+
line = `${colorizedFrame} ${colorizedText}`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (status === 'succeed') {
|
|
190
|
+
prefixLength += succeedPrefix.length + 1;
|
|
191
|
+
if (hasActiveSpinners) {
|
|
192
|
+
text = breakText(text, prefixLength);
|
|
193
|
+
}
|
|
194
|
+
const colorizedText = safeColor(text, succeedColor);
|
|
195
|
+
line = `${chalk.green(succeedPrefix)} ${colorizedText}`;
|
|
196
|
+
}
|
|
197
|
+
else if (status === 'fail') {
|
|
198
|
+
prefixLength += failPrefix.length + 1;
|
|
199
|
+
if (hasActiveSpinners) {
|
|
200
|
+
text = breakText(text, prefixLength);
|
|
201
|
+
}
|
|
202
|
+
const colorizedText = safeColor(text, failColor);
|
|
203
|
+
line = `${chalk.red(failPrefix)} ${colorizedText}`;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (hasActiveSpinners) {
|
|
207
|
+
text = breakText(text, prefixLength);
|
|
208
|
+
}
|
|
209
|
+
line = safeColor(text, color);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
linesLength.push(...getLinesLength(text, prefixLength));
|
|
213
|
+
output += indent ? `${' '.repeat(indent)}${line}\n` : `${line}\n`;
|
|
214
|
+
});
|
|
215
|
+
if (!hasActiveSpinners) {
|
|
216
|
+
readline.clearScreenDown(this.stream);
|
|
217
|
+
}
|
|
218
|
+
writeStream(this.stream, output, linesLength);
|
|
219
|
+
if (hasActiveSpinners) {
|
|
220
|
+
cleanStream(this.stream, linesLength);
|
|
221
|
+
}
|
|
222
|
+
this.lineCount = linesLength.length;
|
|
223
|
+
}
|
|
224
|
+
setRawStreamOutput() {
|
|
225
|
+
Object.values(this.spinners).forEach(i => {
|
|
226
|
+
process.stderr.write(`- ${i.text}\n`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
checkIfActiveSpinners() {
|
|
230
|
+
if (!this.hasActiveSpinners()) {
|
|
231
|
+
if (this.spin) {
|
|
232
|
+
this.setStreamOutput();
|
|
233
|
+
readline.moveCursor(this.stream, 0, this.lineCount);
|
|
234
|
+
if (this.currentInterval) {
|
|
235
|
+
clearInterval(this.currentInterval);
|
|
236
|
+
}
|
|
237
|
+
this.isCursorHidden = false;
|
|
238
|
+
cliCursor.show();
|
|
239
|
+
}
|
|
240
|
+
this.spinners = {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
bindSigint() {
|
|
244
|
+
process.removeAllListeners('SIGINT');
|
|
245
|
+
process.on('SIGINT', () => {
|
|
246
|
+
cliCursor.show();
|
|
247
|
+
readline.moveCursor(process.stderr, 0, this.lineCount);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
154
251
|
}
|
|
155
252
|
const toExport = new SpinniesManager();
|
|
156
253
|
export default toExport;
|
package/lib/usageTracking.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function trackCommandUsage(command, meta = {}, accountId) {
|
|
|
44
44
|
action: 'cli-command',
|
|
45
45
|
command,
|
|
46
46
|
authType,
|
|
47
|
-
|
|
47
|
+
meta,
|
|
48
48
|
accountId,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
@@ -133,12 +133,12 @@ async function trackCliInteraction({ action, accountId, command, authType, meta
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
try {
|
|
136
|
+
logger.debug('Sent usage tracking command event: %o', usageTrackingEvent);
|
|
136
137
|
return trackUsage('cli-interaction', EventClass.INTERACTION, usageTrackingEvent, accountId);
|
|
137
138
|
}
|
|
138
139
|
catch (error) {
|
|
139
140
|
debugError(error);
|
|
140
141
|
}
|
|
141
|
-
logger.debug('Sent usage tracking command event: %o', usageTrackingEvent);
|
|
142
142
|
}
|
|
143
143
|
catch (e) {
|
|
144
144
|
debugError(e);
|
|
@@ -31,7 +31,7 @@ const inputSchema = {
|
|
|
31
31
|
};
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
33
33
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
34
|
-
const toolName = 'create-
|
|
34
|
+
const toolName = 'create-cms-function';
|
|
35
35
|
export class HsCreateFunctionTool extends Tool {
|
|
36
36
|
constructor(mcpServer) {
|
|
37
37
|
super(mcpServer);
|
|
@@ -47,7 +47,7 @@ const inputSchema = {
|
|
|
47
47
|
};
|
|
48
48
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
49
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
50
|
-
const toolName = 'create-
|
|
50
|
+
const toolName = 'create-cms-module';
|
|
51
51
|
export class HsCreateModuleTool extends Tool {
|
|
52
52
|
constructor(mcpServer) {
|
|
53
53
|
super(mcpServer);
|
|
@@ -23,7 +23,7 @@ const inputSchema = {
|
|
|
23
23
|
};
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
25
25
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
26
|
-
const toolName = 'create-
|
|
26
|
+
const toolName = 'create-cms-template';
|
|
27
27
|
export class HsCreateTemplateTool extends Tool {
|
|
28
28
|
constructor(mcpServer) {
|
|
29
29
|
super(mcpServer);
|
|
@@ -26,7 +26,7 @@ const inputSchema = {
|
|
|
26
26
|
};
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
28
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
29
|
-
const toolName = 'get-
|
|
29
|
+
const toolName = 'get-cms-serverless-function-logs';
|
|
30
30
|
export class HsFunctionLogsTool extends Tool {
|
|
31
31
|
constructor(mcpServer) {
|
|
32
32
|
super(mcpServer);
|
|
@@ -69,7 +69,7 @@ export class HsFunctionLogsTool extends Tool {
|
|
|
69
69
|
register() {
|
|
70
70
|
return this.mcpServer.registerTool(toolName, {
|
|
71
71
|
title: 'Get HubSpot CMS serverless function logs for an endpoint',
|
|
72
|
-
description: 'Retrieve logs for HubSpot CMS serverless functions. Use this tool to help debug issues with serverless functions by reading the production logs. Supports various options like latest, compact, and limiting results. Use after listing functions with list-
|
|
72
|
+
description: 'Retrieve logs for HubSpot CMS serverless functions. Use this tool to help debug issues with serverless functions by reading the production logs. Supports various options like latest, compact, and limiting results. Use after listing functions with list-cms-serverless-functions to get the endpoint path.',
|
|
73
73
|
inputSchema,
|
|
74
74
|
}, this.handler);
|
|
75
75
|
}
|
|
@@ -18,7 +18,7 @@ const inputSchema = {
|
|
|
18
18
|
};
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
20
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
21
|
-
const toolName = 'list-
|
|
21
|
+
const toolName = 'list-cms-serverless-functions';
|
|
22
22
|
export class HsListFunctionsTool extends Tool {
|
|
23
23
|
constructor(mcpServer) {
|
|
24
24
|
super(mcpServer);
|
|
@@ -18,7 +18,7 @@ const inputSchema = {
|
|
|
18
18
|
};
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
20
|
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
21
|
-
const toolName = 'list-
|
|
21
|
+
const toolName = 'list-cms-remote-contents';
|
|
22
22
|
export class HsListTool extends Tool {
|
|
23
23
|
constructor(mcpServer) {
|
|
24
24
|
super(mcpServer);
|
|
@@ -28,7 +28,7 @@ describe('HsCreateFunctionTool', () => {
|
|
|
28
28
|
describe('register', () => {
|
|
29
29
|
it('should register the tool with the MCP server', () => {
|
|
30
30
|
const result = tool.register();
|
|
31
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-
|
|
31
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-cms-function', {
|
|
32
32
|
title: 'Create HubSpot CMS Serverless Function',
|
|
33
33
|
description: `Creates a new HubSpot CMS serverless function using the hs create function command. Functions can be created non-interactively by specifying functionsFolder, filename, and endpointPath. Supports all HTTP methods (${HTTP_METHODS.join(', ')}).`,
|
|
34
34
|
inputSchema: expect.any(Object),
|
|
@@ -27,7 +27,7 @@ describe('HsCreateModuleTool', () => {
|
|
|
27
27
|
describe('register', () => {
|
|
28
28
|
it('should register the tool with the MCP server', () => {
|
|
29
29
|
const result = tool.register();
|
|
30
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-cms-module', {
|
|
31
31
|
title: 'Create HubSpot CMS Module',
|
|
32
32
|
description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
|
|
33
33
|
inputSchema: expect.any(Object),
|
|
@@ -28,7 +28,7 @@ describe('HsCreateTemplateTool', () => {
|
|
|
28
28
|
describe('register', () => {
|
|
29
29
|
it('should register the tool with the MCP server', () => {
|
|
30
30
|
const result = tool.register();
|
|
31
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-
|
|
31
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-cms-template', {
|
|
32
32
|
title: 'Create HubSpot CMS Template',
|
|
33
33
|
description: `Creates a new HubSpot CMS template using the hs create template command. Templates can be created non-interactively by specifying templateType. Supports all template types including: ${TEMPLATE_TYPES.join(', ')}.`,
|
|
34
34
|
inputSchema: expect.any(Object),
|
|
@@ -27,9 +27,9 @@ describe('HsFunctionLogsTool', () => {
|
|
|
27
27
|
describe('register', () => {
|
|
28
28
|
it('should register the tool with the MCP server', () => {
|
|
29
29
|
const result = tool.register();
|
|
30
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('get-cms-serverless-function-logs', expect.objectContaining({
|
|
31
31
|
title: 'Get HubSpot CMS serverless function logs for an endpoint',
|
|
32
|
-
description: 'Retrieve logs for HubSpot CMS serverless functions. Use this tool to help debug issues with serverless functions by reading the production logs. Supports various options like latest, compact, and limiting results. Use after listing functions with list-
|
|
32
|
+
description: 'Retrieve logs for HubSpot CMS serverless functions. Use this tool to help debug issues with serverless functions by reading the production logs. Supports various options like latest, compact, and limiting results. Use after listing functions with list-cms-serverless-functions to get the endpoint path.',
|
|
33
33
|
inputSchema: expect.any(Object),
|
|
34
34
|
}), expect.any(Function));
|
|
35
35
|
expect(result).toBe(mockRegisteredTool);
|
|
@@ -27,7 +27,7 @@ describe('HsListFunctionsTool', () => {
|
|
|
27
27
|
describe('register', () => {
|
|
28
28
|
it('should register the tool with the MCP server', () => {
|
|
29
29
|
const result = tool.register();
|
|
30
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('list-
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('list-cms-serverless-functions', {
|
|
31
31
|
title: 'List HubSpot CMS Serverless Functions',
|
|
32
32
|
description: 'Get a list of all serverless functions deployed in a HubSpot portal/account. Shows function routes, HTTP methods, secrets, and timestamps.',
|
|
33
33
|
inputSchema: expect.any(Object),
|
|
@@ -27,7 +27,7 @@ describe('HsListTool', () => {
|
|
|
27
27
|
describe('register', () => {
|
|
28
28
|
it('should register the tool with the MCP server', () => {
|
|
29
29
|
const result = tool.register();
|
|
30
|
-
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('list-
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('list-cms-remote-contents', {
|
|
31
31
|
title: 'List HubSpot CMS Directory Contents',
|
|
32
32
|
description: 'List remote contents of a HubSpot CMS directory.',
|
|
33
33
|
inputSchema: expect.any(Object),
|