@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.
- package/bin/cli.js +6 -1
- package/bin/hs.js +1 -0
- package/bin/hscms.js +1 -0
- package/bin/setupColor.d.ts +2 -0
- package/bin/setupColor.js +10 -0
- package/commands/getStarted.js +1 -1
- package/commands/project/create.js +4 -1
- package/commands/project/download.d.ts +3 -2
- package/commands/project/download.js +15 -2
- package/commands/project/migrate.js +0 -7
- package/commands/project/validate.d.ts +3 -2
- package/commands/project/validate.js +68 -15
- package/lang/en.d.ts +4 -1
- package/lang/en.js +4 -1
- package/lib/constants.d.ts +0 -1
- package/lib/constants.js +0 -1
- package/lib/filesystem.d.ts +6 -0
- package/lib/filesystem.js +9 -0
- package/lib/jsonOutput/download.d.ts +7 -0
- package/lib/jsonOutput/download.js +6 -0
- package/lib/jsonOutput/validate.d.ts +36 -0
- package/lib/jsonOutput/validate.js +20 -0
- package/lib/mcp/setup.js +11 -1
- package/lib/projects/projectProfiles.d.ts +1 -0
- package/lib/projects/projectProfiles.js +8 -0
- package/lib/projects/uieLinting.d.ts +2 -0
- package/lib/projects/uieLinting.js +13 -8
- package/lib/projects/upload.d.ts +5 -1
- package/lib/projects/upload.js +6 -1
- package/lib/projects/validate.d.ts +10 -1
- package/lib/projects/validate.js +76 -30
- package/lib/projects/validateLintConfigOnUpload.js +6 -5
- package/lib/projects/workspaces.js +6 -13
- package/lib/testUtils.d.ts +1 -0
- package/lib/testUtils.js +14 -0
- package/lib/theme/cmsDevServerProcess.js +1 -1
- package/lib/ui/SpinniesManager.d.ts +1 -0
- package/lib/ui/SpinniesManager.js +14 -4
- package/lib/ui/index.js +2 -2
- package/lib/ui/supportsColor.d.ts +4 -0
- package/lib/ui/supportsColor.js +25 -6
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
- package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -1
- package/mcp-server/tools/project/constants.d.ts +1 -1
- 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
package/bin/hscms.js
CHANGED
|
@@ -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
|
+
}
|
package/commands/getStarted.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
outputError(error);
|
|
25
54
|
return exit(EXIT_CODES.ERROR);
|
|
26
55
|
}
|
|
27
56
|
if (isLegacyProject(projectConfig.platformVersion)) {
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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: {
|
package/lib/constants.d.ts
CHANGED
|
@@ -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
|
};
|
package/lib/filesystem.d.ts
CHANGED
|
@@ -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,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
|
|
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
|
|
173
|
-
const srcDirAbsolute = path.resolve(
|
|
174
|
-
|
|
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() {
|
package/lib/projects/upload.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/lib/projects/upload.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {};
|
package/lib/projects/validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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 (
|
|
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
|
|
43
|
-
for (const profileName of
|
|
44
|
-
const
|
|
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
|
-
|
|
52
|
-
|
|
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(
|
|
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 (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
335
|
+
if (!isPathInsideDirectory(localPath, srcDir)) {
|
|
343
336
|
dirs.add(path.dirname(sourcePackageJsonPath));
|
|
344
337
|
}
|
|
345
338
|
}
|
package/lib/testUtils.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
@@ -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
|
-
|
|
50
|
-
process.stderr
|
|
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.
|
|
234
|
-
|
|
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 {
|
|
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:
|
|
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 {};
|
package/lib/ui/supportsColor.js
CHANGED
|
@@ -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";
|