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

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.
Files changed (45) hide show
  1. package/bin/cli.js +6 -1
  2. package/bin/hs.js +1 -0
  3. package/bin/hscms.js +1 -0
  4. package/bin/setupColor.d.ts +2 -0
  5. package/bin/setupColor.js +10 -0
  6. package/commands/getStarted.js +1 -1
  7. package/commands/project/create.js +4 -1
  8. package/commands/project/download.d.ts +3 -2
  9. package/commands/project/download.js +15 -2
  10. package/commands/project/migrate.js +0 -7
  11. package/commands/project/validate.d.ts +3 -2
  12. package/commands/project/validate.js +68 -15
  13. package/lang/en.d.ts +4 -1
  14. package/lang/en.js +4 -1
  15. package/lib/constants.d.ts +0 -1
  16. package/lib/constants.js +0 -1
  17. package/lib/filesystem.d.ts +6 -0
  18. package/lib/filesystem.js +9 -0
  19. package/lib/jsonOutput/download.d.ts +7 -0
  20. package/lib/jsonOutput/download.js +6 -0
  21. package/lib/jsonOutput/validate.d.ts +36 -0
  22. package/lib/jsonOutput/validate.js +20 -0
  23. package/lib/mcp/setup.js +11 -1
  24. package/lib/projects/projectProfiles.d.ts +1 -0
  25. package/lib/projects/projectProfiles.js +8 -0
  26. package/lib/projects/uieLinting.d.ts +2 -0
  27. package/lib/projects/uieLinting.js +13 -8
  28. package/lib/projects/upload.d.ts +5 -1
  29. package/lib/projects/upload.js +6 -1
  30. package/lib/projects/validate.d.ts +10 -1
  31. package/lib/projects/validate.js +76 -30
  32. package/lib/projects/validateLintConfigOnUpload.js +6 -5
  33. package/lib/projects/workspaces.js +6 -13
  34. package/lib/testUtils.d.ts +1 -0
  35. package/lib/testUtils.js +14 -0
  36. package/lib/theme/cmsDevServerProcess.js +1 -1
  37. package/lib/ui/SpinniesManager.d.ts +1 -0
  38. package/lib/ui/SpinniesManager.js +14 -4
  39. package/lib/ui/index.js +2 -2
  40. package/lib/ui/supportsColor.d.ts +4 -0
  41. package/lib/ui/supportsColor.js +25 -6
  42. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
  43. package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -1
  44. package/mcp-server/tools/project/constants.d.ts +1 -1
  45. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -33,6 +33,7 @@ import mcpCommand from '../commands/mcp.js';
33
33
  import upgradeCommand from '../commands/upgrade.js';
34
34
  import apiCommand from '../commands/api.js';
35
35
  import { uiLogger } from '../lib/ui/logger.js';
36
+ import { lib } from '../lang/en.js';
36
37
  import { initializeSpinniesManager } from '../lib/middleware/spinniesMiddleware.js';
37
38
  import { addCommandSuggestions } from '../lib/commandSuggestion.js';
38
39
  import { pkg } from '../lib/jsonLoader.js';
@@ -80,10 +81,14 @@ const argv = yargs(process.argv.slice(2))
80
81
  describe: 'prevent hyperlinks from displaying in the ui',
81
82
  hidden: true,
82
83
  type: 'boolean',
84
+ })
85
+ .option('color', {
86
+ default: true,
87
+ describe: lib.commonOpts.options.color,
88
+ type: 'boolean',
83
89
  })
84
90
  .option('noColor', {
85
91
  default: false,
86
- describe: 'prevent color from displaying in the ui',
87
92
  hidden: true,
88
93
  type: 'boolean',
89
94
  })
package/bin/hs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import './setupColor.js';
3
4
  import './silenceErrors.js';
4
5
 
5
6
  import './cli.js';
package/bin/hscms.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import './setupColor.js';
3
4
  import './silenceErrors.js';
