From f9dd09cef87358b967390866edaa8e896cb6cb43 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 17 Apr 2025 10:38:15 -0700 Subject: [PATCH 01/17] feat: Migrations polish --- api/migrate.ts | 8 +------ lang/en.ts | 3 +++ lib/app/migrate.ts | 46 +++++++++++++++++++++++++++++--------- lib/prompts/promptUtils.ts | 4 +++- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/api/migrate.ts b/api/migrate.ts index 4ffa7de8b..b540bfd91 100644 --- a/api/migrate.ts +++ b/api/migrate.ts @@ -5,8 +5,6 @@ import { } from '@hubspot/local-dev-lib/constants/projects'; import { http } from '@hubspot/local-dev-lib/http'; import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration'; -import { logger } from '@hubspot/local-dev-lib/logger'; -import util from 'util'; const MIGRATIONS_API_PATH_V2 = 'dfs/migrations/v2'; @@ -147,11 +145,7 @@ export async function checkMigrationStatusV2( accountId: number, id: number ): HubSpotPromise { - const response = await http.get(accountId, { + return http.get(accountId, { url: `${MIGRATIONS_API_PATH_V2}/migrations/${id}/status`, }); - - logger.debug(util.inspect(response.data, { depth: null })); - - return response; } diff --git a/lang/en.ts b/lang/en.ts index 87f7810d1..6136a1e21 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -3432,6 +3432,9 @@ export const lib = { `The following component types will be migrated: ${components}`, componentsThatWillNotBeMigrated: components => `[NOTE] These component types are not yet supported for migration but will be available later: ${components}`, + sourceContentsMoved: (newLocation: string) => + `The contents of your old source directory have been moved to ${newLocation}, move any required files to the new source directory.`, + projectMigrationWarning: `Migrating a project is irreversible and cannot be undone.`, errors: { project: { invalidConfig: diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 1ac7cae46..b1ecc18cc 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -31,7 +31,6 @@ import { ConfigArgs, EnvironmentArgs, } from '../../types/Yargs'; -import util from 'util'; import { hasFeature } from '../hasFeature'; export type MigrateAppArgs = CommonArgs & @@ -131,7 +130,8 @@ async function fetchMigrationApps( async function selectAppToMigrate( allApps: MigrationApp[], - appId?: number + appId?: number, + projectConfig?: LoadedProjectConfig ): Promise<{ proceed: boolean; appIdToMigrate?: number }> { if ( appId && @@ -179,7 +179,7 @@ async function selectAppToMigrate( if (migratableComponents.length !== 0) { logger.log( lib.migrate.componentsToBeMigrated( - `\n - ${migratableComponents.join('\n - ')}` + `\n - ${[...new Set(migratableComponents)].join('\n - ')}` ) ); } @@ -187,13 +187,22 @@ async function selectAppToMigrate( if (unmigratableComponents.length !== 0) { logger.log( lib.migrate.componentsThatWillNotBeMigrated( - `\n - ${unmigratableComponents.join('\n - ')}` + `\n - ${[...new Set(unmigratableComponents)].join('\n - ')}` ) ); } logger.log(); - const proceed = await confirmPrompt(lib.migrate.prompt.proceed); + + if (projectConfig?.projectConfig) { + logger.log(lib.migrate.projectMigrationWarning); + } + + const promptMessage = projectConfig?.projectConfig + ? `${lib.migrate.projectMigrationWarning} ${lib.migrate.prompt.proceed}` + : lib.migrate.prompt.proceed; + + const proceed = await confirmPrompt(promptMessage); return { proceed, appIdToMigrate, @@ -218,7 +227,11 @@ async function handleMigrationSetup( projectConfig ); - const { proceed, appIdToMigrate } = await selectAppToMigrate(allApps, appId); + const { proceed, appIdToMigrate } = await selectAppToMigrate( + allApps, + appId, + projectConfig + ); if (!proceed) { return {}; @@ -238,9 +251,22 @@ async function handleMigrationSetup( } const projectName = - projectConfig?.projectConfig?.name || name || - (await inputPrompt(lib.migrate.prompt.inputName)); + (await inputPrompt(lib.migrate.prompt.inputName, { + validate: async (input: string) => { + const { projectExists } = await ensureProjectExists( + derivedAccountId, + input, + { allowCreate: false, noLogs: true } + ); + + if (projectExists) { + return lib.migrate.errors.project.alreadyExists(input); + } + + return true; + }, + })); const { projectExists } = await ensureProjectExists( derivedAccountId, @@ -374,8 +400,6 @@ async function finalizeMigration( throw new Error(lib.migrate.errors.migrationFailed); } - logger.debug(util.inspect(pollResponse, { depth: null })); - if (pollResponse.status === MIGRATION_STATUS.SUCCESS) { SpinniesManager.succeed('finishingMigration', { text: lib.migrate.spinners.migrationComplete, @@ -414,6 +438,8 @@ async function downloadProjectFiles( // Move the existing source directory to archive fs.renameSync(path.join(projectDir, srcDir), archiveDest); + + logger.info(lib.migrate.sourceContentsMoved(archiveDest)); } else { absoluteDestPath = projectDest ? path.resolve(getCwd(), projectDest) diff --git a/lib/prompts/promptUtils.ts b/lib/prompts/promptUtils.ts index 346d9fdbe..5289245af 100644 --- a/lib/prompts/promptUtils.ts +++ b/lib/prompts/promptUtils.ts @@ -73,7 +73,9 @@ export async function inputPrompt( defaultAnswer, }: { when?: boolean | (() => boolean); - validate?: (input: string) => boolean | string; + validate?: ( + input: string + ) => (boolean | string) | Promise; defaultAnswer?: string; } = {} ): Promise { From 656c407dbecbac9b27bb608aa0807bb43961cf48 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 17 Apr 2025 10:54:59 -0700 Subject: [PATCH 02/17] clean up dedupe logic --- lib/app/migrate.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index b1ecc18cc..a9cdf2efb 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -165,29 +165,29 @@ async function selectAppToMigrate( const selectedApp = allApps.find(app => app.appId === appIdToMigrate); - const migratableComponents: string[] = []; - const unmigratableComponents: string[] = []; + const migratableComponents: Set = new Set(); + const unmigratableComponents: Set = new Set(); selectedApp?.migrationComponents.forEach(component => { if (component.isSupported) { - migratableComponents.push(mapToUserFacingType(component.componentType)); + migratableComponents.add(mapToUserFacingType(component.componentType)); } else { - unmigratableComponents.push(mapToUserFacingType(component.componentType)); + unmigratableComponents.add(mapToUserFacingType(component.componentType)); } }); - if (migratableComponents.length !== 0) { + if (migratableComponents.size !== 0) { logger.log( lib.migrate.componentsToBeMigrated( - `\n - ${[...new Set(migratableComponents)].join('\n - ')}` + `\n - ${[...migratableComponents].join('\n - ')}` ) ); } - if (unmigratableComponents.length !== 0) { + if (unmigratableComponents.size !== 0) { logger.log( lib.migrate.componentsThatWillNotBeMigrated( - `\n - ${[...new Set(unmigratableComponents)].join('\n - ')}` + `\n - ${[...unmigratableComponents].join('\n - ')}` ) ); } From b21050c8ee6180ecd0222623c28e0aa2d06adbfc Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 17 Apr 2025 11:02:26 -0700 Subject: [PATCH 03/17] Remove duplicate log message --- lib/app/migrate.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index a9cdf2efb..a6aea9428 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -194,10 +194,6 @@ async function selectAppToMigrate( logger.log(); - if (projectConfig?.projectConfig) { - logger.log(lib.migrate.projectMigrationWarning); - } - const promptMessage = projectConfig?.projectConfig ? `${lib.migrate.projectMigrationWarning} ${lib.migrate.prompt.proceed}` : lib.migrate.prompt.proceed; From 1caccd536161503196f33de71247fbe01afb9145 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Tue, 22 Apr 2025 15:09:55 -0700 Subject: [PATCH 04/17] feat: Incorporate UX feedback --- api/migrate.ts | 9 ++++++++- commands/app/migrate.ts | 2 +- commands/project/migrate.ts | 5 ++++- lang/en.lyaml | 2 +- lang/en.ts | 5 ++++- lib/app/migrate.ts | 32 ++++++++++++++++++++++---------- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/api/migrate.ts b/api/migrate.ts index b540bfd91..6b2083ff6 100644 --- a/api/migrate.ts +++ b/api/migrate.ts @@ -18,11 +18,18 @@ interface BaseMigrationApp { export interface MigratableApp extends BaseMigrationApp { isMigratable: true; + unmigratableReason: undefined; } +export const CLI_UNMIGRATABLE_REASONS = { + PART_OF_PROJECT_ALREADY: 'PART_OF_PROJECT_ALREADY', +} as const; + export interface UnmigratableApp extends BaseMigrationApp { isMigratable: false; - unmigratableReason: keyof typeof UNMIGRATABLE_REASONS; + unmigratableReason: + | keyof typeof UNMIGRATABLE_REASONS + | keyof typeof CLI_UNMIGRATABLE_REASONS; } export type MigrationApp = MigratableApp | UnmigratableApp; diff --git a/commands/app/migrate.ts b/commands/app/migrate.ts index ab05b4603..e6c251cec 100644 --- a/commands/app/migrate.ts +++ b/commands/app/migrate.ts @@ -124,7 +124,7 @@ export function builder(yargs: Argv): Argv { type: 'string', choices: validMigrationTargets, hidden: true, - default: '2023.2', + default: '2025.2', }, }); diff --git a/commands/project/migrate.ts b/commands/project/migrate.ts index a635077da..d0d83824e 100644 --- a/commands/project/migrate.ts +++ b/commands/project/migrate.ts @@ -18,6 +18,7 @@ import { getProjectConfig } from '../../lib/projects'; import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects'; import { logError } from '../../lib/errorHandlers'; import { EXIT_CODES } from '../../lib/enums/exitCodes'; +import { uiCommandReference } from '../../lib/ui'; export type ProjectMigrateArgs = CommonArgs & AccountArgs & @@ -37,7 +38,9 @@ export async function handler( if (!projectConfig.projectConfig) { logger.error( - i18n('commands.project.subcommands.migrate.errors.noProjectConfig') + i18n('commands.project.subcommands.migrate.errors.noProjectConfig', { + command: uiCommandReference('hs app migrate'), + }) ); return process.exit(EXIT_CODES.ERROR); } diff --git a/lang/en.lyaml b/lang/en.lyaml index 3fb387b23..278e55dd1 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -695,7 +695,7 @@ en: migrate: describe: "Migrate an existing project to the new version of the projects framework." errors: - noProjectConfig: "No project detected. Please run this command again from a project directory." + noProjectConfig: "No project detected. Please run this command again from a project directory. If you are trying to migrate an application, run {{ command }}" examples: default: "Migrate an existing project to the new version of the projects framework." deploy: diff --git a/lang/en.ts b/lang/en.ts index 6136a1e21..c40682847 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -1,5 +1,6 @@ // @ts-nocheck import chalk from 'chalk'; +import { uiAccountDescription, uiCommandReference } from '../lib/ui'; export const commands = { generalErrors: { @@ -3439,7 +3440,8 @@ export const lib = { project: { invalidConfig: 'The project configuration file is invalid. Please check the config file and try again.', - doesNotExist: 'Project does not exist, unable to migrate', + doesNotExist: (account: number) => + `Project does not exist in ${uiAccountDescription(account)}. Migrations are only supported for existing projects.`, multipleApps: 'Multiple apps found in project, this is not allowed in 2025.2', alreadyExists: projectName => @@ -3449,6 +3451,7 @@ export const lib = { upToDate: 'App is already up to date', isPrivateApp: 'Private apps are not currently migratable', listedInMarketplace: 'Listed apps are not currently migratable', + partOfProjectAlready: `This app is part of a project, run ${uiCommandReference('hs project migrate')} from the project directory to migrate it`, generic: reasonCode => `Unable to migrate app: ${reasonCode}`, }, noAppsEligible: (accountId, reasons) => diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index a6aea9428..637c9add9 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -16,6 +16,7 @@ import SpinniesManager from '../ui/SpinniesManager'; import { DEFAULT_POLLING_STATUS_LOOKUP, poll } from '../polling'; import { checkMigrationStatusV2, + CLI_UNMIGRATABLE_REASONS, continueMigration, initializeMigration, isMigrationStatus, @@ -51,6 +52,8 @@ function getUnmigratableReason(reasonCode: string): string { return lib.migrate.errors.unmigratableReasons.isPrivateApp; case UNMIGRATABLE_REASONS.LISTED_IN_MARKETPLACE: return lib.migrate.errors.unmigratableReasons.listedInMarketplace; + case CLI_UNMIGRATABLE_REASONS.PART_OF_PROJECT_ALREADY: + return lib.migrate.errors.unmigratableReasons.partOfProjectAlready; default: return lib.migrate.errors.unmigratableReasons.generic(reasonCode); } @@ -63,7 +66,7 @@ function filterAppsByProjectName( if (projectConfig) { return app.projectName === projectConfig?.projectConfig?.name; } - return !app.projectName; + return true; }; } @@ -91,6 +94,16 @@ async function fetchMigrationApps( throw new Error(lib.migrate.errors.project.multipleApps); } + if (!projectConfig?.projectConfig) { + allApps.forEach(app => { + if (app.projectName) { + app.isMigratable = false; + app.unmigratableReason = + CLI_UNMIGRATABLE_REASONS.PART_OF_PROJECT_ALREADY; + } + }); + } + if (allApps.length === 0 && projectConfig) { throw new Error( lib.migrate.errors.noAppsForProject( @@ -99,10 +112,7 @@ async function fetchMigrationApps( ); } - if ( - allApps.length === 0 || - filteredUnmigratableApps.length === allApps.length - ) { + if (allApps.length === 0 || !allApps.some(app => app.isMigratable)) { const reasons = filteredUnmigratableApps.map( app => `${chalk.bold(app.appName)}: ${getUnmigratableReason(app.unmigratableReason)}` @@ -331,7 +341,7 @@ async function beginMigration( const { componentHint, componentType } = component; uidMap[componentId] = await inputPrompt( lib.migrate.prompt.uidForComponent( - componentHint + componentHint && componentHint ? `${mapToUserFacingType(componentType)} '${componentHint}'` : mapToUserFacingType(componentType) ), @@ -340,9 +350,9 @@ async function beginMigration( const result = validateUid(uid); return result === undefined ? true : result; }, - defaultAnswer: (componentHint || '') - .toLowerCase() - .replace(/[^a-z0-9_]/g, ''), + defaultAnswer: componentHint + ? componentHint.toLowerCase().replace(/[^a-z0-9_]/g, '') + : undefined, } ); } @@ -496,7 +506,9 @@ export async function migrateApp2025_2( ); if (!projectExists) { - throw new Error(lib.migrate.errors.project.doesNotExist); + throw new Error( + lib.migrate.errors.project.doesNotExist(derivedAccountId) + ); } } From ec5998034e116118ffd4b69b2f9cd1668e72d862 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Tue, 22 Apr 2025 16:20:47 -0700 Subject: [PATCH 05/17] Clean up, add componentErrorDetails --- api/__tests__/migrate.test.ts | 4 +--- api/migrate.ts | 2 +- commands/app/__tests__/migrate.test.ts | 13 ++++++------- lang/en.lyaml | 2 +- lib/app/migrate.ts | 13 +++++++++++-- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/api/__tests__/migrate.test.ts b/api/__tests__/migrate.test.ts index 5d1263c00..48598c5b5 100644 --- a/api/__tests__/migrate.test.ts +++ b/api/__tests__/migrate.test.ts @@ -179,9 +179,7 @@ describe('api/migrate', () => { id: mockMigrationId, status: MIGRATION_STATUS.FAILURE, projectErrorDetail: 'Error details', - componentErrorDetails: { - 'component-1': 'Component error', - }, + componentErrorDetails: {}, }; // @ts-expect-error Mock httpMock.get.mockResolvedValue(mockResponse); diff --git a/api/migrate.ts b/api/migrate.ts index 6b2083ff6..853fb37f3 100644 --- a/api/migrate.ts +++ b/api/migrate.ts @@ -18,7 +18,7 @@ interface BaseMigrationApp { export interface MigratableApp extends BaseMigrationApp { isMigratable: true; - unmigratableReason: undefined; + unmigratableReason?: undefined; } export const CLI_UNMIGRATABLE_REASONS = { diff --git a/commands/app/__tests__/migrate.test.ts b/commands/app/__tests__/migrate.test.ts index febf1d86e..9d323282e 100644 --- a/commands/app/__tests__/migrate.test.ts +++ b/commands/app/__tests__/migrate.test.ts @@ -101,8 +101,7 @@ describe('commands/app/migrate', () => { }); it('should add required options', async () => { - await builder(mockYargs); - + builder(mockYargs); expect(mockYargs.options).toHaveBeenCalledWith( expect.objectContaining({ name: expect.objectContaining({ @@ -119,27 +118,27 @@ describe('commands/app/migrate', () => { }), 'platform-version': expect.objectContaining({ type: 'string', - default: '2023.2', + default: '2025.2', hidden: true, }), }) ); }); - it('should set default platform version to 2023.2', async () => { - await builder(mockYargs); + it('should set default platform version to 2025.2', async () => { + builder(mockYargs); expect(mockYargs.options).toHaveBeenCalledWith( expect.objectContaining({ 'platform-version': expect.objectContaining({ - default: '2023.2', + default: '2025.2', }), }) ); }); it('should add example command', async () => { - await builder(mockYargs); + builder(mockYargs); expect(mockYargs.example).toHaveBeenCalledWith([ ['$0 app migrate', expect.any(String)], diff --git a/lang/en.lyaml b/lang/en.lyaml index 278e55dd1..7b5e0d3e8 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -647,7 +647,7 @@ en: migrationInterrupted: "\nThe command is terminated, but app migration is still in progress. Please check your account to ensure that the project and associated app have been created successfully." createAppPrompt: "Proceed with migrating this app to a project component (this process can't be aborted)?" projectDetailsLink: "View project details in your developer account" - componentsToBeMigrated: "The following component types will be migrated: {{ components }}" + componentsToBeMigrated: "The following features will be migrated: {{ components }}" componentsThatWillNotBeMigrated: "[NOTE] These component types are not yet supported for migration but will be available later: {{ components }}" errors: noAppsForProject: "No apps associated with project {{ projectName }}" diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 637c9add9..b94ef0c3e 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -208,7 +208,7 @@ async function selectAppToMigrate( ? `${lib.migrate.projectMigrationWarning} ${lib.migrate.prompt.proceed}` : lib.migrate.prompt.proceed; - const proceed = await confirmPrompt(promptMessage); + const proceed = await confirmPrompt(promptMessage, { defaultAnswer: false }); return { proceed, appIdToMigrate, @@ -394,7 +394,16 @@ async function finalizeMigration( }); if (isMigrationStatus(error) && error.status === MIGRATION_STATUS.FAILURE) { - throw new Error(error.projectErrorDetail); + const errorMessage = error.componentErrorDetails + ? `${error.projectErrorDetail} \n\t - ${Object.entries( + error.componentErrorDetails + ) + .map(([key, value]) => { + return `${mapToUserFacingType(key)}: ${value}`; + }) + .join('\n\t - ')}` + : error.projectErrorDetail; + throw new Error(errorMessage); } throw new Error(lib.migrate.errors.migrationFailed, { From 1079ae2106fa22c58942d4c42eebbd6fc69c3830 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Tue, 22 Apr 2025 16:38:57 -0700 Subject: [PATCH 06/17] Update suggestion for uid --- api/__tests__/migrate.test.ts | 4 +++- lib/app/migrate.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/__tests__/migrate.test.ts b/api/__tests__/migrate.test.ts index 48598c5b5..5d1263c00 100644 --- a/api/__tests__/migrate.test.ts +++ b/api/__tests__/migrate.test.ts @@ -179,7 +179,9 @@ describe('api/migrate', () => { id: mockMigrationId, status: MIGRATION_STATUS.FAILURE, projectErrorDetail: 'Error details', - componentErrorDetails: {}, + componentErrorDetails: { + 'component-1': 'Component error', + }, }; // @ts-expect-error Mock httpMock.get.mockResolvedValue(mockResponse); diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index b94ef0c3e..0f7815a83 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -351,7 +351,7 @@ async function beginMigration( return result === undefined ? true : result; }, defaultAnswer: componentHint - ? componentHint.toLowerCase().replace(/[^a-z0-9_]/g, '') + ? componentHint.replace(/[^A-Za-z0-9_\-.]/g, '') : undefined, } ); From b76f4371e40f0a8fe6f0637d1b0ec2ff5bc8a7b6 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Wed, 23 Apr 2025 11:44:58 -0700 Subject: [PATCH 07/17] Add unstable flag --- commands/app/migrate.ts | 23 ++++++++++++++++------- commands/project/migrate.ts | 26 +++++++++++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/commands/app/migrate.ts b/commands/app/migrate.ts index e6c251cec..a823fe0ce 100644 --- a/commands/app/migrate.ts +++ b/commands/app/migrate.ts @@ -20,14 +20,14 @@ import { uiBetaTag, uiCommandReference, uiLink } from '../../lib/ui'; import { migrateApp2023_2 } from '../../lib/app/migrate_legacy'; import { getIsInProject } from '../../lib/projects'; -const { v2023_2, v2025_2, unstable } = PLATFORM_VERSIONS; -export const validMigrationTargets = [v2023_2, v2025_2, unstable]; +const { v2023_2, v2025_2 } = PLATFORM_VERSIONS; +export const validMigrationTargets = [v2023_2, v2025_2]; const command = 'migrate'; const describe = undefined; // uiBetaTag(i18n(`commands.project.subcommands.migrateApp.header.text.describe`), false); export async function handler(options: ArgumentsCamelCase) { - const { derivedAccountId, platformVersion } = options; + const { derivedAccountId, platformVersion, unstable } = options; await trackCommandUsage('migrate-app', {}, derivedAccountId); const accountConfig = getAccountConfig(derivedAccountId); @@ -54,7 +54,7 @@ export async function handler(options: ArgumentsCamelCase) { logger.log(''); try { - if (platformVersion === v2025_2 || platformVersion === unstable) { + if (platformVersion === v2025_2 || unstable) { if (getIsInProject()) { logger.error( i18n( @@ -62,9 +62,13 @@ export async function handler(options: ArgumentsCamelCase) { { command: uiCommandReference('hs project migrate') } ) ); - process.exit(EXIT_CODES.ERROR); + return process.exit(EXIT_CODES.ERROR); } + options.platformVersion = unstable + ? PLATFORM_VERSIONS.unstable + : platformVersion; + await migrateApp2025_2(derivedAccountId, options); } else { await migrateApp2023_2(derivedAccountId, options, accountConfig); @@ -85,7 +89,7 @@ export async function handler(options: ArgumentsCamelCase) { { successful: false }, derivedAccountId ); - process.exit(EXIT_CODES.ERROR); + return process.exit(EXIT_CODES.ERROR); } await trackCommandMetadataUsage( @@ -124,7 +128,12 @@ export function builder(yargs: Argv): Argv { type: 'string', choices: validMigrationTargets, hidden: true, - default: '2025.2', + default: v2025_2, + }, + unstable: { + type: 'boolean', + default: false, + hidden: true, }, }); diff --git a/commands/project/migrate.ts b/commands/project/migrate.ts index d0d83824e..b59d7a489 100644 --- a/commands/project/migrate.ts +++ b/commands/project/migrate.ts @@ -27,6 +27,9 @@ export type ProjectMigrateArgs = CommonArgs & platformVersion: string; }; +const { v2025_2 } = PLATFORM_VERSIONS; +export const validMigrationTargets = [v2025_2]; + export const command = 'migrate'; export const describe = undefined; // i18n('commands.project.subcommands.migrate.noProjectConfig') @@ -34,6 +37,7 @@ export const describe = undefined; // i18n('commands.project.subcommands.migrate export async function handler( options: ArgumentsCamelCase ): Promise { + const { platformVersion, unstable } = options; const projectConfig = await getProjectConfig(); if (!projectConfig.projectConfig) { @@ -52,7 +56,9 @@ export async function handler( { ...options, name: projectConfig?.projectConfig?.name, - platformVersion: options.platformVersion, + platformVersion: unstable + ? PLATFORM_VERSIONS.unstable + : platformVersion, }, projectConfig ); @@ -68,12 +74,18 @@ export function builder(yargs: Argv): Argv { addAccountOptions(yargs); addGlobalOptions(yargs); - yargs.option('platform-version', { - type: 'string', - choices: Object.values(PLATFORM_VERSIONS), - default: PLATFORM_VERSIONS.v2025_2, - hidden: true, - }); + yargs + .option('platform-version', { + type: 'string', + choices: validMigrationTargets, + default: PLATFORM_VERSIONS.v2025_2, + hidden: true, + }) + .option('unstable', { + type: 'boolean', + default: false, + hidden: true, + }); return yargs as Argv; } From 7ab98ab540e7e92bd9fbfaf74a5348812a92259a Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Wed, 23 Apr 2025 12:44:01 -0700 Subject: [PATCH 08/17] Tweaks to flags --- commands/app/migrate.ts | 3 +-- commands/project/migrateApp.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/commands/app/migrate.ts b/commands/app/migrate.ts index a823fe0ce..229017616 100644 --- a/commands/app/migrate.ts +++ b/commands/app/migrate.ts @@ -21,7 +21,6 @@ import { migrateApp2023_2 } from '../../lib/app/migrate_legacy'; import { getIsInProject } from '../../lib/projects'; const { v2023_2, v2025_2 } = PLATFORM_VERSIONS; -export const validMigrationTargets = [v2023_2, v2025_2]; const command = 'migrate'; const describe = undefined; // uiBetaTag(i18n(`commands.project.subcommands.migrateApp.header.text.describe`), false); @@ -126,7 +125,7 @@ export function builder(yargs: Argv): Argv { }, 'platform-version': { type: 'string', - choices: validMigrationTargets, + choices: [v2023_2, v2025_2], hidden: true, default: v2025_2, }, diff --git a/commands/project/migrateApp.ts b/commands/project/migrateApp.ts index a465716dc..e2ec9f310 100644 --- a/commands/project/migrateApp.ts +++ b/commands/project/migrateApp.ts @@ -1,9 +1,6 @@ import { i18n } from '../../lib/lang'; import { uiCommandReference, uiDeprecatedTag } from '../../lib/ui'; -import { - handler as migrateHandler, - validMigrationTargets, -} from '../app/migrate'; +import { handler as migrateHandler } from '../app/migrate'; import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { logger } from '@hubspot/local-dev-lib/logger'; @@ -13,7 +10,9 @@ import { addUseEnvironmentOptions, } from '../../lib/commonOpts'; import { MigrateAppArgs } from '../../lib/app/migrate'; +import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects'; +const { v2023_2, v2025_2 } = PLATFORM_VERSIONS; export const command = 'migrate-app'; // TODO: Leave this as deprecated and remove in the next major release @@ -27,7 +26,9 @@ export async function handler(yargs: ArgumentsCamelCase) { logger.warn( i18n(`commands.project.subcommands.migrateApp.deprecationWarning`, { oldCommand: uiCommandReference('hs project migrate-app'), - newCommand: uiCommandReference('hs app migrate'), + newCommand: uiCommandReference( + 'hs app migrate --platform-version=2023.2' + ), }) ); await migrateHandler(yargs); @@ -59,9 +60,9 @@ export function builder(yargs: Argv): Argv { }, 'platform-version': { type: 'string', - choices: validMigrationTargets, + choices: [v2023_2, v2025_2], hidden: true, - default: '2023.2', + default: v2023_2, }, }); From 66a31b3176f9576b83149c6ee6be710f2aa31bf6 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Wed, 23 Apr 2025 12:46:54 -0700 Subject: [PATCH 09/17] remove intermediate variable --- commands/project/migrate.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/commands/project/migrate.ts b/commands/project/migrate.ts index b59d7a489..6eba0f52e 100644 --- a/commands/project/migrate.ts +++ b/commands/project/migrate.ts @@ -28,7 +28,6 @@ export type ProjectMigrateArgs = CommonArgs & }; const { v2025_2 } = PLATFORM_VERSIONS; -export const validMigrationTargets = [v2025_2]; export const command = 'migrate'; @@ -77,8 +76,8 @@ export function builder(yargs: Argv): Argv { yargs .option('platform-version', { type: 'string', - choices: validMigrationTargets, - default: PLATFORM_VERSIONS.v2025_2, + choices: [v2025_2], + default: v2025_2, hidden: true, }) .option('unstable', { From 8f6b3d99eb05be509053cb149f1bd3ee95038e71 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 08:02:12 -0700 Subject: [PATCH 10/17] UX Feedback --- commands/project/migrate.ts | 18 +++++++++++------- lang/en.lyaml | 8 +------- lang/en.ts | 37 +++++++++++++++---------------------- lib/app/migrate.ts | 27 +++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/commands/project/migrate.ts b/commands/project/migrate.ts index 6eba0f52e..dde2c9fda 100644 --- a/commands/project/migrate.ts +++ b/commands/project/migrate.ts @@ -1,5 +1,3 @@ -import { i18n } from '../../lib/lang'; - import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { logger } from '@hubspot/local-dev-lib/logger'; import { @@ -18,7 +16,8 @@ import { getProjectConfig } from '../../lib/projects'; import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects'; import { logError } from '../../lib/errorHandlers'; import { EXIT_CODES } from '../../lib/enums/exitCodes'; -import { uiCommandReference } from '../../lib/ui'; +import { uiBetaTag, uiCommandReference } from '../../lib/ui'; +import { commands } from '../../lang/en'; export type ProjectMigrateArgs = CommonArgs & AccountArgs & @@ -31,7 +30,7 @@ const { v2025_2 } = PLATFORM_VERSIONS; export const command = 'migrate'; -export const describe = undefined; // i18n('commands.project.subcommands.migrate.noProjectConfig') +export const describe = undefined; export async function handler( options: ArgumentsCamelCase @@ -41,13 +40,18 @@ export async function handler( if (!projectConfig.projectConfig) { logger.error( - i18n('commands.project.subcommands.migrate.errors.noProjectConfig', { - command: uiCommandReference('hs app migrate'), - }) + commands.project.migrate.errors.noProjectConfig( + uiCommandReference('hs app migrate') + ) ); return process.exit(EXIT_CODES.ERROR); } + logger.log(); + logger.log( + uiBetaTag(commands.project.migrate.preamble(platformVersion), false) + ); + const { derivedAccountId } = options; try { await migrateApp2025_2( diff --git a/lang/en.lyaml b/lang/en.lyaml index 7b5e0d3e8..074d58d51 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -629,7 +629,7 @@ en: name: describe: "Project name (cannot be changed)" header: - text: "Migrate an app to the projects framework" + text: "This command will migrate an application to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing." link: "Learn more about migrating apps to the projects framework" deprecationWarning: "The {{ oldCommand }} command is deprecated and will be removed. Use {{ newCommand }} going forward." migrationStatus: @@ -692,12 +692,6 @@ en: examples: default: "Create a component within your project" withFlags: "Use --name and --type flags to bypass the prompt." - migrate: - describe: "Migrate an existing project to the new version of the projects framework." - errors: - noProjectConfig: "No project detected. Please run this command again from a project directory. If you are trying to migrate an application, run {{ command }}" - examples: - default: "Migrate an existing project to the new version of the projects framework." deploy: describe: "Deploy a project build." deployBuildIdPrompt: "[--build] Deploy which build?" diff --git a/lang/en.ts b/lang/en.ts index c40682847..c84b11b96 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -943,7 +943,7 @@ export const commands = { }, }, header: { - text: 'Migrate an app to the projects framework', + text: 'This command will migrate an application to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing.', link: 'Learn more about migrating apps to the projects framework', }, deprecationWarning: (oldCommand, newCommand) => @@ -977,26 +977,19 @@ export const commands = { createAppPrompt: "Proceed with migrating this app to a project component (this process can't be aborted)?", projectDetailsLink: 'View project details in your developer account', - componentsToBeMigrated: components => - `The following component types will be migrated: ${components}`, - componentsThatWillNotBeMigrated: components => - `[NOTE] These component types are not yet supported for migration but will be available later: ${components}`, + }, + migrate: { + preamble: (platformVersion: string) => + `This command will migrate an existing project to platformVersion ${platformVersion}. It will walk you through the fields required to complete the migration and download the new project source code into the project source directory. It will also copy all of your existing files to a new directory (archive) in case you need access to your old files later.`, + describe: + 'Migrate an existing project to the new version of the projects framework.', errors: { - noApps: accountId => `No apps found in account ${accountId}`, - noAppsEligible: accountId => - `No apps in account ${accountId} are currently migratable`, - invalidAccountTypeTitle: () => - `${chalk.bold('Developer account not targeted')}`, - invalidAccountTypeDescription: (useCommand, authCommand) => - `Only public apps created in a developer account can be converted to a project component. Select a connected developer account with ${useCommand} or ${authCommand} and try again.`, - projectAlreadyExists: projectName => - `A project with name ${projectName} already exists. Please choose another name.`, - invalidApp: appId => - `Could not migrate appId ${appId}. This app cannot be migrated at this time. Please choose another public app.`, - appWithAppIdNotFound: appId => - `Could not find an app with the id ${appId} `, - notAllowedWithinProject: command => - `This command cannot be run from within a project directory. Run the command again from outside a project directory. If you are trying to migrate a project, run ${command}`, + noProjectConfig: command => + `No project detected. Please run this command again from a project directory. If you are trying to migrate an application, run ${command}`, + }, + examples: { + default: + 'Migrate an existing project to the new version of the projects framework.', }, }, cloneApp: { @@ -3430,9 +3423,9 @@ export const lib = { }, migrate: { componentsToBeMigrated: components => - `The following component types will be migrated: ${components}`, + `The following features will be migrated: ${components}`, componentsThatWillNotBeMigrated: components => - `[NOTE] These component types are not yet supported for migration but will be available later: ${components}`, + `[NOTE] These features are not yet supported for migration but will be available later: ${components}`, sourceContentsMoved: (newLocation: string) => `The contents of your old source directory have been moved to ${newLocation}, move any required files to the new source directory.`, projectMigrationWarning: `Migrating a project is irreversible and cannot be undone.`, diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 0f7815a83..8e418b0a2 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -10,7 +10,12 @@ import { mapToUserFacingType } from '@hubspot/project-parsing-lib/src/lib/transf import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration'; import { downloadProject } from '@hubspot/local-dev-lib/api/projects'; import { confirmPrompt, inputPrompt, listPrompt } from '../prompts/promptUtils'; -import { uiAccountDescription, uiCommandReference, uiLine } from '../ui'; +import { + uiAccountDescription, + uiCommandReference, + uiLine, + uiLink, +} from '../ui'; import { ensureProjectExists, LoadedProjectConfig } from '../projects'; import SpinniesManager from '../ui/SpinniesManager'; import { DEFAULT_POLLING_STATUS_LOOKUP, poll } from '../polling'; @@ -33,6 +38,10 @@ import { EnvironmentArgs, } from '../../types/Yargs'; import { hasFeature } from '../hasFeature'; +import { + getProjectBuildDetailUrl, + getProjectDetailUrl, +} from '../projects/urls'; export type MigrateAppArgs = CommonArgs & AccountArgs & @@ -341,7 +350,7 @@ async function beginMigration( const { componentHint, componentType } = component; uidMap[componentId] = await inputPrompt( lib.migrate.prompt.uidForComponent( - componentHint && componentHint + componentHint ? `${mapToUserFacingType(componentType)} '${componentHint}'` : mapToUserFacingType(componentType) ), @@ -553,6 +562,20 @@ export async function migrateApp2025_2( projectDest, projectConfig ); + + logger.log( + uiLink( + 'Project Details', + getProjectDetailUrl(projectName, derivedAccountId)! + ) + ); + + logger.log( + uiLink( + 'Build Details', + getProjectBuildDetailUrl(projectName, buildId, derivedAccountId)! + ) + ); } export function logInvalidAccountError(): void { From 1081cdcb78a31871efaeb7a160f069e1910f9a31 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 08:46:57 -0700 Subject: [PATCH 11/17] sort the apps based on if they are migratable, and add a separator --- lib/app/migrate.ts | 54 +++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 8e418b0a2..8477f8ad4 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -43,6 +43,8 @@ import { getProjectDetailUrl, } from '../projects/urls'; +const inquirer = require('inquirer'); + export type MigrateAppArgs = CommonArgs & AccountArgs & EnvironmentArgs & @@ -147,6 +149,40 @@ async function fetchMigrationApps( return allApps; } +async function promptForAppToMigrate(allApps: MigrationApp[]) { + const appChoices = allApps + .map(app => ({ + name: app.isMigratable + ? app.appName + : `[${chalk.yellow('DISABLED')}] ${app.appName} `, + value: app, + disabled: app.isMigratable + ? false + : getUnmigratableReason(app.unmigratableReason), + })) + .sort((a, b) => { + if (a.disabled === b.disabled) { + return 0; + } + return a.disabled ? 1 : -1; + }); + + const enabledChoices = appChoices.filter(app => !app.disabled); + const disabledChoices = appChoices.filter(app => app.disabled); + + const { appId: selectedAppId } = await listPrompt( + lib.migrate.prompt.chooseApp, + { + choices: [ + ...enabledChoices, + new inquirer.Separator(), + ...disabledChoices, + ], + } + ); + + return selectedAppId; +} async function selectAppToMigrate( allApps: MigrationApp[], appId?: number, @@ -161,25 +197,9 @@ async function selectAppToMigrate( throw new Error(lib.migrate.errors.appWithAppIdNotFound(appId)); } - const appChoices = allApps.map(app => ({ - name: app.isMigratable - ? app.appName - : `[${chalk.yellow('DISABLED')}] ${app.appName} `, - value: app, - disabled: app.isMigratable - ? false - : getUnmigratableReason(app.unmigratableReason), - })); - let appIdToMigrate = appId; if (!appIdToMigrate) { - const { appId: selectedAppId } = await listPrompt( - lib.migrate.prompt.chooseApp, - { - choices: appChoices, - } - ); - appIdToMigrate = selectedAppId; + appIdToMigrate = await promptForAppToMigrate(allApps); } const selectedApp = allApps.find(app => app.appId === appIdToMigrate); From a40ef9740c8348c3fbbc16682dcf300854ae2a63 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 09:38:40 -0700 Subject: [PATCH 12/17] Remove component detail errors since it is in flux --- lib/app/migrate.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index cce802d0c..8022b868b 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -424,16 +424,7 @@ async function finalizeMigration( }); if (isMigrationStatus(error) && error.status === MIGRATION_STATUS.FAILURE) { - const errorMessage = error.componentErrorDetails - ? `${error.projectErrorDetail} \n\t - ${Object.entries( - error.componentErrorDetails - ) - .map(([key, value]) => { - return `${mapToUserFacingType(key)}: ${value}`; - }) - .join('\n\t - ')}` - : error.projectErrorDetail; - throw new Error(errorMessage); + throw new Error(error.projectErrorDetail); } throw new Error(lib.migrate.errors.migrationFailed, { From a5e988c17b651e37785ddaf798ec2cde26dfe8d6 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 09:44:56 -0700 Subject: [PATCH 13/17] Update log message --- commands/project/migrateApp.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/project/migrateApp.ts b/commands/project/migrateApp.ts index e2ec9f310..ac981322e 100644 --- a/commands/project/migrateApp.ts +++ b/commands/project/migrateApp.ts @@ -22,16 +22,16 @@ export const describe = uiDeprecatedTag( ); export const deprecated = true; -export async function handler(yargs: ArgumentsCamelCase) { +export async function handler(options: ArgumentsCamelCase) { logger.warn( i18n(`commands.project.subcommands.migrateApp.deprecationWarning`, { oldCommand: uiCommandReference('hs project migrate-app'), newCommand: uiCommandReference( - 'hs app migrate --platform-version=2023.2' + `hs app migrate --platform-version=${options.platformVersion}` ), }) ); - await migrateHandler(yargs); + await migrateHandler(options); } export function builder(yargs: Argv): Argv { From 29b603c4f5408ec3b1cdd3d5472a46e97749b453 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 09:55:12 -0700 Subject: [PATCH 14/17] Remove redundant sort --- lib/app/migrate.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 8022b868b..553adb344 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -151,22 +151,15 @@ async function fetchMigrationApps( } async function promptForAppToMigrate(allApps: MigrationApp[]) { - const appChoices = allApps - .map(app => ({ - name: app.isMigratable - ? app.appName - : `[${chalk.yellow('DISABLED')}] ${app.appName} `, - value: app, - disabled: app.isMigratable - ? false - : getUnmigratableReason(app.unmigratableReason), - })) - .sort((a, b) => { - if (a.disabled === b.disabled) { - return 0; - } - return a.disabled ? 1 : -1; - }); + const appChoices = allApps.map(app => ({ + name: app.isMigratable + ? app.appName + : `[${chalk.yellow('DISABLED')}] ${app.appName} `, + value: app, + disabled: app.isMigratable + ? false + : getUnmigratableReason(app.unmigratableReason), + })); const enabledChoices = appChoices.filter(app => !app.disabled); const disabledChoices = appChoices.filter(app => app.disabled); From 7dafd63b113f1fc8f953ce42a739a36b6b22f0be Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 10:24:33 -0700 Subject: [PATCH 15/17] application -> app --- lang/en.lyaml | 2 +- lang/en.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/en.lyaml b/lang/en.lyaml index 700866c49..23a4f463e 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -629,7 +629,7 @@ en: name: describe: "Project name (cannot be changed)" header: - text: "This command will migrate an application to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing." + text: "This command will migrate an app to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing." link: "Learn more about migrating apps to the projects framework" deprecationWarning: "The {{ oldCommand }} command is deprecated and will be removed. Use {{ newCommand }} going forward." migrationStatus: diff --git a/lang/en.ts b/lang/en.ts index c84b11b96..c2c0e6f25 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -943,7 +943,7 @@ export const commands = { }, }, header: { - text: 'This command will migrate an application to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing.', + text: 'This command will migrate an app to the projects framework. It will walk you through the fields required to complete the migration and download the project source code into a directory of your choosing.', link: 'Learn more about migrating apps to the projects framework', }, deprecationWarning: (oldCommand, newCommand) => @@ -985,7 +985,7 @@ export const commands = { 'Migrate an existing project to the new version of the projects framework.', errors: { noProjectConfig: command => - `No project detected. Please run this command again from a project directory. If you are trying to migrate an application, run ${command}`, + `No project detected. Please run this command again from a project directory. If you are trying to migrate an app, run ${command}`, }, examples: { default: From c2eda82f9171435cb5015296b03366e6bcba53ec Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 11:07:31 -0700 Subject: [PATCH 16/17] chore: Add verbiage for linked to GH Repo --- lang/en.ts | 2 ++ lib/app/migrate.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lang/en.ts b/lang/en.ts index c2c0e6f25..7ca9de294 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -3444,6 +3444,8 @@ export const lib = { upToDate: 'App is already up to date', isPrivateApp: 'Private apps are not currently migratable', listedInMarketplace: 'Listed apps are not currently migratable', + projectConnectedToGitHub: + 'The app is linked to a GitHub repository. You will need to unlink it before migrating.', partOfProjectAlready: `This app is part of a project, run ${uiCommandReference('hs project migrate')} from the project directory to migrate it`, generic: reasonCode => `Unable to migrate app: ${reasonCode}`, }, diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index 553adb344..f5477bcc0 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -64,6 +64,8 @@ function getUnmigratableReason(reasonCode: string): string { return lib.migrate.errors.unmigratableReasons.isPrivateApp; case UNMIGRATABLE_REASONS.LISTED_IN_MARKETPLACE: return lib.migrate.errors.unmigratableReasons.listedInMarketplace; + case UNMIGRATABLE_REASONS.PROJECT_CONNECTED_TO_GITHUB: + return lib.migrate.errors.unmigratableReasons.projectConnectedToGitHub; case CLI_UNMIGRATABLE_REASONS.PART_OF_PROJECT_ALREADY: return lib.migrate.errors.unmigratableReasons.partOfProjectAlready; default: From cf90e63d5e896d66901e6f601f9eee87b8195153 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Thu, 24 Apr 2025 12:20:02 -0700 Subject: [PATCH 17/17] chore: Add error case for GH connected projects --- lang/en.ts | 11 +++++++---- lib/app/migrate.ts | 28 ++++++++++++++++++++++------ lib/projects/urls.ts | 8 ++++++++ package.json | 2 +- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lang/en.ts b/lang/en.ts index 8f1bf6c1e..4b2a8ac99 100644 --- a/lang/en.ts +++ b/lang/en.ts @@ -1,6 +1,7 @@ // @ts-nocheck import chalk from 'chalk'; -import { uiAccountDescription, uiCommandReference } from '../lib/ui'; +import { uiAccountDescription, uiCommandReference, uiLink } from '../lib/ui'; +import { getProjectSettingsUrl } from '../lib/projects/urls'; type LangFunction = (...args: (string | number)[]) => string; @@ -3451,9 +3452,11 @@ export const lib = { upToDate: 'App is already up to date', isPrivateApp: 'Private apps are not currently migratable', listedInMarketplace: 'Listed apps are not currently migratable', - projectConnectedToGitHub: - 'The app is linked to a GitHub repository. You will need to unlink it before migrating.', - partOfProjectAlready: `This app is part of a project, run ${uiCommandReference('hs project migrate')} from the project directory to migrate it`, + projectConnectedToGitHub: ( + projectName: string | undefined, + accountId: number + ) => + `The project is linked to a GitHub repository. ${uiLink('Visit the project settings page to unlink it', getProjectSettingsUrl(projectName, accountId))}`, partOfProjectAlready: `This app is part of a project, run ${uiCommandReference('hs project migrate')} from the project directory to migrate it`, generic: reasonCode => `Unable to migrate app: ${reasonCode}`, }, diff --git a/lib/app/migrate.ts b/lib/app/migrate.ts index f5477bcc0..a5ec4dd81 100644 --- a/lib/app/migrate.ts +++ b/lib/app/migrate.ts @@ -56,7 +56,11 @@ export type MigrateAppArgs = CommonArgs & platformVersion: string; }; -function getUnmigratableReason(reasonCode: string): string { +function getUnmigratableReason( + reasonCode: string, + projectName: string | undefined, + accountId: number +): string { switch (reasonCode) { case UNMIGRATABLE_REASONS.UP_TO_DATE: return lib.migrate.errors.unmigratableReasons.upToDate; @@ -65,7 +69,10 @@ function getUnmigratableReason(reasonCode: string): string { case UNMIGRATABLE_REASONS.LISTED_IN_MARKETPLACE: return lib.migrate.errors.unmigratableReasons.listedInMarketplace; case UNMIGRATABLE_REASONS.PROJECT_CONNECTED_TO_GITHUB: - return lib.migrate.errors.unmigratableReasons.projectConnectedToGitHub; + return lib.migrate.errors.unmigratableReasons.projectConnectedToGitHub( + projectName, + accountId + ); case CLI_UNMIGRATABLE_REASONS.PART_OF_PROJECT_ALREADY: return lib.migrate.errors.unmigratableReasons.partOfProjectAlready; default: @@ -129,7 +136,7 @@ async function fetchMigrationApps( if (allApps.length === 0 || !allApps.some(app => app.isMigratable)) { const reasons = filteredUnmigratableApps.map( app => - `${chalk.bold(app.appName)}: ${getUnmigratableReason(app.unmigratableReason)}` + `${chalk.bold(app.appName)}: ${getUnmigratableReason(app.unmigratableReason, app.projectName, derivedAccountId)}` ); throw new Error( @@ -152,7 +159,10 @@ async function fetchMigrationApps( return allApps; } -async function promptForAppToMigrate(allApps: MigrationApp[]) { +async function promptForAppToMigrate( + allApps: MigrationApp[], + derivedAccountId: number +) { const appChoices = allApps.map(app => ({ name: app.isMigratable ? app.appName @@ -160,7 +170,11 @@ async function promptForAppToMigrate(allApps: MigrationApp[]) { value: app, disabled: app.isMigratable ? false - : getUnmigratableReason(app.unmigratableReason), + : getUnmigratableReason( + app.unmigratableReason, + app.projectName, + derivedAccountId + ), })); const enabledChoices = appChoices.filter(app => !app.disabled); @@ -181,6 +195,7 @@ async function promptForAppToMigrate(allApps: MigrationApp[]) { } async function selectAppToMigrate( allApps: MigrationApp[], + derivedAccountId: number, appId?: number, projectConfig?: LoadedProjectConfig ): Promise<{ proceed: boolean; appIdToMigrate?: number }> { @@ -195,7 +210,7 @@ async function selectAppToMigrate( let appIdToMigrate = appId; if (!appIdToMigrate) { - appIdToMigrate = await promptForAppToMigrate(allApps); + appIdToMigrate = await promptForAppToMigrate(allApps, derivedAccountId); } const selectedApp = allApps.find(app => app.appId === appIdToMigrate); @@ -260,6 +275,7 @@ async function handleMigrationSetup( const { proceed, appIdToMigrate } = await selectAppToMigrate( allApps, + derivedAccountId, appId, projectConfig ); diff --git a/lib/projects/urls.ts b/lib/projects/urls.ts index 55e33bd42..15643c1d5 100644 --- a/lib/projects/urls.ts +++ b/lib/projects/urls.ts @@ -18,6 +18,14 @@ export function getProjectDetailUrl( return `${getProjectHomeUrl(accountId)}/project/${projectName}`; } +export function getProjectSettingsUrl( + projectName: string, + accountId: number +): string | undefined { + if (!projectName) return; + return `${getProjectDetailUrl(projectName, accountId)}/settings`; +} + export function getProjectActivityUrl( projectName: string, accountId: number diff --git a/package.json b/package.json index 2671a38d1..9a2658604 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "repository": "https://github.com/HubSpot/hubspot-cli", "dependencies": { - "@hubspot/local-dev-lib": "3.5.2", + "@hubspot/local-dev-lib": "3.5.3", "@hubspot/project-parsing-lib": "0.1.7", "@hubspot/serverless-dev-runtime": "7.0.2", "@hubspot/theme-preview-dev-server": "0.0.10",