@hubspot/cli 8.15.0-beta.0 → 8.15.0-beta.1

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.
@@ -117,7 +117,7 @@ async function handler(args) {
117
117
  // 4. Clone the project template from GitHub
118
118
  try {
119
119
  await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDest, {
120
- sourceDir: '2026.03/private-app-get-started-template',
120
+ sourceDir: '2026.09/private-app-get-started-template',
121
121
  hideLogs: true,
122
122
  });
123
123
  await trackCommandMetadataUsage('get-started', {
@@ -22,6 +22,7 @@ import { showMcpPromotionNudge } from '../../lib/mcp/promotion.js';
22
22
  const command = ['create', 'init'];
23
23
  const describe = commands.project.create.describe;
24
24
  const BETA_VERSIONS = [
25
+ PLATFORM_VERSIONS.v2027_03_BETA,
25
26
  PLATFORM_VERSIONS.v2026_09_BETA,
26
27
  PLATFORM_VERSIONS.v2026_03_BETA,
27
28
  ];
@@ -151,8 +152,10 @@ function projectCreateBuilder(yargs) {
151
152
  PLATFORM_VERSIONS.v2026_03_BETA,
152
153
  PLATFORM_VERSIONS.v2026_03,
153
154
  PLATFORM_VERSIONS.v2026_09_BETA,
155
+ PLATFORM_VERSIONS.v2026_09,
156
+ PLATFORM_VERSIONS.v2027_03_BETA,
154
157
  ],
155
- default: PLATFORM_VERSIONS.v2026_03,
158
+ default: PLATFORM_VERSIONS.v2026_09,
156
159
  },
157
160
  'project-base': {
158
161
  describe: commands.project.create.options.projectBase.describe,
@@ -1,5 +1,6 @@
1
- import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectDownloadArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ import { DownloadJsonOutput } from '../../lib/jsonOutput/download.js';
3
+ export type ProjectDownloadArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs<DownloadJsonOutput> & {
3
4
  project?: string;
4
5
  dest?: string;
5
6
  build?: number;
@@ -11,10 +11,11 @@ import { uiLogger } from '../../lib/ui/logger.js';
11
11
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
12
12
  import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler.js';
13
13
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
14
+ import { DownloadSchema, } from '../../lib/jsonOutput/download.js';
14
15
  const command = 'download';
15
16
  const describe = commands.project.download.describe;
16
17
  async function handler(args) {
17
- const { dest, build, derivedAccountId, exit } = args;
18
+ const { dest, build, derivedAccountId, exit, addJsonOutput } = args;
18
19
  const isInProjectDir = getIsInProject();
19
20
  if (isInProjectDir) {
20
21
  uiLogger.error(commands.project.download.warnings.cannotDownloadWithinProject);
@@ -41,6 +42,11 @@ async function handler(args) {
41
42
  : path.resolve(getCwd(), sanitizedProjectName);
42
43
  const { data: zippedProject } = await downloadProject(derivedAccountId, projectName, buildNumberToDownload);
43
44
  await extractZipArchive(zippedProject, sanitizeFileName(projectName), path.resolve(absoluteDestPath));
45
+ addJsonOutput({
46
+ projectName,
47
+ buildId: buildNumberToDownload,
48
+ dest: absoluteDestPath,
49
+ });
44
50
  uiLogger.log(commands.project.download.logs.downloadSucceeded(buildNumberToDownload, projectName));
45
51
  return exit(EXIT_CODES.SUCCESS);
46
52
  }
@@ -76,6 +82,10 @@ function projectDownloadBuilder(yargs) {
76
82
  '$0 project download --project=myProject --dest=myProjectFolder',
77
83
  commands.project.download.examples.default,
78
84
  ],
85
+ [
86
+ '$0 project download --project=myProject --json',
87
+ commands.project.download.examples.json,
88
+ ],
79
89
  ]);
80
90
  return yargs;
81
91
  }
@@ -84,11 +94,14 @@ const builder = makeYargsBuilder(projectDownloadBuilder, command, describe, {
84
94
  useConfigOptions: true,
85
95
  useAccountOptions: true,
86
96
  useEnvironmentOptions: true,
97
+ useJSONOutputOptions: true,
87
98
  });
88
99
  const projectDownloadCommand = {
89
100
  command,
90
101
  describe,
91
- handler: makeWrappedYargsHandler('project-download', handler),
102
+ handler: makeWrappedYargsHandler('project-download', handler, {
103
+ jsonOutputSchema: DownloadSchema,
104
+ }),
92
105
  builder,
93
106
  };
94
107
  export default projectDownloadCommand;
@@ -11,8 +11,6 @@ import { uiLogger } from '../../lib/ui/logger.js';
11
11
  import { renderInline } from '../../ui/render.js';
12
12
  import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
13
13
  import { getHasMigratableThemes, migrateThemesV2, } from '../../lib/theme/migrate.js';
14
- import { hasFeature } from '../../lib/hasFeature.js';
15
- import { FEATURES } from '../../lib/constants.js';
16
14
  import { trackCommandMetadataUsage } from '../../lib/usageTracking.js';
17
15
  const { v2025_2, v2026_03_BETA, v2026_03 } = PLATFORM_VERSIONS;
18
16
  const command = 'migrate';
@@ -35,11 +33,6 @@ async function handler(args) {
35
33
  try {
36
34
  const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes({ projectConfig, projectDir });
37
35
  if (hasMigratableThemes) {
38
- const hasThemeMigrationAccess = await hasFeature(derivedAccountId, FEATURES.THEME_MIGRATION_2025_2);
39
- if (!hasThemeMigrationAccess) {
40
- uiLogger.error(commands.project.migrate.errors.noThemeMigrationAccess(derivedAccountId));
41
- return exit(EXIT_CODES.ERROR);
42
- }
43
36
  await migrateThemesV2(derivedAccountId, {
44
37
  ...args,
45
38
  platformVersion: unstable
@@ -1,5 +1,6 @@
1
- import { CommonArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- export type ProjectValidateArgs = CommonArgs & {
1
+ import { CommonArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ import { ValidateJsonOutput } from '../../lib/jsonOutput/validate.js';
3
+ export type ProjectValidateArgs = CommonArgs & JSONOutputArgs<ValidateJsonOutput> & {
3
4
  profile?: string;
4
5
  };
5
6
  declare const projectValidateCommand: YargsCommandModule<unknown, ProjectValidateArgs>;
@@ -7,34 +7,83 @@ import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler
7
7
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
8
8
  import { commands } from '../../lang/en.js';
9
9
  import { logError } from '../../lib/errorHandlers/index.js';
10
- import { validateProject } from '../../lib/projects/validate.js';
10
+ import { validateProject, toIssue, } from '../../lib/projects/validate.js';
11
+ import { ValidateSchema, } from '../../lib/jsonOutput/validate.js';
11
12
  const command = 'validate';
12
13
  const describe = commands.project.validate.describe;
14
+ function generateJson({ result, projectConfig, profile, }) {
15
+ const { valid, errors, warnings, profiles } = result;
16
+ const output = { valid, errors, warnings };
17
+ if (projectConfig) {
18
+ output.projectName = projectConfig.name;
19
+ output.platformVersion = projectConfig.platformVersion;
20
+ }
21
+ if (profile) {
22
+ output.profile = profile;
23
+ }
24
+ if (profiles.length > 0) {
25
+ output.profiles = profiles;
26
+ }
27
+ return output;
28
+ }
13
29
  async function handler(args) {
14
- const { derivedAccountId, profile, exit, addUsageMetadata } = args;
15
- const accountConfig = getConfigAccountById(derivedAccountId);
16
- const accountType = accountConfig && accountConfig.accountType;
17
- addUsageMetadata({ type: accountType });
30
+ const { derivedAccountId, profile, exit, addUsageMetadata, formatOutputAsJson, addJsonOutput, } = args;
18
31
  let projectConfig;
19
32
  let projectDir;
33
+ function outputError(error) {
34
+ logError(error);
35
+ addJsonOutput(generateJson({
36
+ result: {
37
+ valid: false,
38
+ errors: [toIssue(error)],
39
+ warnings: [],
40
+ profiles: [],
41
+ },
42
+ projectConfig,
43
+ profile,
44
+ }));
45
+ }
20
46
  try {
47
+ const accountConfig = getConfigAccountById(derivedAccountId);
48
+ const accountType = accountConfig && accountConfig.accountType;
49
+ addUsageMetadata({ type: accountType });
21
50
  ({ projectConfig, projectDir } = getProjectConfig());
22
51
  }
23
52
  catch (error) {
24
- logError(error);
53
+ outputError(error);
25
54
  return exit(EXIT_CODES.ERROR);
26
55
  }
27
56
  if (isLegacyProject(projectConfig.platformVersion)) {
28
- uiLogger.error(commands.project.validate.badVersion);
57
+ const message = commands.project.validate.badVersion;
58
+ uiLogger.error(message);
59
+ addJsonOutput(generateJson({
60
+ result: {
61
+ valid: false,
62
+ errors: [{ message }],
63
+ warnings: [],
64
+ profiles: [],
65
+ },
66
+ projectConfig,
67
+ profile,
68
+ }));
29
69
  return exit(EXIT_CODES.ERROR);
30
70
  }
31
- const valid = await validateProject({
32
- projectConfig,
33
- projectDir,
34
- derivedAccountId,
35
- profile,
36
- });
37
- if (!valid) {
71
+ let result;
72
+ try {
73
+ result = await validateProject({
74
+ projectConfig,
75
+ projectDir,
76
+ derivedAccountId,
77
+ profile,
78
+ formatOutputAsJson,
79
+ });
80
+ }
81
+ catch (error) {
82
+ outputError(error);
83
+ return exit(EXIT_CODES.ERROR);
84
+ }
85
+ addJsonOutput(generateJson({ result, projectConfig, profile }));
86
+ if (!result.valid) {
38
87
  return exit(EXIT_CODES.ERROR);
39
88
  }
40
89
  uiLogger.success(commands.project.validate.success(projectConfig.name));
@@ -55,6 +104,7 @@ function projectValidateBuilder(yargs) {
55
104
  '$0 project validate --profile=profileName',
56
105
  commands.project.validate.examples.withProfile,
57
106
  ],
107
+ ['$0 project validate --json', commands.project.validate.examples.json],
58
108
  ]);
59
109
  return yargs;
60
110
  }
@@ -63,11 +113,14 @@ const builder = makeYargsBuilder(projectValidateBuilder, command, describe, {
63
113
  useConfigOptions: true,
64
114
  useAccountOptions: true,
65
115
  useEnvironmentOptions: true,
116
+ useJSONOutputOptions: true,
66
117
  });
67
118
  const projectValidateCommand = {
68
119
  command,
69
120
  describe,
70
- handler: makeWrappedYargsHandler('project-validate', handler),
121
+ handler: makeWrappedYargsHandler('project-validate', handler, {
122
+ jsonOutputSchema: ValidateSchema,
123
+ }),
71
124
  builder,
72
125
  };
73
126
  export default projectValidateCommand;
package/lang/en.d.ts CHANGED
@@ -1646,7 +1646,6 @@ export declare const commands: {
1646
1646
  describe: string;
1647
1647
  errors: {
1648
1648
  noProjectConfig: (command: string) => string;
1649
- noThemeMigrationAccess: (accountId?: number) => string;
1650
1649
  };
1651
1650
  examples: {
1652
1651
  default: string;
@@ -1986,6 +1985,7 @@ export declare const commands: {
1986
1985
  describe: string;
1987
1986
  examples: {
1988
1987
  default: string;
1988
+ json: string;
1989
1989
  };
1990
1990
  logs: {
1991
1991
  downloadCancelled: string;
@@ -2097,6 +2097,7 @@ export declare const commands: {
2097
2097
  examples: {
2098
2098
  default: string;
2099
2099
  withProfile: string;
2100
+ json: string;
2100
2101
  };
2101
2102
  success: (projectName: string) => string;
2102
2103
  failure: (projectName: string, profileName?: string) => string;
package/lang/en.js CHANGED
@@ -1662,7 +1662,6 @@ export const commands = {
1662
1662
  describe: 'Migrate an existing project to the new version of the projects framework.',
1663
1663
  errors: {
1664
1664
  noProjectConfig: (command) => `No project detected. Please run this command again from a project directory. If you are trying to migrate an app, run ${command}`,
1665
- 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))}`,
1666
1665
  },
1667
1666
  examples: {
1668
1667
  default: 'Migrate an existing project to the new version of the projects framework.',
@@ -2002,6 +2001,7 @@ export const commands = {
2002
2001
  describe: 'Download your project files from HubSpot.',
2003
2002
  examples: {
2004
2003
  default: 'Download the project myProject into myProjectFolder folder',
2004
+ json: 'Output the download result as JSON for scripting',
2005
2005
  },
2006
2006
  logs: {
2007
2007
  downloadCancelled: 'Cancelling project download',
@@ -2125,6 +2125,7 @@ export const commands = {
2125
2125
  examples: {
2126
2126
  default: 'Validate the project before uploading',
2127
2127
  withProfile: 'Validate the project with a profile before uploading.',
2128
+ json: 'Output validation results as JSON for scripting',
2128
2129
  },
2129
2130
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
2130
2131
  failure: (projectName, profileName) => `Project ${projectName} is invalid${profileName ? ` with profile "${profileName}" applied` : ''}`,
@@ -80,7 +80,6 @@ export declare const APP_AUTH_TYPES: {
80
80
  export declare const FEATURES: {
81
81
  readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
82
82
  readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
83
- readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
84
83
  readonly AGENT_TOOLS: "ThirdPartyAgentTools";
85
84
  readonly APP_ACTIONS: "Developers:AppActions";
86
85
  };
package/lib/constants.js CHANGED
@@ -72,7 +72,6 @@ export const APP_AUTH_TYPES = {
72
72
  export const FEATURES = {
73
73
  UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
74
74
  APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
75
- THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
76
75
  AGENT_TOOLS: 'ThirdPartyAgentTools',
77
76
  APP_ACTIONS: 'Developers:AppActions',
78
77
  };
@@ -1,2 +1,8 @@
1
1
  export declare function resolveLocalPath(filepath?: string): string;
2
+ /**
3
+ * Returns true if childPath is parentDirectory itself or a descendant of it.
4
+ * Compares resolved paths segment by segment, so sibling directories with a
5
+ * shared name prefix (foo-backup vs. foo) are not treated as descendants.
6
+ */
7
+ export declare function isPathInsideDirectory(childPath: string, parentDirectory: string): boolean;
2
8
  export declare function isPathFolder(path: string): boolean;
package/lib/filesystem.js CHANGED
@@ -7,6 +7,15 @@ export function resolveLocalPath(filepath) {
7
7
  : // Use CWD if optional filepath is not passed.
8
8
  getCwd();
9
9
  }
10
+ /**
11
+ * Returns true if childPath is parentDirectory itself or a descendant of it.
12
+ * Compares resolved paths segment by segment, so sibling directories with a
13
+ * shared name prefix (foo-backup vs. foo) are not treated as descendants.
14
+ */
15
+ export function isPathInsideDirectory(childPath, parentDirectory) {
16
+ const relativePath = path.relative(path.resolve(parentDirectory), path.resolve(childPath));
17
+ return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
18
+ }
10
19
  export function isPathFolder(path) {
11
20
  const splitPath = path.split('/');
12
21
  const fileOrFolderName = splitPath[splitPath.length - 1];
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export declare const DownloadSchema: z.ZodObject<{
3
+ projectName: z.ZodString;
4
+ buildId: z.ZodNumber;
5
+ dest: z.ZodString;
6
+ }, z.core.$strip>;
7
+ export type DownloadJsonOutput = z.infer<typeof DownloadSchema>;
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ export const DownloadSchema = z.object({
3
+ projectName: z.string(),
4
+ buildId: z.number(),
5
+ dest: z.string(),
6
+ });
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ declare const ValidationIssueSchema: z.ZodObject<{
3
+ message: z.ZodString;
4
+ file: z.ZodOptional<z.ZodString>;
5
+ profile: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ declare const ProfileValidationSchema: z.ZodObject<{
8
+ name: z.ZodString;
9
+ accountId: z.ZodOptional<z.ZodNumber>;
10
+ valid: z.ZodBoolean;
11
+ }, z.core.$strip>;
12
+ export declare const ValidateSchema: z.ZodObject<{
13
+ valid: z.ZodBoolean;
14
+ projectName: z.ZodOptional<z.ZodString>;
15
+ platformVersion: z.ZodOptional<z.ZodString>;
16
+ profile: z.ZodOptional<z.ZodString>;
17
+ errors: z.ZodArray<z.ZodObject<{
18
+ message: z.ZodString;
19
+ file: z.ZodOptional<z.ZodString>;
20
+ profile: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>;
22
+ warnings: z.ZodArray<z.ZodObject<{
23
+ message: z.ZodString;
24
+ file: z.ZodOptional<z.ZodString>;
25
+ profile: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>;
27
+ profiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
+ name: z.ZodString;
29
+ accountId: z.ZodOptional<z.ZodNumber>;
30
+ valid: z.ZodBoolean;
31
+ }, z.core.$strip>>>;
32
+ }, z.core.$strip>;
33
+ export type ValidationIssueJsonOutput = z.infer<typeof ValidationIssueSchema>;
34
+ export type ProfileValidationJsonOutput = z.infer<typeof ProfileValidationSchema>;
35
+ export type ValidateJsonOutput = z.infer<typeof ValidateSchema>;
36
+ export {};
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ const ValidationIssueSchema = z.object({
3
+ message: z.string(),
4
+ file: z.string().optional(),
5
+ profile: z.string().optional(),
6
+ });
7
+ const ProfileValidationSchema = z.object({
8
+ name: z.string(),
9
+ accountId: z.number().optional(),
10
+ valid: z.boolean(),
11
+ });
12
+ export const ValidateSchema = z.object({
13
+ valid: z.boolean(),
14
+ projectName: z.string().optional(),
15
+ platformVersion: z.string().optional(),
16
+ profile: z.string().optional(),
17
+ errors: z.array(ValidationIssueSchema),
18
+ warnings: z.array(ValidationIssueSchema),
19
+ profiles: z.array(ProfileValidationSchema).optional(),
20
+ });
@@ -3,6 +3,7 @@ import { ProjectConfig } from '../../types/Projects.js';
3
3
  export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
5
  export declare function loadProfile(projectConfig: ProjectConfig, projectDir: string, profileName: string): HsProfileFile | never;
6
+ export declare function getProfileAccountId(projectConfig: ProjectConfig, projectDir: string, profileName: string): number | undefined;
6
7
  export declare function loadAndValidateProfile(projectConfig: ProjectConfig, projectDir: string, profileName: string, silent?: boolean): Promise<HsProfileFile | never>;
7
8
  type ValidateProjectForProfileOptions = {
8
9
  projectConfig: ProjectConfig;
@@ -48,6 +48,14 @@ export function loadProfile(projectConfig, projectDir, profileName) {
48
48
  }
49
49
  return profile;
50
50
  }
51
+ export function getProfileAccountId(projectConfig, projectDir, profileName) {
52
+ try {
53
+ return loadProfile(projectConfig, projectDir, profileName).accountId;
54
+ }
55
+ catch (_e) {
56
+ return undefined;
57
+ }
58
+ }
51
59
  export async function loadAndValidateProfile(projectConfig, projectDir, profileName, silent = false) {
52
60
  if (!silent) {
53
61
  logProfileHeader(profileName);
@@ -24,6 +24,8 @@ export declare function hasEslintConfig(directory: string): boolean;
24
24
  export declare function hasDeprecatedEslintConfig(directory: string): boolean;
25
25
  export declare function getDeprecatedEslintConfigFiles(directory: string): string[];
26
26
  export declare function createEslintConfig(directory: string, platformVersion?: string | null): Promise<string>;
27
+ export declare function getUieComponentDirPrefixes(srcDir: string): string[];
28
+ export declare function isUieComponentDirectory(directory: string, srcDir: string): boolean;
27
29
  export declare function getUieLintablePackageJsonLocations(projectConfig: LoadedProjectConfig): Promise<string[]>;
28
30
  export declare const HUBSPOT_UI_EXTENSIONS_RULE_PREFIX = "@hubspot/ui-extensions/";
29
31
  export declare function isHubSpotEslintConfigActive(directory: string): Promise<boolean>;
@@ -9,9 +9,10 @@ import { commands } from '../../lang/en.js';
9
9
  import { uiLogger } from '../ui/logger.js';
10
10
  import { clearPackageJsonCache, safeGetPackageJsonCached, } from '../npm/packageJson.js';
11
11
  import { debugError } from '../errorHandlers/index.js';
12
+ import { isPathInsideDirectory } from '../filesystem.js';
12
13
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
14
  import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, } from '../constants.js';
14
- import { CARDS_KEY, Components, PAGES_KEY, SETTINGS_KEY, } from '@hubspot/project-parsing-lib/constants';
15
+ import { ACTIONS_KEY, CARDS_KEY, Components, PAGES_KEY, SETTINGS_KEY, } from '@hubspot/project-parsing-lib/constants';
15
16
  export const REQUIRED_PACKAGES_AND_MIN_VERSIONS = {
16
17
  eslint: '9.0.0',
17
18
  '@eslint/js': '9.0.0',
@@ -42,6 +43,7 @@ const DEPRECATED_ESLINT_CONFIG_FILES = [
42
43
  '.eslintrc',
43
44
  ];
44
45
  const UIE_COMPONENTS = [
46
+ Components[ACTIONS_KEY],
45
47
  Components[CARDS_KEY],
46
48
  Components[SETTINGS_KEY],
47
49
  Components[PAGES_KEY],
@@ -169,16 +171,19 @@ export async function createEslintConfig(directory, platformVersion) {
169
171
  throw error;
170
172
  }
171
173
  }
172
- export async function getUieLintablePackageJsonLocations(projectConfig) {
173
- const srcDirAbsolute = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
174
- const uiePackageDirPrefixes = UIE_COMPONENTS.map(component => path.join(srcDirAbsolute, component.parentComponent
174
+ export function getUieComponentDirPrefixes(srcDir) {
175
+ const srcDirAbsolute = path.resolve(srcDir);
176
+ return UIE_COMPONENTS.map(component => path.join(srcDirAbsolute, component.parentComponent
175
177
  ? Components[component.parentComponent].dir
176
178
  : '', component.dir));
179
+ }
180
+ export function isUieComponentDirectory(directory, srcDir) {
181
+ return getUieComponentDirPrefixes(srcDir).some(componentDir => isPathInsideDirectory(directory, componentDir));
182
+ }
183
+ export async function getUieLintablePackageJsonLocations(projectConfig) {
184
+ const srcDirAbsolute = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
177
185
  const allLocations = await getProjectPackageJsonLocations(projectConfig.projectDir);
178
- return allLocations.filter(location => {
179
- const resolvedLocation = path.resolve(location);
180
- return uiePackageDirPrefixes.some(prefix => resolvedLocation.startsWith(prefix));
181
- });
186
+ return allLocations.filter(location => isUieComponentDirectory(location, srcDirAbsolute));
182
187
  }
183
188
  export const HUBSPOT_UI_EXTENSIONS_RULE_PREFIX = '@hubspot/ui-extensions/';
184
189
  function getEnvironmentWithoutNpmConfig() {
@@ -25,7 +25,11 @@ type HandleProjectUploadArg<T> = {
25
25
  force?: boolean;
26
26
  };
27
27
  export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, skipNpmAudit, skipAutoDeploy, force, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
28
- export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig, projectDir: string): Promise<void>;
28
+ export type SourceDirectoryWarning = {
29
+ message: string;
30
+ file: string;
31
+ };
32
+ export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig, projectDir: string): Promise<SourceDirectoryWarning[]>;
29
33
  export declare function validateNoHSMetaMismatch(srcDir: string, projectConfig: ProjectConfig): Promise<void>;
30
34
  type HandleTranslateArg = {
31
35
  projectDir: string;
@@ -159,14 +159,19 @@ export async function validateSourceDirectory(srcDir, projectConfig, projectDir)
159
159
  if (!projectFilePaths || projectFilePaths.length === 0) {
160
160
  throw new ProjectValidationError(lib.projectUpload.handleProjectUpload.emptySource(projectConfig.srcDir));
161
161
  }
162
+ const warnings = [];
162
163
  if (!isLegacyProject(projectConfig.platformVersion)) {
163
164
  projectFilePaths.forEach(filePath => {
164
165
  const filename = path.basename(filePath);
165
166
  if (LEGACY_CONFIG_FILES.includes(filename)) {
166
- uiLogger.warn(lib.projectUpload.handleProjectUpload.legacyFileDetected(path.relative(projectDir, filePath), projectConfig.platformVersion));
167
+ const relativePath = path.relative(projectDir, filePath);
168
+ const message = lib.projectUpload.handleProjectUpload.legacyFileDetected(relativePath, projectConfig.platformVersion);
169
+ warnings.push({ message, file: relativePath });
170
+ uiLogger.warn(message);
167
171
  }
168
172
  });
169
173
  }
174
+ return warnings;
170
175
  }
171
176
  export async function validateNoHSMetaMismatch(srcDir, projectConfig) {
172
177
  const hasHsMetaFiles = await projectContainsHsMetaFiles(srcDir);
@@ -1,9 +1,18 @@
1
1
  import { ProjectConfig } from '../../types/Projects.js';
2
+ import { ProfileValidationJsonOutput, ValidationIssueJsonOutput } from '../jsonOutput/validate.js';
3
+ export type ProjectValidationResult = {
4
+ valid: boolean;
5
+ errors: ValidationIssueJsonOutput[];
6
+ warnings: ValidationIssueJsonOutput[];
7
+ profiles: ProfileValidationJsonOutput[];
8
+ };
2
9
  type ValidateProjectArgs = {
3
10
  projectConfig: ProjectConfig;
4
11
  projectDir: string;
5
12
  derivedAccountId: number;
6
13
  profile?: string;
14
+ formatOutputAsJson?: boolean;
7
15
  };
8
- export declare function validateProject({ projectConfig, projectDir, derivedAccountId, profile, }: ValidateProjectArgs): Promise<boolean>;
16
+ export declare function toIssue(error: unknown, profile?: string): ValidationIssueJsonOutput;
17
+ export declare function validateProject({ projectConfig, projectDir, derivedAccountId, profile, formatOutputAsJson, }: ValidateProjectArgs): Promise<ProjectValidationResult>;
9
18
  export {};
@@ -1,11 +1,21 @@
1
1
  import path from 'path';
2
+ import stripAnsi from 'strip-ansi';
2
3
  import { getAllHsProfiles } from '@hubspot/project-parsing-lib/profiles';
3
4
  import { uiLogger } from '../ui/logger.js';
4
5
  import SpinniesManager from '../ui/SpinniesManager.js';
5
- import { logError } from '../errorHandlers/index.js';
6
+ import { logError, getErrorMessage } from '../errorHandlers/index.js';
6
7
  import { commands } from '../../lang/en.js';
7
- import { validateProjectForProfile } from './projectProfiles.js';
8
+ import { getProfileAccountId, validateProjectForProfile, } from './projectProfiles.js';
8
9
  import { handleTranslate, validateSourceDirectory } from './upload.js';
10
+ export function toIssue(error, profile) {
11
+ const issue = {
12
+ message: stripAnsi(getErrorMessage(error)).trim(),
13
+ };
14
+ if (profile) {
15
+ issue.profile = profile;
16
+ }
17
+ return issue;
18
+ }
9
19
  function logValidationErrors(validationErrors) {
10
20
  uiLogger.log('');
11
21
  validationErrors.forEach(error => {
@@ -17,41 +27,73 @@ function logValidationErrors(validationErrors) {
17
27
  }
18
28
  });
19
29
  }
20
- export async function validateProject({ projectConfig, projectDir, derivedAccountId, profile, }) {
30
+ async function validateProfile({ projectConfig, projectDir, derivedAccountId, profileName, formatOutputAsJson, indentSpinners, }) {
31
+ const rawErrors = await validateProjectForProfile({
32
+ projectConfig,
33
+ projectDir,
34
+ profileName,
35
+ derivedAccountId,
36
+ ...(indentSpinners ? { indentSpinners: true } : {}),
37
+ });
38
+ const accountId = formatOutputAsJson
39
+ ? getProfileAccountId(projectConfig, projectDir, profileName)
40
+ : undefined;
41
+ const profile = {
42
+ name: profileName,
43
+ valid: rawErrors.length === 0,
44
+ };
45
+ if (accountId !== undefined) {
46
+ profile.accountId = accountId;
47
+ }
48
+ return {
49
+ profile,
50
+ errors: rawErrors.map(e => toIssue(e, profileName)),
51
+ rawErrors,
52
+ };
53
+ }
54
+ export async function validateProject({ projectConfig, projectDir, derivedAccountId, profile, formatOutputAsJson, }) {
55
+ const errors = [];
56
+ const warnings = [];
57
+ const profiles = [];
21
58
  let valid = true;
22
59
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
23
- const profiles = await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir));
24
- // If a profile is specified, only validate that profile
60
+ const projectProfiles = await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir));
25
61
  if (profile) {
26
- const validationErrors = await validateProjectForProfile({
62
+ const result = await validateProfile({
27
63
  projectConfig,
28
64
  projectDir,
29
- profileName: profile,
30
65
  derivedAccountId,
66
+ profileName: profile,
67
+ formatOutputAsJson,
68
+ indentSpinners: false,
31
69
  });
32
- if (validationErrors.length) {
33
- logValidationErrors(validationErrors);
70
+ profiles.push(result.profile);
71
+ if (result.rawErrors.length) {
34
72
  valid = false;
73
+ errors.push(...result.errors);
74
+ logValidationErrors(result.rawErrors);
35
75
  }
36
76
  }
37
- else if (profiles.length > 0) {
38
- // If no profile was specified and the project has profiles, validate all of them
77
+ else if (projectProfiles.length > 0) {
39
78
  SpinniesManager.add('validatingAllProfiles', {
40
79
  text: commands.project.validate.spinners.validatingAllProfiles,
41
80
  });
42
- const errors = [];
43
- for (const profileName of profiles) {
44
- const validationErrors = await validateProjectForProfile({
81
+ const rawErrors = [];
82
+ for (const profileName of projectProfiles) {
83
+ const result = await validateProfile({
45
84
  projectConfig,
46
85
  projectDir,
47
- profileName,
48
86
  derivedAccountId,
87
+ profileName,
88
+ formatOutputAsJson,
49
89
  indentSpinners: true,
50
90
  });
51
- if (validationErrors.length) {
52
- errors.push(...validationErrors);
91
+ profiles.push(result.profile);
92
+ if (result.rawErrors.length) {
53
93
  valid = false;
94
+ errors.push(...result.errors);
54
95
  }
96
+ rawErrors.push(...result.rawErrors);
55
97
  }
56
98
  if (valid) {
57
99
  SpinniesManager.succeed('validatingAllProfiles', {
@@ -63,10 +105,9 @@ export async function validateProject({ projectConfig, projectDir, derivedAccoun
63
105
  text: commands.project.validate.spinners.allProfilesValidationFailed,
64
106
  });
65
107
  }
66
- logValidationErrors(errors);
108
+ logValidationErrors(rawErrors);
67
109
  }
68
110
  else {
69
- // If the project has no profiles, validate the project without a profile
70
111
  try {
71
112
  await handleTranslate({
72
113
  projectDir,
@@ -76,21 +117,26 @@ export async function validateProject({ projectConfig, projectDir, derivedAccoun
76
117
  });
77
118
  }
78
119
  catch (e) {
120
+ valid = false;
121
+ errors.push(toIssue(e));
79
122
  uiLogger.error(commands.project.validate.failure(projectConfig.name));
80
123
  logError(e);
81
- valid = false;
82
124
  uiLogger.log('');
83
125
  }
84
126
  }
85
- if (!valid) {
86
- return false;
87
- }
88
- try {
89
- await validateSourceDirectory(srcDir, projectConfig, projectDir);
90
- }
91
- catch (e) {
92
- logError(e);
93
- return false;
127
+ if (valid) {
128
+ try {
129
+ const sourceWarnings = await validateSourceDirectory(srcDir, projectConfig, projectDir);
130
+ warnings.push(...sourceWarnings.map(warning => ({
131
+ message: stripAnsi(warning.message).trim(),
132
+ file: warning.file,
133
+ })));
134
+ }
135
+ catch (e) {
136
+ valid = false;
137
+ errors.push(toIssue(e));
138
+ logError(e);
139
+ }
94
140
  }
95
- return true;
141
+ return { valid, errors, warnings, profiles };
96
142
  }
@@ -1,18 +1,19 @@
1
1
  import path from 'path';
2
2
  import { lib } from '../../lang/en.js';
3
3
  import { uiLogger } from '../ui/logger.js';
4
- import { areAllLintPackagesInstalled, hasEslintConfig, isHubSpotEslintConfigActive, } from './uieLinting.js';
4
+ import { areAllLintPackagesInstalled, hasEslintConfig, isHubSpotEslintConfigActive, isUieComponentDirectory, } from './uieLinting.js';
5
5
  export async function validateLintConfigOnUpload({ srcDir, projectDir, parsedPackageJsons, isLegacyPlatform, }) {
6
6
  const lintRoots = new Set();
7
7
  if (isLegacyPlatform) {
8
8
  lintRoots.add(srcDir);
9
9
  }
10
10
  else {
11
+ // Only check the directories that `hs project lint` actually covers,
12
+ // otherwise these warnings can never be resolved.
11
13
  for (const { dir } of parsedPackageJsons) {
12
- lintRoots.add(dir);
13
- }
14
- if (lintRoots.size === 0) {
15
- lintRoots.add(srcDir);
14
+ if (isUieComponentDirectory(dir, srcDir)) {
15
+ lintRoots.add(dir);
16
+ }
16
17
  }
17
18
  }
18
19
  let hasAnyOutput = false;
@@ -4,6 +4,7 @@ import crypto from 'crypto';
4
4
  import { shouldIgnoreFile } from '@hubspot/local-dev-lib/ignoreRules';
5
5
  import { getPackableFiles, } from '@hubspot/project-parsing-lib/workspaces';
6
6
  import { uiLogger } from '../ui/logger.js';
7
+ import { isPathInsideDirectory } from '../filesystem.js';
7
8
  import { lib } from '../../lang/en.js';
8
9
  const FILE_PROTOCOL_PREFIX = 'file:';
9
10
  const LINK_PROTOCOL_PREFIX = 'link:';
@@ -62,14 +63,6 @@ export function computeExternalArchivePath(absolutePath, kind = KIND_DIRECTORY)
62
63
  }
63
64
  return path.posix.join('_workspaces', `${name}-${shortHash(resolved)}`);
64
65
  }
65
- /**
66
- * Returns true if dir is inside srcDir (i.e. it will already be included
67
- * in the archive from the srcDir walk and must not be copied again).
68
- */
69
- function isInsideSrcDir(dir, srcDir) {
70
- const rel = path.relative(path.resolve(srcDir), path.resolve(dir));
71
- return !rel.startsWith('..') && !path.isAbsolute(rel);
72
- }
73
66
  /**
74
67
  * Creates a file filter function for workspace archiving.
75
68
  * Filters files based on packable files list and ignore rules.
@@ -108,7 +101,7 @@ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
108
101
  if (!packageWorkspaceEntries.has(sourcePackageJsonPath)) {
109
102
  packageWorkspaceEntries.set(sourcePackageJsonPath, []);
110
103
  }
111
- if (isInsideSrcDir(workspaceDir, srcDir)) {
104
+ if (isPathInsideDirectory(workspaceDir, srcDir)) {
112
105
  // Internal: already in archive from srcDir walk.
113
106
  // Store the relative path from the package.json directory so npm can resolve it.
114
107
  const relPath = toPosixPath(path.relative(path.dirname(sourcePackageJsonPath), path.resolve(workspaceDir)));
@@ -161,7 +154,7 @@ async function archiveFileDependencies(archive, srcDir, fileDependencyMappings,
161
154
  const toArchive = [];
162
155
  for (const mapping of fileDependencyMappings) {
163
156
  const { packageName, localPath, sourcePackageJsonPath, kind, protocol } = mapping;
164
- if (isInsideSrcDir(localPath, srcDir)) {
157
+ if (isPathInsideDirectory(localPath, srcDir)) {
165
158
  continue;
166
159
  }
167
160
  const archivePath = computeExternalArchivePath(localPath, kind);
@@ -325,7 +318,7 @@ export function getPackageJsonPathsToUpdate(srcDir, workspaceMappings, fileDepen
325
318
  paths.add(toPosixPath(path.relative(srcDir, sourcePackageJsonPath)));
326
319
  }
327
320
  for (const { localPath, sourcePackageJsonPath } of fileDependencyMappings) {
328
- if (!isInsideSrcDir(localPath, srcDir)) {
321
+ if (!isPathInsideDirectory(localPath, srcDir)) {
329
322
  paths.add(toPosixPath(path.relative(srcDir, sourcePackageJsonPath)));
330
323
  }
331
324
  }
@@ -334,12 +327,12 @@ export function getPackageJsonPathsToUpdate(srcDir, workspaceMappings, fileDepen
334
327
  function getDirsWithExternalDeps(srcDir, workspaceMappings, fileDependencyMappings) {
335
328
  const dirs = new Set();
336
329
  for (const { workspaceDir, sourcePackageJsonPath } of workspaceMappings) {
337
- if (!isInsideSrcDir(workspaceDir, srcDir)) {
330
+ if (!isPathInsideDirectory(workspaceDir, srcDir)) {
338
331
  dirs.add(path.dirname(sourcePackageJsonPath));
339
332
  }
340
333
  }
341
334
  for (const { localPath, sourcePackageJsonPath } of fileDependencyMappings) {
342
- if (!isInsideSrcDir(localPath, srcDir)) {
335
+ if (!isPathInsideDirectory(localPath, srcDir)) {
343
336
  dirs.add(path.dirname(sourcePackageJsonPath));
344
337
  }
345
338
  }
@@ -18,9 +18,9 @@ declare const inputSchemaZodObject: z.ZodObject<{
18
18
  features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
19
19
  card: "card";
20
20
  settings: "settings";
21
+ "crm-bulk-action": "crm-bulk-action";
21
22
  "app-event": "app-event";
22
23
  "workflow-action-tool": "workflow-action-tool";
23
- "crm-bulk-action": "crm-bulk-action";
24
24
  page: "page";
25
25
  webhooks: "webhooks";
26
26
  "app-function": "app-function";
@@ -22,9 +22,9 @@ declare const inputSchemaZodObject: z.ZodObject<{
22
22
  features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
23
23
  card: "card";
24
24
  settings: "settings";
25
+ "crm-bulk-action": "crm-bulk-action";
25
26
  "app-event": "app-event";
26
27
  "workflow-action-tool": "workflow-action-tool";
27
- "crm-bulk-action": "crm-bulk-action";
28
28
  page: "page";
29
29
  webhooks: "webhooks";
30
30
  "app-function": "app-function";
@@ -4,9 +4,9 @@ export declare const absoluteCurrentWorkingDirectory: z.ZodString;
4
4
  export declare const features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
5
5
  card: "card";
6
6
  settings: "settings";
7
+ "crm-bulk-action": "crm-bulk-action";
7
8
  "app-event": "app-event";
8
9
  "workflow-action-tool": "workflow-action-tool";
9
- "crm-bulk-action": "crm-bulk-action";
10
10
  page: "page";
11
11
  webhooks: "webhooks";
12
12
  "app-function": "app-function";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.15.0-beta.0",
3
+ "version": "8.15.0-beta.1",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",