4
5
 
5
6
  import './cli.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { hasColorDisableSignal } from '../lib/ui/supportsColor.js';
3
+ // Runs before cli.js so it executes before chalk is imported. chalk reads
4
+ // FORCE_COLOR (but not NO_COLOR / COLOR) once at load and caches its color
5
+ // level, and lang/en.ts bakes chalk styling into strings at import time. So
6
+ // translating a no-color request into FORCE_COLOR=0 here, ahead of those
7
+ // imports, is what makes NO_COLOR / COLOR actually disable color everywhere.
8
+ if (hasColorDisableSignal()) {
9
+ process.env.FORCE_COLOR = '0';
10
+ }
@@ -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
@@ -1376,6 +1376,7 @@ export declare const commands: {
1376
1376
  failedToConfigureVsCode: string;
1377
1377
  configuredVsCode: string;
1378
1378
  vsCodeNotFound: string;
1379
+ vsCodeVersionUnsupported: string;
1379
1380
  };
1380
1381
  otherInstructions: {
1381
1382
  header: string;
@@ -1646,7 +1647,6 @@ export declare const commands: {
1646
1647
  describe: string;
1647
1648
  errors: {
1648
1649
  noProjectConfig: (command: string) => string;
1649
- noThemeMigrationAccess: (accountId?: number) => string;
1650
1650
  };
1651
1651
  examples: {
1652
1652
  default: string;
@@ -1986,6 +1986,7 @@ export declare const commands: {
1986
1986
  describe: string;
1987
1987
  examples: {
1988
1988
  default: string;
1989
+ json: string;
1989
1990
  };
1990
1991
  logs: {
1991
1992
  downloadCancelled: string;
@@ -2097,6 +2098,7 @@ export declare const commands: {
2097
2098
  examples: {
2098
2099
  default: string;
2099
2100
  withProfile: string;
2101
+ json: string;
2100
2102
  };
2101
2103
  success: (projectName: string) => string;
2102
2104
  failure: (projectName: string, profileName?: string) => string;
@@ -3837,6 +3839,7 @@ export declare const lib: {
3837
3839
  jsonOutput: string;
3838
3840
  jsonSchema: string;
3839
3841
  debug: string;
3842
+ color: string;
3840
3843
  };
3841
3844
  };
3842
3845
  configMigrate: {
package/lang/en.js CHANGED
@@ -1392,6 +1392,7 @@ export const commands = {
1392
1392
  failedToConfigureVsCode: 'Failed to configure VSCode',
1393
1393
  configuredVsCode: 'Configured VSCode',
1394
1394
  vsCodeNotFound: "VSCode CLI is not installed (missing 'code' command). Install it and re-run hs mcp setup.",
1395
+ vsCodeVersionUnsupported: 'VSCode 1.99 or later is required for MCP server support. Please update VSCode and try again.',
1395
1396
  },
1396
1397
  otherInstructions: {
1397
1398
  header: 'To connect the HubSpot MCP server to another tool, add the following MCP server configuration.',
@@ -1662,7 +1663,6 @@ export const commands = {
1662
1663
  describe: 'Migrate an existing project to the new version of the projects framework.',
1663
1664
  errors: {
1664
1665
  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
1666
  },
1667
1667
  examples: {
1668
1668
  default: 'Migrate an existing project to the new version of the projects framework.',
@@ -2002,6 +2002,7 @@ export const commands = {
2002
2002
  describe: 'Download your project files from HubSpot.',
2003
2003
  examples: {
2004
2004
  default: 'Download the project myProject into myProjectFolder folder',
2005
+ json: 'Output the download result as JSON for scripting',
2005
2006
  },
2006
2007
  logs: {
2007
2008
  downloadCancelled: 'Cancelling project download',
@@ -2125,6 +2126,7 @@ export const commands = {
2125
2126
  examples: {
2126
2127
  default: 'Validate the project before uploading',
2127
2128
  withProfile: 'Validate the project with a profile before uploading.',
2129
+ json: 'Output validation results as JSON for scripting',
2128
2130
  },
2129
2131
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
2130
2132
  failure: (projectName, profileName) => `Project ${projectName} is invalid${profileName ? ` with profile "${profileName}" applied` : ''}`,
@@ -3876,6 +3878,7 @@ export const lib = {
3876
3878
  jsonOutput: 'Format output as JSON',
3877
3879
  jsonSchema: 'Print the JSON output schema and exit',
3878
3880
  debug: 'Set log level to debug',
3881
+ color: 'Enable colored output (use --no-color to disable)',
3879
3882
  },
3880
3883
  },
3881
3884
  configMigrate: {
@@ -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
+ });
package/lib/mcp/setup.js CHANGED
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import os from 'os';
10
10
  import fs from 'fs-extra';
11
11
  import { existsSync } from 'fs';
12
+ import semver from 'semver';
12
13
  const mcpServerName = MCP_SERVER_NAME;
13
14
  const claudeCode = 'claude';
14
15
  const cursor = 'cursor';
@@ -18,6 +19,7 @@ const codex = 'codex';
18
19
  const gemini = 'gemini';
19
20
  const opencode = 'opencode';
20
21
  const OTHER_TOOL = 'other';
22
+ const MINIMUM_VSCODE_VERSION = '1.99.0';
21
23
  const clientLabels = {
22
24
  codex: commands.mcp.setup.codex,
23
25
  claude: commands.mcp.setup.claudeCode,
@@ -204,11 +206,19 @@ function setupMcpConfigFile(config) {
204
206
  }
205
207
  }
206
208
  export async function setupVsCode(mcpCommand = defaultMcpCommand) {
209
+ const commandWithAgent = buildCommandWithAgentString(mcpCommand, vscode);
207
210
  try {
208
211
  SpinniesManager.add('vsCode', {
209
212
  text: commands.mcp.setup.spinners.configuringVsCode,
210
213
  });
211
- const commandWithAgent = buildCommandWithAgentString(mcpCommand, vscode);
214
+ const { stdout } = await execAsync('code --version');
215
+ const version = stdout.trim().split('\n')[0];
216
+ if (!semver.gte(version, MINIMUM_VSCODE_VERSION)) {
217
+ SpinniesManager.fail('vsCode', {
218
+ text: commands.mcp.setup.spinners.vsCodeVersionUnsupported,
219
+ });
220
+ return false;
221
+ }
212
222
  const configObject = {
213
223
  name: mcpServerName,
214
224
  ...commandWithAgent,
@@ -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
  }
@@ -11,4 +11,5 @@ type MockErrorResponse = {
11
11
  };
12
12
  export declare function mockHubSpotHttpResponse<T>(data?: any): HubSpotPromise<T>;
13
13
  export declare function mockHubSpotHttpError(message: string, response: MockErrorResponse): HubSpotHttpError;
14
+ export declare function stubColorEnv(): void;
14
15
  export {};
package/lib/testUtils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { vi } from 'vitest';
1
2
  import { HubSpotHttpError } from '@hubspot/local-dev-lib/models/HubSpotHttpError';
2
3
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
4
  export function mockHubSpotHttpResponse(data) {
@@ -16,3 +17,16 @@ export function mockHubSpotHttpError(message, response) {
16
17
  cause: { isAxiosError: true, response },
17
18
  });
18
19
  }
20
+ // Stubs the color-detection env to a color-capable interactive baseline.
21
+ // Call vi.unstubAllEnvs() in afterEach to restore.
22
+ export function stubColorEnv() {
23
+ [
24
+ 'CI',
25
+ 'NO_COLOR',
26
+ 'COLOR',
27
+ 'FORCE_COLOR',
28
+ 'COLORTERM',
29
+ 'TERM_PROGRAM',
30
+ ].forEach(key => vi.stubEnv(key, undefined));
31
+ vi.stubEnv('TERM', 'xterm-256color');
32
+ }
@@ -10,7 +10,7 @@ import { EXIT_CODES } from '../enums/exitCodes.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
  // cms-dev-server version to install to isolated cache
13
- const TARGET_CMS_DEV_SERVER_VERSION = '1.2.44';
13
+ const TARGET_CMS_DEV_SERVER_VERSION = '1.2.70';
14
14
  /**
15
15
  * Ensures cms-dev-server is installed in an isolated cache directory.
16
16
  * This prevents React version conflicts with the CLI.
@@ -13,6 +13,7 @@ declare class SpinniesManager {
13
13
  private currentFrameIndex;
14
14
  private spin;
15
15
  private sigintHandler;
16
+ private rawOutputCache;
16
17
  constructor();
17
18
  init(options?: Partial<SpinnerState>): void;
18
19
  private resetState;
@@ -12,6 +12,7 @@ import chalk from 'chalk';
12
12
  import cliCursor from 'cli-cursor';
13
13
  import { breakText, cleanStream, colorOptions, getLinesLength, prefixOptions, purgeSpinnerOptions, purgeSpinnersOptions, SPINNERS, writeStream, } from './spinniesUtils.js';
14
14
  import { isUnicodeSupported } from '@hubspot/local-dev-lib/isUnicodeSupported';
15
+ import { hasColorDisableSignal } from './supportsColor.js';
15
16
  import { uiLogger } from './logger.js';
16
17
  function safeColor(text, color) {
17
18
  const chalkFn = chalk[color];
@@ -31,6 +32,7 @@ class SpinniesManager {
31
32
  currentFrameIndex = 0;
32
33
  spin;
33
34
  sigintHandler = null;
35
+ rawOutputCache = {};
34
36
  constructor() {
35
37
  this.resetState();
36
38
  }
@@ -46,8 +48,8 @@ class SpinniesManager {
46
48
  this.spin =
47
49
  !this.options.disableSpins &&
48
50
  !process.env.CI &&
49
- process.stderr &&
50
- process.stderr.isTTY;
51
+ !hasColorDisableSignal() &&
52
+ Boolean(process.stderr?.isTTY);
51
53
  if (!this.hasAnySpinners()) {
52
54
  this.resetState();
53
55
  }
@@ -64,6 +66,7 @@ class SpinniesManager {
64
66
  this.stream = process.stderr;
65
67
  this.lineCount = 0;
66
68
  this.currentFrameIndex = 0;
69
+ this.rawOutputCache = {};
67
70
  }
68
71
  setDisableOutput(disableOutput) {
69
72
  this.disableOutput = disableOutput;
@@ -105,6 +108,7 @@ class SpinniesManager {
105
108
  return;
106
109
  }
107
110
  delete this.spinners[name];
111
+ delete this.rawOutputCache[name];
108
112
  // Update the spinner state to clean up the deleted spinner
109
113
  this.updateSpinnerState();
110
114
  }
@@ -230,8 +234,13 @@ class SpinniesManager {
230
234
  this.lineCount = linesLength.length;
231
235
  }
232
236
  setRawStreamOutput() {
233
- Object.values(this.spinners).forEach(i => {
234
- process.stderr.write(`- ${i.text}\n`);
237
+ Object.entries(this.spinners).forEach(([name, spinner]) => {
238
+ const line = `- ${spinner.text ?? ''}\n`;
239
+ if (this.rawOutputCache[name] === line) {
240
+ return;
241
+ }
242
+ this.rawOutputCache[name] = line;
243
+ process.stderr.write(line);
235
244
  });
236
245
  }
237
246
  checkIfActiveSpinners() {
@@ -246,6 +255,7 @@ class SpinniesManager {
246
255
  cliCursor.show();
247
256
  }
248
257
  this.spinners = {};
258
+ this.rawOutputCache = {};
249
259
  }
250
260
  }
251
261
  bindSigint() {
package/lib/ui/index.js CHANGED
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { getConfigAccountIfExists, globalConfigFileExists, } from '@hubspot/local-dev-lib/config';
3
3
  import { uiLogger } from './logger.js';
4
4
  import { supportsHyperlinkModule } from './supportHyperlinks.js';
5
- import { supportsColor } from './supportsColor.js';
5
+ import { isColorEnabled } from './supportsColor.js';
6
6
  import { uiMessages } from './uiMessages.js';
7
7
  import { HUBSPOT_ACCOUNT_TYPE_STRINGS } from '@hubspot/local-dev-lib/constants/config';
8
8
  export const UI_COLORS = {
@@ -16,7 +16,7 @@ export function uiLine() {
16
16
  export function getTerminalUISupport() {
17
17
  return {
18
18
  hyperlinks: supportsHyperlinkModule.stdout,
19
- color: supportsColor.stdout.hasBasic,
19
+ color: isColorEnabled(),
20
20
  };
21
21
  }
22
22
  export function uiLink(linkText, url) {
@@ -4,6 +4,7 @@ interface ColorSupportLevel {
4
4
  has256: boolean;
5
5
  has16m: boolean;
6
6
  }
7
+ export declare function hasColorDisableSignal(): boolean;
7
8
  declare function createSupportsColor(stream: {
8
9
  isTTY?: boolean;
9
10
  } | null, options?: {
@@ -14,4 +15,7 @@ export declare const supportsColor: {
14
15
  stdout: ColorSupportLevel;
15
16
  stderr: ColorSupportLevel;
16
17
  };
18
+ export declare function isColorEnabled(stream?: {
19
+ isTTY?: boolean;
20
+ } | null): boolean;
17
21
  export {};
@@ -19,17 +19,29 @@ function translateLevel(level) {
19
19
  has16m: level >= 3,
20
20
  };
21
21
  }
22
+ function resolveColorPreference() {
23
+ const { NO_COLOR, COLOR, FORCE_COLOR, TERM } = env;
24
+ if (hasFlag('no-color') || hasFlag('noColor'))
25
+ return 'never';
26
+ if (NO_COLOR)
27
+ return 'never';
28
+ if (COLOR === 'false' || COLOR === '0')
29
+ return 'never';
30
+ if (FORCE_COLOR !== undefined) {
31
+ return FORCE_COLOR === '0' || FORCE_COLOR === 'false' ? 'never' : 'always';
32
+ }
33
+ if (TERM === 'dumb')
34
+ return 'never';
35
+ return 'auto';
36
+ }
37
+ export function hasColorDisableSignal() {
38
+ return resolveColorPreference() === 'never';
39
+ }
22
40
  function _supportsColor(haveStream, { streamIsTTY } = {}) {
23
41
  if (haveStream && !streamIsTTY) {
24
42
  return 0;
25
43
  }
26
44
  const min = 0;
27
- if (env.TERM === 'dumb') {
28
- return min;
29
- }
30
- if (hasFlag('noColor')) {
31
- return 0;
32
- }
33
45
  if (process.platform === 'win32') {
34
46
  // Windows 10 build 10586 is the first Windows release that supports 256 colors.
35
47
  // Windows 10 build 14931 is the first release that supports 16m/TrueColor.
@@ -95,3 +107,10 @@ export const supportsColor = {
95
107
  stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
96
108
  stderr: createSupportsColor({ isTTY: tty.isatty(2) }),
97
109
  };
110
+ export function isColorEnabled(stream = process.stdout) {
111
+ const preference = resolveColorPreference();
112
+ if (preference !== 'auto') {
113
+ return preference === 'always';
114
+ }
115
+ return createSupportsColor(stream, { streamIsTTY: stream?.isTTY }).hasBasic;
116
+ }
@@ -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.2",
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",