@hubspot/cli 7.7.35-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/commands/mcp/setup.js +8 -0
- package/commands/project/create.js +1 -1
- package/commands/project/migrate.js +26 -7
- package/lang/en.d.ts +3 -0
- package/lang/en.js +4 -0
- 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 +2 -0
- package/lib/constants.js +2 -0
- package/lib/links.d.ts +1 -0
- package/lib/links.js +10 -3
- 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/usageTracking.js +2 -2
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +2 -2
- package/mcp-server/tools/project/CreateProjectTool.d.ts +2 -2
- package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocsSearchTool.js +5 -5
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +10 -4
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +12 -10
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +8 -7
- 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 -2
package/commands/mcp/setup.js
CHANGED
|
@@ -4,9 +4,17 @@ import { commands } from '../../lang/en.js';
|
|
|
4
4
|
import { uiLogger } from '../../lib/ui/logger.js';
|
|
5
5
|
import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
|
|
6
6
|
import { trackCommandUsage } from '../../lib/usageTracking.js';
|
|
7
|
+
import { hasFeature } from '../../lib/hasFeature.js';
|
|
8
|
+
import { FEATURES } from '../../lib/constants.js';
|
|
7
9
|
const command = ['setup', 'update'];
|
|
8
10
|
const describe = undefined; // Leave hidden for now
|
|
9
11
|
async function handler(args) {
|
|
12
|
+
const { derivedAccountId } = args;
|
|
13
|
+
const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
|
|
14
|
+
if (!hasMcpAccess) {
|
|
15
|
+
uiLogger.error(commands.mcp.setup.errors.needsMcpAccess(derivedAccountId));
|
|
16
|
+
process.exit(EXIT_CODES.ERROR);
|
|
17
|
+
}
|
|
10
18
|
try {
|
|
11
19
|
await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
12
20
|
}
|
|
@@ -125,7 +125,7 @@ function projectCreateBuilder(yargs) {
|
|
|
125
125
|
hidden: true,
|
|
126
126
|
type: 'string',
|
|
127
127
|
choices: [v2023_2, v2025_1, v2025_2],
|
|
128
|
-
default:
|
|
128
|
+
default: v2023_2,
|
|
129
129
|
},
|
|
130
130
|
'project-base': {
|
|
131
131
|
describe: commands.project.create.options.projectBase.describe,
|
|
@@ -8,6 +8,9 @@ import { uiCommandReference } from '../../lib/ui/index.js';
|
|
|
8
8
|
import { commands, lib } from '../../lang/en.js';
|
|
9
9
|
import { uiLogger } from '../../lib/ui/logger.js';
|
|
10
10
|
import { logInBox } from '../../lib/ui/boxen.js';
|
|
11
|
+
import { getHasMigratableThemes, migrateThemes2025_2, } from '../../lib/theme/migrate.js';
|
|
12
|
+
import { hasFeature } from '../../lib/hasFeature.js';
|
|
13
|
+
import { FEATURES } from '../../lib/constants.js';
|
|
11
14
|
const { v2025_2 } = PLATFORM_VERSIONS;
|
|
12
15
|
const command = 'migrate';
|
|
13
16
|
const describe = undefined; // commands.project.migrate.describe
|
|
@@ -26,13 +29,29 @@ async function handler(args) {
|
|
|
26
29
|
}
|
|
27
30
|
const { derivedAccountId } = args;
|
|
28
31
|
try {
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes(projectConfig);
|
|
33
|
+
if (hasMigratableThemes) {
|
|
34
|
+
const hasThemeMigrationAccess = await hasFeature(derivedAccountId, FEATURES.THEME_MIGRATION_2025_2);
|
|
35
|
+
if (!hasThemeMigrationAccess) {
|
|
36
|
+
uiLogger.error(commands.project.migrate.errors.noThemeMigrationAccess(derivedAccountId));
|
|
37
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
38
|
+
}
|
|
39
|
+
await migrateThemes2025_2(derivedAccountId, {
|
|
40
|
+
...args,
|
|
41
|
+
platformVersion: unstable
|
|
42
|
+
? PLATFORM_VERSIONS.unstable
|
|
43
|
+
: platformVersion,
|
|
44
|
+
}, migratableThemesCount, projectConfig);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await migrateApp2025_2(derivedAccountId, {
|
|
48
|
+
...args,
|
|
49
|
+
name: projectConfig?.projectConfig?.name,
|
|
50
|
+
platformVersion: unstable
|
|
51
|
+
? PLATFORM_VERSIONS.unstable
|
|
52
|
+
: platformVersion,
|
|
53
|
+
}, projectConfig);
|
|
54
|
+
}
|
|
36
55
|
}
|
|
37
56
|
catch (error) {
|
|
38
57
|
logError(error);
|
package/lang/en.d.ts
CHANGED
|
@@ -872,6 +872,7 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
|
|
|
872
872
|
};
|
|
873
873
|
readonly success: (derivedTargets: string[]) => string;
|
|
874
874
|
readonly errors: {
|
|
875
|
+
readonly needsMcpAccess: (accountId?: number) => string;
|
|
875
876
|
readonly needsNode20: "This feature requires node >=20";
|
|
876
877
|
readonly errorParsingJsonFIle: (filename: string, errorMessage: string) => string;
|
|
877
878
|
};
|
|
@@ -1136,6 +1137,7 @@ ${string}`;
|
|
|
1136
1137
|
readonly describe: "Migrate an existing project to the new version of the projects framework.";
|
|
1137
1138
|
readonly errors: {
|
|
1138
1139
|
readonly noProjectConfig: (command: string) => string;
|
|
1140
|
+
readonly noThemeMigrationAccess: (accountId?: number) => string;
|
|
1139
1141
|
};
|
|
1140
1142
|
readonly examples: {
|
|
1141
1143
|
readonly default: "Migrate an existing project to the new version of the projects framework.";
|
|
@@ -3388,6 +3390,7 @@ Run ${string} to upgrade to version ${string}`;
|
|
|
3388
3390
|
readonly sourceContentsMoved: (newLocation: string) => string;
|
|
3389
3391
|
readonly projectMigrationWarningTitle: "Important: Migrating to platformVersion 2025.2 is irreversible";
|
|
3390
3392
|
readonly projectMigrationWarning: string;
|
|
3393
|
+
readonly exitWithoutMigrating: "Exiting without migrating";
|
|
3391
3394
|
readonly success: {
|
|
3392
3395
|
readonly downloadedProject: (projectName: string, projectDest: string) => string;
|
|
3393
3396
|
readonly themesMigrationSuccess: (platformVersion: string) => string;
|
package/lang/en.js
CHANGED
|
@@ -5,6 +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, getAppAllowlistUrl, } from '../lib/projects/urls.js';
|
|
8
|
+
import { getProductUpdatesUrl } from '../lib/links.js';
|
|
8
9
|
import { APP_DISTRIBUTION_TYPES, APP_AUTH_TYPES, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
|
|
9
10
|
import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier';
|
|
10
11
|
export const commands = {
|
|
@@ -875,6 +876,7 @@ export const commands = {
|
|
|
875
876
|
},
|
|
876
877
|
success: (derivedTargets) => `You can now use the HubSpot CLI MCP Server in ${derivedTargets.join(', ')}. ${chalk.bold('You may need to restart these tools to apply the changes')}.`,
|
|
877
878
|
errors: {
|
|
879
|
+
needsMcpAccess: (accountId) => `You must opt in to the developer MCP beta to use this feature on ${uiAccountDescription(accountId)}. Try again with a different account or ${uiLink('join the beta now', getProductUpdatesUrl('239890', accountId))}`,
|
|
878
880
|
needsNode20: `This feature requires node >=20`,
|
|
879
881
|
errorParsingJsonFIle: (filename, errorMessage) => `Unable to update ${chalk.bold(filename)} due to invalid JSON: ${errorMessage}`,
|
|
880
882
|
},
|
|
@@ -1135,6 +1137,7 @@ export const commands = {
|
|
|
1135
1137
|
describe: 'Migrate an existing project to the new version of the projects framework.',
|
|
1136
1138
|
errors: {
|
|
1137
1139
|
noProjectConfig: (command) => `No project detected. Please run this command again from a project directory. If you are trying to migrate an app, run ${command}`,
|
|
1140
|
+
noThemeMigrationAccess: (accountId) => `This project contains a CMS theme. You must opt in to theme migration beta to continue updating it on ${uiAccountDescription(accountId)}. Try again with a different account or ${uiLink('join the beta now', getProductUpdatesUrl('253920', accountId))}`,
|
|
1138
1141
|
},
|
|
1139
1142
|
examples: {
|
|
1140
1143
|
default: 'Migrate an existing project to the new version of the projects framework.',
|
|
@@ -3382,6 +3385,7 @@ export const lib = {
|
|
|
3382
3385
|
sourceContentsMoved: (newLocation) => `The contents of your old source directory have been moved to ${newLocation}, move any required files to the new source directory.`,
|
|
3383
3386
|
projectMigrationWarningTitle: 'Important: Migrating to platformVersion 2025.2 is irreversible',
|
|
3384
3387
|
projectMigrationWarning: uiBetaTag(`Running the ${uiCommandReference('hs project migrate')} command will permanently upgrade your project to platformVersion 2025.2. This action cannot be undone. To ensure you have access to your original files, they will be copied to a new directory (archive) for safekeeping.\n\nThis command will guide you through the process, prompting you to enter the required fields and will download the new project source code into your project source directory.`, false),
|
|
3388
|
+
exitWithoutMigrating: 'Exiting without migrating',
|
|
3385
3389
|
success: {
|
|
3386
3390
|
downloadedProject: (projectName, projectDest) => `Saved ${projectName} to ${projectDest}`,
|
|
3387
3391
|
themesMigrationSuccess: (platformVersion) => `Successfully migrated project to platformVersion ${chalk.bold(platformVersion)}. Upload your project using ${uiCommandReference('hs project upload')}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
2
2
|
import { getCwd, sanitizeFileName } from '@hubspot/local-dev-lib/path';
|
|
3
3
|
import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
|
|
4
|
-
import { validateUid
|
|
4
|
+
import { validateUid } from '@hubspot/project-parsing-lib';
|
|
5
5
|
import { UNMIGRATABLE_REASONS } from '@hubspot/local-dev-lib/constants/projects';
|
|
6
6
|
import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration';
|
|
7
7
|
import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
|
|
@@ -12,7 +12,7 @@ import { poll } from '../../polling.js';
|
|
|
12
12
|
import { CLI_UNMIGRATABLE_REASONS, continueAppMigration, initializeAppMigration, listAppsForMigration, } from '../../../api/migrate.js';
|
|
13
13
|
import { lib } from '../../../lang/en.js';
|
|
14
14
|
import { hasUnfiedAppsAccess } from '../../hasFeature.js';
|
|
15
|
-
import { getUnmigratableReason, generateFilterAppsByProjectNameFunction, buildErrorMessageFromMigrationStatus, fetchMigrationApps, promptForAppToMigrate, selectAppToMigrate, handleMigrationSetup,
|
|
15
|
+
import { getUnmigratableReason, generateFilterAppsByProjectNameFunction, buildErrorMessageFromMigrationStatus, fetchMigrationApps, promptForAppToMigrate, selectAppToMigrate, handleMigrationSetup, beginAppMigration, pollMigrationStatus, finalizeAppMigration, downloadProjectFiles, migrateApp2025_2, logInvalidAccountError, validateMigrationApps, } from '../migrate.js';
|
|
16
16
|
vi.mock('@hubspot/local-dev-lib/logger');
|
|
17
17
|
vi.mock('@hubspot/local-dev-lib/path');
|
|
18
18
|
vi.mock('@hubspot/local-dev-lib/archive');
|
|
@@ -40,7 +40,6 @@ const mockedListPrompt = listPrompt;
|
|
|
40
40
|
const mockedEnsureProjectExists = ensureProjectExists;
|
|
41
41
|
const mockedPoll = poll;
|
|
42
42
|
const mockedListAppsForMigration = listAppsForMigration;
|
|
43
|
-
const mockedGetProjectThemeDetails = getProjectThemeDetails;
|
|
44
43
|
const mockedInitializeAppMigration = initializeAppMigration;
|
|
45
44
|
const mockedContinueAppMigration = continueAppMigration;
|
|
46
45
|
const mockedHasUnfiedAppsAccess = hasUnfiedAppsAccess;
|
|
@@ -205,58 +204,31 @@ describe('lib/app/migrate', () => {
|
|
|
205
204
|
expect(result.migratableApps[0].projectName).toBe(PROJECT_NAME);
|
|
206
205
|
});
|
|
207
206
|
});
|
|
208
|
-
describe('
|
|
209
|
-
it('should return false when no projectConfig is provided', async () => {
|
|
210
|
-
const result = await getHasMigratableThemes();
|
|
211
|
-
expect(result).toEqual({
|
|
212
|
-
hasMigratableThemes: false,
|
|
213
|
-
migratableThemesCount: 0,
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
it('should return true when there are migratable themes', async () => {
|
|
217
|
-
mockedGetProjectThemeDetails.mockResolvedValue({
|
|
218
|
-
legacyThemeDetails: [
|
|
219
|
-
{
|
|
220
|
-
configFilepath: 'src/theme.json',
|
|
221
|
-
themePath: 'src/theme',
|
|
222
|
-
themeConfig: {
|
|
223
|
-
secret_names: ['my-secret'],
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
legacyReactThemeDetails: [],
|
|
228
|
-
});
|
|
229
|
-
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
230
|
-
const result = await getHasMigratableThemes(projectConfig);
|
|
231
|
-
expect(result).toEqual({
|
|
232
|
-
hasMigratableThemes: true,
|
|
233
|
-
migratableThemesCount: 1,
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
describe('validateMigrationAppsAndThemes', () => {
|
|
207
|
+
describe('validateMigrationApps', () => {
|
|
238
208
|
const mockMigratableApp1 = createMockMigratableApp(1, 'App 1', PROJECT_NAME);
|
|
239
209
|
const mockMigratableApp2 = createMockMigratableApp(2, 'App 2', PROJECT_NAME);
|
|
240
210
|
it('should throw an error when multiple apps are found for a project', async () => {
|
|
241
211
|
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
242
|
-
await expect(
|
|
212
|
+
await expect(validateMigrationApps(APP_ID, ACCOUNT_ID, {
|
|
243
213
|
migratableApps: [mockMigratableApp1, mockMigratableApp2],
|
|
244
214
|
unmigratableApps: [],
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
it('should throw an error when cms themes are found for a project', async () => {
|
|
248
|
-
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
249
|
-
await expect(validateMigrationAppsAndThemes(APP_ID, ACCOUNT_ID, { migratableApps: [mockMigratableApp1], unmigratableApps: [] }, true, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
215
|
+
}, projectConfig)).rejects.toThrow(lib.migrate.errors.project.multipleApps);
|
|
250
216
|
});
|
|
251
217
|
it('should throw an error when no apps are found for a project', async () => {
|
|
252
218
|
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
253
|
-
await expect(
|
|
219
|
+
await expect(validateMigrationApps(APP_ID, ACCOUNT_ID, { migratableApps: [], unmigratableApps: [] }, projectConfig)).rejects.toThrow(lib.migrate.errors.noAppsForProject(PROJECT_NAME));
|
|
254
220
|
});
|
|
255
221
|
it('should throw an error when no migratable apps are found', async () => {
|
|
256
|
-
await expect(
|
|
222
|
+
await expect(validateMigrationApps(APP_ID, ACCOUNT_ID, {
|
|
223
|
+
migratableApps: [],
|
|
224
|
+
unmigratableApps: mockUnmigratableApps,
|
|
225
|
+
})).rejects.toThrow(/No apps in account/);
|
|
257
226
|
});
|
|
258
227
|
it('should throw an error when appId is provided but not found', async () => {
|
|
259
|
-
await expect(
|
|
228
|
+
await expect(validateMigrationApps(APP_ID, ACCOUNT_ID, {
|
|
229
|
+
migratableApps: [],
|
|
230
|
+
unmigratableApps: [],
|
|
231
|
+
})).rejects.toThrow(/No apps in account/);
|
|
260
232
|
});
|
|
261
233
|
});
|
|
262
234
|
describe('promptForAppToMigrate', () => {
|
|
@@ -330,10 +302,6 @@ describe('lib/app/migrate', () => {
|
|
|
330
302
|
unmigratableApps: [],
|
|
331
303
|
},
|
|
332
304
|
});
|
|
333
|
-
mockedGetProjectThemeDetails.mockResolvedValue({
|
|
334
|
-
legacyThemeDetails: [],
|
|
335
|
-
legacyReactThemeDetails: [],
|
|
336
|
-
});
|
|
337
305
|
mockedListPrompt.mockResolvedValue({ appId: 1 });
|
|
338
306
|
mockedConfirmPrompt.mockResolvedValue(true);
|
|
339
307
|
mockedEnsureProjectExists.mockResolvedValue({ projectExists: false });
|
|
@@ -352,16 +320,11 @@ describe('lib/app/migrate', () => {
|
|
|
352
320
|
unmigratableApps: [],
|
|
353
321
|
},
|
|
354
322
|
});
|
|
355
|
-
mockedGetProjectThemeDetails.mockResolvedValueOnce({
|
|
356
|
-
legacyThemeDetails: [],
|
|
357
|
-
legacyReactThemeDetails: [],
|
|
358
|
-
});
|
|
359
323
|
const result = await handleMigrationSetup(ACCOUNT_ID, defaultOptions, projectConfig);
|
|
360
324
|
expect(result).toEqual({
|
|
361
325
|
appIdToMigrate: 1,
|
|
362
326
|
projectName: PROJECT_NAME,
|
|
363
327
|
projectDest: MOCK_PROJECT_DIR,
|
|
364
|
-
isThemesMigration: false,
|
|
365
328
|
});
|
|
366
329
|
});
|
|
367
330
|
it('should prompt for project name when not provided', async () => {
|
package/lib/app/migrate.d.ts
CHANGED
|
@@ -12,18 +12,14 @@ export type MigrateAppArgs = CommonArgs & AccountArgs & EnvironmentArgs & Config
|
|
|
12
12
|
export declare function getUnmigratableReason(reasonCode: string, projectName: string | undefined, accountId: number): string;
|
|
13
13
|
export declare function generateFilterAppsByProjectNameFunction(projectConfig?: LoadedProjectConfig): (app: MigrationApp) => boolean;
|
|
14
14
|
export declare function buildErrorMessageFromMigrationStatus(error: MigrationFailed): string;
|
|
15
|
-
export declare function getHasMigratableThemes(projectConfig?: LoadedProjectConfig): Promise<{
|
|
16
|
-
hasMigratableThemes: boolean;
|
|
17
|
-
migratableThemesCount: number;
|
|
18
|
-
}>;
|
|
19
15
|
export declare function fetchMigrationApps(derivedAccountId: number, platformVersion: string, projectConfig?: LoadedProjectConfig): Promise<{
|
|
20
16
|
migratableApps: MigratableApp[];
|
|
21
17
|
unmigratableApps: UnmigratableApp[];
|
|
22
18
|
}>;
|
|
23
|
-
export declare function
|
|
19
|
+
export declare function validateMigrationApps(appId: MigrateAppArgs['appId'], derivedAccountId: number, { migratableApps, unmigratableApps, }: {
|
|
24
20
|
migratableApps: MigratableApp[];
|
|
25
21
|
unmigratableApps: UnmigratableApp[];
|
|
26
|
-
},
|
|
22
|
+
}, projectConfig?: LoadedProjectConfig): Promise<void>;
|
|
27
23
|
export declare function promptForAppToMigrate(allApps: MigrationApp[], derivedAccountId: number): Promise<number>;
|
|
28
24
|
export declare function selectAppToMigrate(allApps: MigrationApp[], derivedAccountId: number, appId?: number): Promise<{
|
|
29
25
|
proceed: boolean;
|
|
@@ -33,9 +29,7 @@ export declare function handleMigrationSetup(derivedAccountId: number, options:
|
|
|
33
29
|
appIdToMigrate?: number | undefined;
|
|
34
30
|
projectName?: string;
|
|
35
31
|
projectDest?: string;
|
|
36
|
-
isThemesMigration?: boolean;
|
|
37
32
|
}>;
|
|
38
|
-
export declare function handleThemesMigration(projectConfig: LoadedProjectConfig, platformVersion: string): Promise<void>;
|
|
39
33
|
export declare function beginAppMigration(derivedAccountId: number, appId: number, platformVersion: string): Promise<{
|
|
40
34
|
migrationId: number;
|
|
41
35
|
uidMap: Record<string, string>;
|
package/lib/app/migrate.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { getCwd, sanitizeFileName } from '@hubspot/local-dev-lib/path';
|
|
3
3
|
import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { validateUid
|
|
5
|
+
import { validateUid } from '@hubspot/project-parsing-lib';
|
|
6
6
|
import { UNMIGRATABLE_REASONS } from '@hubspot/local-dev-lib/constants/projects';
|
|
7
7
|
import { mapToUserFacingType } from '@hubspot/project-parsing-lib/src/lib/transform.js';
|
|
8
8
|
import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration';
|
|
@@ -10,19 +10,15 @@ import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
|
|
|
10
10
|
import { Separator } from '@inquirer/prompts';
|
|
11
11
|
import { confirmPrompt, inputPrompt, listPrompt, } from '../prompts/promptUtils.js';
|
|
12
12
|
import { uiAccountDescription, uiCommandReference, uiLine, uiLink, } from '../ui/index.js';
|
|
13
|
-
import { writeProjectConfig } from '../projects/config.js';
|
|
14
13
|
import { ensureProjectExists } from '../projects/ensureProjectExists.js';
|
|
15
14
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
16
15
|
import { DEFAULT_POLLING_STATUS_LOOKUP, poll } from '../polling.js';
|
|
17
16
|
import { checkMigrationStatusV2, CLI_UNMIGRATABLE_REASONS, continueAppMigration, initializeAppMigration, isMigrationStatus, listAppsForMigration, } from '../../api/migrate.js';
|
|
18
17
|
import fs from 'fs';
|
|
19
18
|
import { lib } from '../../lang/en.js';
|
|
20
|
-
import { PROJECT_CONFIG_FILE } from '../constants.js';
|
|
21
19
|
import { hasUnfiedAppsAccess } from '../hasFeature.js';
|
|
22
20
|
import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
|
|
23
21
|
import { uiLogger } from '../ui/logger.js';
|
|
24
|
-
import { debugError } from '../errorHandlers/index.js';
|
|
25
|
-
import { useV3Api } from '../projects/platformVersion.js';
|
|
26
22
|
export function getUnmigratableReason(reasonCode, projectName, accountId) {
|
|
27
23
|
switch (reasonCode) {
|
|
28
24
|
case UNMIGRATABLE_REASONS.UP_TO_DATE:
|
|
@@ -59,17 +55,6 @@ export function buildErrorMessageFromMigrationStatus(error) {
|
|
|
59
55
|
})
|
|
60
56
|
.join('\n\t- ')}`;
|
|
61
57
|
}
|
|
62
|
-
export async function getHasMigratableThemes(projectConfig) {
|
|
63
|
-
if (!projectConfig?.projectConfig?.name || !projectConfig?.projectDir) {
|
|
64
|
-
return { hasMigratableThemes: false, migratableThemesCount: 0 };
|
|
65
|
-
}
|
|
66
|
-
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
67
|
-
const { legacyThemeDetails, legacyReactThemeDetails } = await getProjectThemeDetails(projectSrcDir);
|
|
68
|
-
return {
|
|
69
|
-
hasMigratableThemes: legacyThemeDetails.length > 0 || legacyReactThemeDetails.length > 0,
|
|
70
|
-
migratableThemesCount: legacyThemeDetails.length + legacyReactThemeDetails.length,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
58
|
export async function fetchMigrationApps(derivedAccountId, platformVersion, projectConfig) {
|
|
74
59
|
const { data: { migratableApps, unmigratableApps }, } = await listAppsForMigration(derivedAccountId, platformVersion);
|
|
75
60
|
const filteredMigratableApps = migratableApps.filter(generateFilterAppsByProjectNameFunction(projectConfig));
|
|
@@ -79,23 +64,11 @@ export async function fetchMigrationApps(derivedAccountId, platformVersion, proj
|
|
|
79
64
|
unmigratableApps: filteredUnmigratableApps,
|
|
80
65
|
};
|
|
81
66
|
}
|
|
82
|
-
export async function
|
|
67
|
+
export async function validateMigrationApps(appId, derivedAccountId, { migratableApps, unmigratableApps, }, projectConfig) {
|
|
83
68
|
const allApps = [...migratableApps, ...unmigratableApps];
|
|
84
69
|
if (allApps.length > 1 && projectConfig) {
|
|
85
70
|
throw new Error(lib.migrate.errors.project.multipleApps);
|
|
86
71
|
}
|
|
87
|
-
if (hasMigratableThemes) {
|
|
88
|
-
if (useV3Api(projectConfig?.projectConfig?.platformVersion)) {
|
|
89
|
-
throw new Error(lib.migrate.errors.project.themesAlreadyMigrated);
|
|
90
|
-
}
|
|
91
|
-
if (allApps.length > 0 && projectConfig) {
|
|
92
|
-
throw new Error(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
93
|
-
}
|
|
94
|
-
if (!projectConfig) {
|
|
95
|
-
throw new Error(lib.migrate.errors.project.noProjectForThemesMigration);
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
72
|
if (!projectConfig?.projectConfig) {
|
|
100
73
|
allApps.forEach(app => {
|
|
101
74
|
if (app.projectName) {
|
|
@@ -178,35 +151,20 @@ export async function handleMigrationSetup(derivedAccountId, options, projectCon
|
|
|
178
151
|
text: lib.migrate.spinners.checkingForMigratableComponents,
|
|
179
152
|
});
|
|
180
153
|
const { name, dest, appId } = options;
|
|
181
|
-
const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes(projectConfig);
|
|
182
154
|
const { migratableApps, unmigratableApps } = await fetchMigrationApps(derivedAccountId, options.platformVersion, projectConfig);
|
|
183
155
|
SpinniesManager.remove('checkingForMigratableComponents');
|
|
184
|
-
await
|
|
156
|
+
await validateMigrationApps(appId, derivedAccountId, { migratableApps, unmigratableApps }, projectConfig);
|
|
185
157
|
const allApps = [...migratableApps, ...unmigratableApps];
|
|
186
|
-
|
|
187
|
-
let appIdToMigrate;
|
|
188
|
-
if (hasMigratableThemes) {
|
|
189
|
-
uiLogger.log(lib.migrate.prompt.themesMigration(migratableThemesCount));
|
|
190
|
-
proceed = await confirmPrompt(lib.migrate.prompt.proceed, {
|
|
191
|
-
defaultAnswer: false,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
const appSelectionPrompt = await selectAppToMigrate(allApps, derivedAccountId, appId);
|
|
196
|
-
appIdToMigrate = appSelectionPrompt.appIdToMigrate;
|
|
197
|
-
proceed = appSelectionPrompt.proceed;
|
|
198
|
-
}
|
|
158
|
+
const { proceed, appIdToMigrate } = await selectAppToMigrate(allApps, derivedAccountId, appId);
|
|
199
159
|
if (!proceed) {
|
|
200
160
|
return {};
|
|
201
161
|
}
|
|
202
162
|
// If it's a project we don't want to prompt for dest and name, so just return early
|
|
203
|
-
// Theme migrations can only be initiated from the project directory
|
|
204
163
|
if (projectConfig &&
|
|
205
164
|
projectConfig?.projectConfig &&
|
|
206
165
|
projectConfig?.projectDir) {
|
|
207
166
|
return {
|
|
208
167
|
appIdToMigrate,
|
|
209
|
-
isThemesMigration: hasMigratableThemes,
|
|
210
168
|
projectName: projectConfig.projectConfig.name,
|
|
211
169
|
projectDest: projectConfig.projectDir,
|
|
212
170
|
};
|
|
@@ -231,35 +189,6 @@ export async function handleMigrationSetup(derivedAccountId, options, projectCon
|
|
|
231
189
|
}));
|
|
232
190
|
return { appIdToMigrate, projectName, projectDest };
|
|
233
191
|
}
|
|
234
|
-
export async function handleThemesMigration(projectConfig, platformVersion) {
|
|
235
|
-
if (!projectConfig?.projectDir || !projectConfig?.projectConfig?.srcDir) {
|
|
236
|
-
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
237
|
-
}
|
|
238
|
-
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
239
|
-
let migrated = false;
|
|
240
|
-
let failureReason;
|
|
241
|
-
try {
|
|
242
|
-
const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
243
|
-
migrated = migrationResult.migrated;
|
|
244
|
-
failureReason = migrationResult.failureReason;
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
debugError(error);
|
|
248
|
-
throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
|
|
249
|
-
}
|
|
250
|
-
if (!migrated) {
|
|
251
|
-
throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
|
|
252
|
-
}
|
|
253
|
-
const newProjectConfig = { ...projectConfig.projectConfig };
|
|
254
|
-
newProjectConfig.platformVersion = platformVersion;
|
|
255
|
-
const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
|
|
256
|
-
const success = writeProjectConfig(projectConfigPath, newProjectConfig);
|
|
257
|
-
if (!success) {
|
|
258
|
-
throw new Error(lib.migrate.errors.project.failedToUpdateProjectConfig);
|
|
259
|
-
}
|
|
260
|
-
uiLogger.log('');
|
|
261
|
-
uiLogger.log(lib.migrate.success.themesMigrationSuccess(platformVersion));
|
|
262
|
-
}
|
|
263
192
|
export async function beginAppMigration(derivedAccountId, appId, platformVersion) {
|
|
264
193
|
SpinniesManager.add('beginningMigration', {
|
|
265
194
|
text: lib.migrate.spinners.beginningMigration,
|
|
@@ -412,11 +341,7 @@ export async function migrateApp2025_2(derivedAccountId, options, projectConfig)
|
|
|
412
341
|
throw new Error(lib.migrate.errors.project.doesNotExist(derivedAccountId));
|
|
413
342
|
}
|
|
414
343
|
}
|
|
415
|
-
const { appIdToMigrate, projectName, projectDest
|
|
416
|
-
if (isThemesMigration) {
|
|
417
|
-
await handleThemesMigration(projectConfig, options.platformVersion);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
344
|
+
const { appIdToMigrate, projectName, projectDest } = await handleMigrationSetup(derivedAccountId, options, projectConfig);
|
|
420
345
|
if (!appIdToMigrate || !projectName || !projectDest) {
|
|
421
346
|
return;
|
|
422
347
|
}
|
package/lib/constants.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare const FEATURES: {
|
|
|
82
82
|
readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
|
|
83
83
|
readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
|
|
84
84
|
readonly APPS_HOME: "UIE:AppHome";
|
|
85
|
+
readonly MCP_ACCESS: "Developers:CLIMCPAccess";
|
|
86
|
+
readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
|
|
85
87
|
};
|
|
86
88
|
export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
87
89
|
UPLOAD_SUCCESS: string;
|
package/lib/constants.js
CHANGED
|
@@ -74,6 +74,8 @@ export const FEATURES = {
|
|
|
74
74
|
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
75
75
|
APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
|
|
76
76
|
APPS_HOME: 'UIE:AppHome',
|
|
77
|
+
MCP_ACCESS: 'Developers:CLIMCPAccess',
|
|
78
|
+
THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
|
|
77
79
|
};
|
|
78
80
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
79
81
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
package/lib/links.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ type SiteLink = {
|
|
|
7
7
|
export declare function getSiteLinksAsArray(accountId: number): SiteLink[];
|
|
8
8
|
export declare function logSiteLinks(accountId: number): void;
|
|
9
9
|
export declare function openLink(accountId: number, shortcut: string): void;
|
|
10
|
+
export declare function getProductUpdatesUrl(rolloutId: string, accountId?: number): string;
|
|
10
11
|
export {};
|
package/lib/links.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
|
|
|
4
4
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
5
5
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
6
6
|
import { getTableContents, getTableHeader } from './ui/table.js';
|
|
7
|
-
const
|
|
7
|
+
const COMMON_SITE_LINKS = {
|
|
8
8
|
APPS_MARKETPLACE: {
|
|
9
9
|
shortcut: 'apps-marketplace',
|
|
10
10
|
alias: 'apm',
|
|
@@ -76,7 +76,7 @@ const SITE_LINKS = {
|
|
|
76
76
|
};
|
|
77
77
|
export function getSiteLinksAsArray(accountId) {
|
|
78
78
|
const baseUrl = getHubSpotWebsiteOrigin(getEnv() === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
|
|
79
|
-
return Object.values(
|
|
79
|
+
return Object.values(COMMON_SITE_LINKS)
|
|
80
80
|
.sort((a, b) => (a.shortcut < b.shortcut ? -1 : 1))
|
|
81
81
|
.map(l => ({ ...l, url: l.getUrl(accountId, baseUrl) }));
|
|
82
82
|
}
|
|
@@ -90,7 +90,7 @@ export function logSiteLinks(accountId) {
|
|
|
90
90
|
logger.log(getTableContents(linksAsArray));
|
|
91
91
|
}
|
|
92
92
|
export function openLink(accountId, shortcut) {
|
|
93
|
-
const match = Object.values(
|
|
93
|
+
const match = Object.values(COMMON_SITE_LINKS).find(l => l.shortcut === shortcut || (l.alias && l.alias === shortcut));
|
|
94
94
|
if (!match) {
|
|
95
95
|
logger.error(`We couldn't find a shortcut matching ${shortcut}. Type 'hs open list' to see a list of available shortcuts`);
|
|
96
96
|
return;
|
|
@@ -99,3 +99,10 @@ export function openLink(accountId, shortcut) {
|
|
|
99
99
|
open(match.getUrl(accountId, baseUrl), { url: true });
|
|
100
100
|
logger.success(`We opened ${match.getUrl(accountId, baseUrl)} in your browser`);
|
|
101
101
|
}
|
|
102
|
+
export function getProductUpdatesUrl(rolloutId, accountId) {
|
|
103
|
+
const baseUrl = getHubSpotWebsiteOrigin(getEnv() === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD);
|
|
104
|
+
if (accountId) {
|
|
105
|
+
return `${baseUrl}/product-updates/${accountId}/in-beta?rollout=${rolloutId}`;
|
|
106
|
+
}
|
|
107
|
+
return `${baseUrl}/l/product-updates/in-beta?rollout=${rolloutId}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|