@hubspot/cli 8.1.2-experimental.2 → 8.1.3-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,9 +15,7 @@ import { commands } from '../../lang/en.js';
15
15
  import { debugError } from '../../lib/errorHandlers/index.js';
16
16
  import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler.js';
17
17
  const command = 'link';
18
- // Hide the command until we're done testing and ready to make linking GA
19
- // const describe = commands.account.subcommands.link.describe;
20
- const describe = undefined;
18
+ const describe = commands.account.subcommands.link.describe;
21
19
  async function handler(args) {
22
20
  const { exit } = args;
23
21
  if (hasDeprecatedConfigConflict(args._)) {
@@ -11,9 +11,7 @@ import { commands } from '../../lang/en.js';
11
11
  import { DEFAULT_HS_SETTINGS_PATH } from '@hubspot/local-dev-lib/constants/config';
12
12
  import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler.js';
13
13
  const command = 'unlink';
14
- // Hide the command until we're done testing and ready to make linking GA
15
- // const describe = commands.account.subcommands.link.describe;
16
- const describe = undefined;
14
+ const describe = commands.account.subcommands.unlink.describe;
17
15
  async function handler(args) {
18
16
  const { exit } = args;
19
17
  if (hasDeprecatedConfigConflict(args._)) {
@@ -1,4 +1,4 @@
1
- import { AccountArgs, CommonArgs, ConfigArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectInstallStatusArgs = CommonArgs & ConfigArgs & AccountArgs & JSONOutputArgs;
1
+ import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ type ProjectInstallStatusArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs;
3
3
  declare const projectInstallStatusCommand: YargsCommandModule<unknown, ProjectInstallStatusArgs>;
4
4
  export default projectInstallStatusCommand;
@@ -121,6 +121,7 @@ const builder = makeYargsBuilder(projectInstallStatusBuilder, command, describe,
121
121
  useGlobalOptions: true,
122
122
  useConfigOptions: true,
123
123
  useAccountOptions: true,
124
+ useEnvironmentOptions: true,
124
125
  useJSONOutputOptions: true,
125
126
  });
126
127
  const projectInstallStatusCommand = {
@@ -1,4 +1,4 @@
1
- import { AccountArgs, CommonArgs, ConfigArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectInfoArgs = CommonArgs & ConfigArgs & AccountArgs & JSONOutputArgs;
1
+ import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ type ProjectInfoArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs;
3
3
  declare const projectInfoCommand: YargsCommandModule<unknown, ProjectInfoArgs>;
4
4
  export default projectInfoCommand;
@@ -56,6 +56,7 @@ const builder = makeYargsBuilder(projectInfoBuilder, command, verboseDescribe, {
56
56
  useGlobalOptions: true,
57
57
  useConfigOptions: true,
58
58
  useAccountOptions: true,
59
+ useEnvironmentOptions: true,
59
60
  useJSONOutputOptions: true,
60
61
  });
61
62
  const projectInfoCommand = {
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { getProjectPackageJsonLocations, installPackages, } from '../../lib/dependencyManagement.js';
2
+ import { installPackages } from '../../lib/dependencyManagement.js';
3
3
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
4
4
  import { isPromptExitError } from '../../lib/errors/PromptExitError.js';
5
5
  import { getProjectConfig } from '../../lib/projects/config.js';
@@ -10,7 +10,7 @@ import { logError } from '../../lib/errorHandlers/index.js';
10
10
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
11
11
  import { promptUser } from '../../lib/prompts/promptUtils.js';
12
12
  import SpinniesManager from '../../lib/ui/SpinniesManager.js';
13
- import { areAllLintPackagesInstalled, getMissingLintPackages, getMissingLintScripts, addLintScriptsToPackageJson, lintPackages, displayLintResults, hasEslintConfig, hasDeprecatedEslintConfig, getDeprecatedEslintConfigFiles, createEslintConfig, REQUIRED_PACKAGES_AND_MIN_VERSIONS, } from '../../lib/projects/uieLinting.js';
13
+ import { addLintScriptsToPackageJson, areAllLintPackagesInstalled, createEslintConfig, displayLintResults, getDeprecatedEslintConfigFiles, getMissingLintPackages, getMissingLintScripts, getUieLintablePackageJsonLocations, hasDeprecatedEslintConfig, hasEslintConfig, lintPackages, REQUIRED_PACKAGES_AND_MIN_VERSIONS, } from '../../lib/projects/uieLinting.js';
14
14
  import { clearPackageJsonCache } from '../../lib/npm/packageJson.js';
15
15
  const command = 'lint';
16
16
  const describe = commands.project.lint.help.describe;
@@ -26,7 +26,7 @@ async function handler(args) {
26
26
  SpinniesManager.add('lintCheck', {
27
27
  text: commands.project.lint.loading.checking,
28
28
  });
29
- const lintLocations = await getProjectPackageJsonLocations();
29
+ const lintLocations = await getUieLintablePackageJsonLocations(projectConfig);
30
30
  const locationsReadyToLint = [];
31
31
  const locationsNeedingPackages = new Map();
32
32
  for (const lintLocation of lintLocations) {
@@ -0,0 +1,7 @@
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../../types/Yargs.js';
2
+ export type ProjectReleaseCreateArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs & {
3
+ build?: number;
4
+ force: boolean;
5
+ };
6
+ declare const projectReleaseCreateCommand: YargsCommandModule<unknown, ProjectReleaseCreateArgs>;
7
+ export default projectReleaseCreateCommand;
@@ -0,0 +1,159 @@
1
+ import { fetchProject, getBuildStatus, } from '@hubspot/local-dev-lib/api/projects';
2
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
3
+ import { logError, ApiErrorContext } from '../../../lib/errorHandlers/index.js';
4
+ import { getProjectConfig, validateProjectConfig, } from '../../../lib/projects/config.js';
5
+ import { confirmPrompt } from '../../../lib/prompts/promptUtils.js';
6
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
7
+ import { uiLogger } from '../../../lib/ui/logger.js';
8
+ import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
9
+ import { commands } from '../../../lang/en.js';
10
+ import { makeWrappedYargsHandler } from '../../../lib/yargs/makeWrappedYargsHandler.js';
11
+ import { createRelease } from '../../../api/releases.js';
12
+ const command = 'create';
13
+ // const describe = commands.project.release.create.describe;
14
+ const describe = undefined;
15
+ // const verboseDescribe = commands.project.release.create.verboseDescribe;
16
+ const verboseDescribe = undefined;
17
+ async function resolveBuildId(accountId, projectName, buildOption) {
18
+ const { data: { deployedBuildId }, } = await fetchProject(accountId, projectName);
19
+ if (buildOption) {
20
+ return buildOption;
21
+ }
22
+ if (!deployedBuildId) {
23
+ throw new Error(commands.project.release.create.errors.noDeployedBuild);
24
+ }
25
+ return deployedBuildId;
26
+ }
27
+ async function validateBuild(accountId, projectName, buildId) {
28
+ try {
29
+ await getBuildStatus(accountId, projectName, buildId);
30
+ }
31
+ catch (e) {
32
+ if (isHubSpotHttpError(e) && e.status === 404) {
33
+ uiLogger.error(commands.project.release.create.errors.buildNotFound(buildId, projectName));
34
+ }
35
+ else {
36
+ logError(e, new ApiErrorContext({
37
+ accountId,
38
+ request: 'project release create',
39
+ }));
40
+ }
41
+ throw e;
42
+ }
43
+ }
44
+ async function executeRelease(accountId, projectName, buildId, formatOutputAsJson) {
45
+ try {
46
+ const { data: release } = await createRelease(accountId, projectName, buildId);
47
+ if (formatOutputAsJson) {
48
+ uiLogger.json(release);
49
+ }
50
+ else {
51
+ uiLogger.success(commands.project.release.create.success(release.releaseTag, release.buildId));
52
+ }
53
+ }
54
+ catch (e) {
55
+ if (isHubSpotHttpError(e) && e.status === 422) {
56
+ uiLogger.error(commands.project.release.create.errors.buildNotDeployed(buildId));
57
+ }
58
+ else {
59
+ logError(e, new ApiErrorContext({
60
+ accountId,
61
+ request: 'project release create',
62
+ }));
63
+ }
64
+ throw e;
65
+ }
66
+ }
67
+ async function handler(args) {
68
+ const { exit, derivedAccountId, build: buildOption, json: formatOutputAsJson, force, } = args;
69
+ const { projectConfig, projectDir } = await getProjectConfig();
70
+ try {
71
+ validateProjectConfig(projectConfig, projectDir);
72
+ }
73
+ catch (error) {
74
+ logError(error);
75
+ return exit(EXIT_CODES.ERROR);
76
+ }
77
+ const projectName = projectConfig.name;
78
+ let buildId;
79
+ try {
80
+ buildId = await resolveBuildId(derivedAccountId, projectName, buildOption);
81
+ }
82
+ catch (e) {
83
+ if (isHubSpotHttpError(e) && e.status === 404) {
84
+ uiLogger.error(commands.project.release.create.errors.projectNotFound(derivedAccountId, projectName));
85
+ }
86
+ else if (!(e instanceof Error) || !e.message) {
87
+ logError(e, new ApiErrorContext({
88
+ accountId: derivedAccountId,
89
+ request: 'project release create',
90
+ }));
91
+ }
92
+ else {
93
+ uiLogger.error(e.message);
94
+ }
95
+ return exit(EXIT_CODES.ERROR);
96
+ }
97
+ if (buildOption) {
98
+ try {
99
+ await validateBuild(derivedAccountId, projectName, buildId);
100
+ }
101
+ catch {
102
+ return exit(EXIT_CODES.ERROR);
103
+ }
104
+ }
105
+ if (!formatOutputAsJson && !force) {
106
+ const confirmed = await confirmPrompt(commands.project.release.create.confirmPrompt(projectName, buildId));
107
+ if (!confirmed) {
108
+ uiLogger.log(commands.project.release.create.cancelled);
109
+ return exit(EXIT_CODES.SUCCESS);
110
+ }
111
+ }
112
+ try {
113
+ await executeRelease(derivedAccountId, projectName, buildId, !!formatOutputAsJson);
114
+ }
115
+ catch {
116
+ return exit(EXIT_CODES.ERROR);
117
+ }
118
+ return exit(EXIT_CODES.SUCCESS);
119
+ }
120
+ function projectReleaseCreateBuilder(yargs) {
121
+ yargs.options({
122
+ build: {
123
+ alias: ['build-id'],
124
+ describe: commands.project.release.create.options.build,
125
+ type: 'number',
126
+ },
127
+ force: {
128
+ alias: ['f'],
129
+ describe: commands.project.release.create.options.force,
130
+ default: false,
131
+ type: 'boolean',
132
+ },
133
+ });
134
+ yargs.example([
135
+ [
136
+ '$0 project release create',
137
+ commands.project.release.create.examples.default,
138
+ ],
139
+ [
140
+ '$0 project release create --build=5',
141
+ commands.project.release.create.examples.withBuild,
142
+ ],
143
+ ]);
144
+ return yargs;
145
+ }
146
+ const builder = makeYargsBuilder(projectReleaseCreateBuilder, command, verboseDescribe, {
147
+ useGlobalOptions: true,
148
+ useConfigOptions: true,
149
+ useAccountOptions: true,
150
+ useEnvironmentOptions: true,
151
+ useJSONOutputOptions: true,
152
+ });
153
+ const projectReleaseCreateCommand = {
154
+ command,
155
+ describe,
156
+ builder,
157
+ handler: makeWrappedYargsHandler('project-release-create', handler),
158
+ };
159
+ export default projectReleaseCreateCommand;
@@ -0,0 +1,6 @@
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../../types/Yargs.js';
2
+ export type ProjectReleaseInfoArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs & {
3
+ tag?: string;
4
+ };
5
+ declare const projectReleaseInfoCommand: YargsCommandModule<unknown, ProjectReleaseInfoArgs>;
6
+ export default projectReleaseInfoCommand;
@@ -0,0 +1,147 @@
1
+ import { getReleaseInfo, listReleases } from '../../../api/releases.js';
2
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
3
+ import { logError, ApiErrorContext } from '../../../lib/errorHandlers/index.js';
4
+ import { getProjectConfig, validateProjectConfig, } from '../../../lib/projects/config.js';
5
+ import { listPrompt } from '../../../lib/prompts/promptUtils.js';
6
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
7
+ import { uiLogger } from '../../../lib/ui/logger.js';
8
+ import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
9
+ import { commands } from '../../../lang/en.js';
10
+ import { renderTable } from '../../../ui/render.js';
11
+ import { makeWrappedYargsHandler } from '../../../lib/yargs/makeWrappedYargsHandler.js';
12
+ import { isPromptExitError } from '../../../lib/errors/PromptExitError.js';
13
+ import { mapToUserFacingType } from '@hubspot/project-parsing-lib/transform';
14
+ import { AUTO_GENERATED_COMPONENT_TYPES } from '@hubspot/project-parsing-lib/constants';
15
+ const command = 'info';
16
+ // const describe = commands.project.release.info.describe;
17
+ const describe = undefined;
18
+ // const verboseDescribe = commands.project.release.info.verboseDescribe;
19
+ const verboseDescribe = undefined;
20
+ function normalizeTag(rawTag) {
21
+ return rawTag.startsWith('v') ? rawTag : `v${rawTag}`;
22
+ }
23
+ async function selectReleasePrompt(accountId, projectName) {
24
+ const { data: { results, paging }, } = await listReleases(accountId, projectName);
25
+ if (results.length === 0) {
26
+ return null;
27
+ }
28
+ if (paging?.next?.after) {
29
+ uiLogger.info(commands.project.release.info.moreReleasesHint);
30
+ }
31
+ return listPrompt(commands.project.release.info.selectRelease(projectName), {
32
+ choices: results.map(release => ({
33
+ name: `${release.releaseTag} (build #${release.buildId}, ${new Date(release.createdAt).toLocaleString()})`,
34
+ value: release.releaseTag,
35
+ })),
36
+ });
37
+ }
38
+ async function handler(args) {
39
+ const { derivedAccountId, tag: rawTag, json: formatOutputAsJson, exit, } = args;
40
+ const { projectConfig, projectDir } = await getProjectConfig();
41
+ try {
42
+ validateProjectConfig(projectConfig, projectDir);
43
+ }
44
+ catch (error) {
45
+ logError(error);
46
+ return exit(EXIT_CODES.ERROR);
47
+ }
48
+ const projectName = projectConfig.name;
49
+ let tag;
50
+ if (rawTag) {
51
+ tag = normalizeTag(rawTag);
52
+ }
53
+ else {
54
+ try {
55
+ const selected = await selectReleasePrompt(derivedAccountId, projectName);
56
+ if (!selected) {
57
+ uiLogger.error(commands.project.release.info.errors.noReleases);
58
+ return exit(EXIT_CODES.ERROR);
59
+ }
60
+ tag = selected;
61
+ }
62
+ catch (e) {
63
+ if (isPromptExitError(e)) {
64
+ throw e;
65
+ }
66
+ logError(e);
67
+ return exit(EXIT_CODES.ERROR);
68
+ }
69
+ }
70
+ try {
71
+ const { data: release } = await getReleaseInfo(derivedAccountId, projectName, tag);
72
+ if (formatOutputAsJson) {
73
+ uiLogger.json(release);
74
+ }
75
+ else {
76
+ uiLogger.log(commands.project.release.info.releaseDetails(release.releaseTag, projectName));
77
+ renderTable(['Release', 'Build', 'Created'], [
78
+ [
79
+ release.releaseTag,
80
+ `#${release.buildId}`,
81
+ new Date(release.createdAt).toLocaleString(),
82
+ ],
83
+ ]);
84
+ const visibleComponents = (release.components ?? []).filter(component => !AUTO_GENERATED_COMPONENT_TYPES.includes(component.buildType));
85
+ if (visibleComponents.length > 0) {
86
+ uiLogger.log('');
87
+ uiLogger.log(commands.project.release.info.components);
88
+ const componentRows = visibleComponents.map(component => [
89
+ mapToUserFacingType(component.buildType),
90
+ component.buildName ?? '',
91
+ component.rootPath ?? '',
92
+ ]);
93
+ renderTable(['Type', 'Name', 'Path'], componentRows);
94
+ }
95
+ else {
96
+ uiLogger.log('');
97
+ uiLogger.log(commands.project.release.info.noComponents);
98
+ }
99
+ }
100
+ }
101
+ catch (e) {
102
+ if (isHubSpotHttpError(e) && e.status === 404) {
103
+ uiLogger.error(commands.project.release.info.errors.releaseNotFound(tag, projectName));
104
+ }
105
+ else {
106
+ logError(e, new ApiErrorContext({
107
+ accountId: derivedAccountId,
108
+ request: 'project release info',
109
+ }));
110
+ }
111
+ return exit(EXIT_CODES.ERROR);
112
+ }
113
+ return exit(EXIT_CODES.SUCCESS);
114
+ }
115
+ function projectReleaseInfoBuilder(yargs) {
116
+ yargs.options({
117
+ tag: {
118
+ describe: commands.project.release.info.options.tag,
119
+ type: 'string',
120
+ },
121
+ });
122
+ yargs.example([
123
+ [
124
+ '$0 project release info --tag=v1.0.0',
125
+ commands.project.release.info.examples.default,
126
+ ],
127
+ [
128
+ '$0 project release info --tag=v1.0.0 --json',
129
+ commands.project.release.info.examples.json,
130
+ ],
131
+ ]);
132
+ return yargs;
133
+ }
134
+ const builder = makeYargsBuilder(projectReleaseInfoBuilder, command, verboseDescribe, {
135
+ useGlobalOptions: true,
136
+ useConfigOptions: true,
137
+ useAccountOptions: true,
138
+ useEnvironmentOptions: true,
139
+ useJSONOutputOptions: true,
140
+ });
141
+ const projectReleaseInfoCommand = {
142
+ command,
143
+ describe,
144
+ builder,
145
+ handler: makeWrappedYargsHandler('project-release-info', handler),
146
+ };
147
+ export default projectReleaseInfoCommand;
@@ -0,0 +1,6 @@
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../../types/Yargs.js';
2
+ export type ProjectReleaseListArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs & {
3
+ limit?: number;
4
+ };
5
+ declare const projectReleaseListCommand: YargsCommandModule<unknown, ProjectReleaseListArgs>;
6
+ export default projectReleaseListCommand;
@@ -0,0 +1,111 @@
1
+ import { listReleases } from '../../../api/releases.js';
2
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
3
+ import { isPromptExitError } from '../../../lib/errors/PromptExitError.js';
4
+ import { logError, ApiErrorContext } from '../../../lib/errorHandlers/index.js';
5
+ import { getProjectConfig, validateProjectConfig, } from '../../../lib/projects/config.js';
6
+ import { promptUser } from '../../../lib/prompts/promptUtils.js';
7
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
8
+ import { uiLogger } from '../../../lib/ui/logger.js';
9
+ import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
10
+ import { commands } from '../../../lang/en.js';
11
+ import { renderTable } from '../../../ui/render.js';
12
+ import { makeWrappedYargsHandler } from '../../../lib/yargs/makeWrappedYargsHandler.js';
13
+ const command = 'list';
14
+ // const describe = commands.project.release.list.describe;
15
+ const describe = undefined;
16
+ // const verboseDescribe = commands.project.release.list.verboseDescribe;
17
+ const verboseDescribe = undefined;
18
+ async function fetchAndDisplayReleases(accountId, projectName, options) {
19
+ const { data: { results, paging }, } = await listReleases(accountId, projectName, options);
20
+ uiLogger.log(commands.project.release.list.showingReleases(results.length, projectName));
21
+ if (results.length === 0) {
22
+ uiLogger.log(commands.project.release.list.noReleases);
23
+ }
24
+ else {
25
+ const rows = results.map(release => [
26
+ release.releaseTag,
27
+ `#${release.buildId}`,
28
+ new Date(release.createdAt).toLocaleString(),
29
+ ]);
30
+ renderTable(['Release', 'Build', 'Created'], rows);
31
+ }
32
+ if (paging?.next?.after) {
33
+ await promptUser({
34
+ name: 'more',
35
+ message: commands.project.release.list.continueOrExitPrompt,
36
+ });
37
+ await fetchAndDisplayReleases(accountId, projectName, {
38
+ limit: options.limit,
39
+ after: paging.next.after,
40
+ });
41
+ }
42
+ }
43
+ async function handler(args) {
44
+ const { derivedAccountId, limit, json: formatOutputAsJson } = args;
45
+ const { projectConfig, projectDir } = await getProjectConfig();
46
+ try {
47
+ validateProjectConfig(projectConfig, projectDir);
48
+ }
49
+ catch (error) {
50
+ logError(error);
51
+ process.exit(EXIT_CODES.ERROR);
52
+ }
53
+ const projectName = projectConfig.name;
54
+ try {
55
+ if (formatOutputAsJson) {
56
+ const { data } = await listReleases(derivedAccountId, projectName, {
57
+ limit,
58
+ });
59
+ uiLogger.json(data);
60
+ }
61
+ else {
62
+ await fetchAndDisplayReleases(derivedAccountId, projectName, { limit });
63
+ }
64
+ }
65
+ catch (e) {
66
+ if (isPromptExitError(e)) {
67
+ throw e;
68
+ }
69
+ if (isHubSpotHttpError(e) && e.status === 404) {
70
+ uiLogger.error(commands.project.release.list.errors.projectNotFound(derivedAccountId, projectName));
71
+ }
72
+ else {
73
+ logError(e, new ApiErrorContext({
74
+ accountId: derivedAccountId,
75
+ request: 'project release list',
76
+ }));
77
+ }
78
+ process.exit(EXIT_CODES.ERROR);
79
+ }
80
+ process.exit(EXIT_CODES.SUCCESS);
81
+ }
82
+ function projectReleaseListBuilder(yargs) {
83
+ yargs.options({
84
+ limit: {
85
+ describe: commands.project.release.list.options.limit,
86
+ type: 'number',
87
+ },
88
+ });
89
+ yargs.example([
90
+ ['$0 project release list', commands.project.release.list.examples.default],
91
+ [
92
+ '$0 project release list --limit=5',
93
+ commands.project.release.list.examples.withLimit,
94
+ ],
95
+ ]);
96
+ return yargs;
97
+ }
98
+ const builder = makeYargsBuilder(projectReleaseListBuilder, command, verboseDescribe, {
99
+ useGlobalOptions: true,
100
+ useConfigOptions: true,
101
+ useAccountOptions: true,
102
+ useEnvironmentOptions: true,
103
+ useJSONOutputOptions: true,
104
+ });
105
+ const projectReleaseListCommand = {
106
+ command,
107
+ describe,
108
+ builder,
109
+ handler: makeWrappedYargsHandler('project-release-list', handler),
110
+ };
111
+ export default projectReleaseListCommand;
@@ -0,0 +1,3 @@
1
+ import { YargsCommandModuleBucket } from '../../types/Yargs.js';
2
+ declare const projectReleaseCommand: YargsCommandModuleBucket;
3
+ export default projectReleaseCommand;
@@ -0,0 +1,20 @@
1
+ import create from './release/create.js';
2
+ import info from './release/info.js';
3
+ import list from './release/list.js';
4
+ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
5
+ // import { commands } from '../../lang/en.js';
6
+ const command = 'release';
7
+ // const describe = commands.project.release.describe;
8
+ const describe = undefined;
9
+ function projectReleaseBuilder(yargs) {
10
+ yargs.command(create).command(info).command(list).demandCommand(1, '');
11
+ return yargs;
12
+ }
13
+ const builder = makeYargsBuilder(projectReleaseBuilder, command, describe);
14
+ const projectReleaseCommand = {
15
+ command,
16
+ describe,
17
+ builder,
18
+ handler: () => { },
19
+ };
20
+ export default projectReleaseCommand;
@@ -124,6 +124,15 @@ async function handler(args) {
124
124
  jsonOutput.deployId = result.deployResult.deployId;
125
125
  }
126
126
  }
127
+ if (result && !result.succeeded) {
128
+ if (formatOutputAsJson) {
129
+ uiLogger.json(jsonOutput);
130
+ }
131
+ return exit(EXIT_CODES.ERROR);
132
+ }
133
+ if (!result && !uploadError) {
134
+ return exit(EXIT_CODES.ERROR);
135
+ }
127
136
  }
128
137
  catch (e) {
129
138
  logError(e, new ApiErrorContext({
@@ -15,6 +15,7 @@ import installDeps from './project/installDeps.js';
15
15
  import lint from './project/lint.js';
16
16
  import updateDeps from './project/updateDeps.js';
17
17
  import profile from './project/profile.js';
18
+ import release from './project/release.js';
18
19
  import projectValidate from './project/validate.js';
19
20
  import list from './project/list.js';
20
21
  import info from './project/info.js';
@@ -66,6 +67,7 @@ function projectBuilder(yargs) {
66
67
  .command(lint)
67
68
  .command(updateDeps)
68
69
  .command(profile)
70
+ .command(release)
69
71
  .command(projectValidate)
70
72
  .command(appInstallStatus)
71
73
  .demandCommand(1, '');
package/lang/en.d.ts CHANGED
@@ -1729,6 +1729,67 @@ export declare const commands: {
1729
1729
  deployLatestBuild: string;
1730
1730
  };
1731
1731
  };
1732
+ release: {
1733
+ describe: string;
1734
+ create: {
1735
+ describe: string;
1736
+ verboseDescribe: string;
1737
+ confirmPrompt: (projectName: string, buildId: number) => string;
1738
+ success: (releaseTag: string, buildId: number) => string;
1739
+ cancelled: string;
1740
+ errors: {
1741
+ projectNotFound: (accountId: number, projectName: string) => string;
1742
+ noDeployedBuild: string;
1743
+ buildNotFound: (buildId: number, projectName: string) => string;
1744
+ buildNotDeployed: (buildId: number) => string;
1745
+ };
1746
+ options: {
1747
+ build: string;
1748
+ force: string;
1749
+ };
1750
+ examples: {
1751
+ default: string;
1752
+ withBuild: string;
1753
+ };
1754
+ };
1755
+ list: {
1756
+ describe: string;
1757
+ verboseDescribe: string;
1758
+ noReleases: string;
1759
+ showingReleases: (count: number, projectName: string) => string;
1760
+ continueOrExitPrompt: string;
1761
+ errors: {
1762
+ projectNotFound: (accountId: number, projectName: string) => string;
1763
+ };
1764
+ options: {
1765
+ limit: string;
1766
+ };
1767
+ examples: {
1768
+ default: string;
1769
+ withLimit: string;
1770
+ };
1771
+ };
1772
+ info: {
1773
+ describe: string;
1774
+ verboseDescribe: string;
1775
+ releaseDetails: (releaseTag: string, projectName: string) => string;
1776
+ components: string;
1777
+ noComponents: string;
1778
+ moreReleasesHint: string;
1779
+ selectRelease: (projectName: string) => string;
1780
+ errors: {
1781
+ releaseNotFound: (releaseTag: string, projectName: string) => string;
1782
+ noReleases: string;
1783
+ };
1784
+ options: {
1785
+ tag: string;
1786
+ };
1787
+ examples: {
1788
+ default: string;
1789
+ json: string;
1790
+ };
1791
+ };
1792
+ };
1732
1793
  listBuilds: {
1733
1794
  describe: string;
1734
1795
  continueOrExitPrompt: string;
@@ -3406,6 +3467,9 @@ export declare const lib: {
3406
3467
  updatingPackageJsonWorkspaces: (packageJsonPath: string) => string;
3407
3468
  updatedWorkspaces: (workspaces: string) => string;
3408
3469
  updatedFileDependency: (packageName: string, relativePath: string) => string;
3470
+ lintPackagesNotConfigured: (packageRoot: string) => string;
3471
+ lintConfigNotFound: (packageRoot: string) => string;
3472
+ lintHubSpotRulesNotActive: (packageRoot: string) => string;
3409
3473
  npmAuditClean: (packageRoot: string) => string;
3410
3474
  npmAuditIssues: (packageRoot: string, details: string) => string;
3411
3475
  npmAuditNpmUnavailable: (packageRoot: string) => string;
package/lang/en.js CHANGED
@@ -1746,6 +1746,67 @@ export const commands = {
1746
1746
  deployLatestBuild: 'Deploy the latest build of the current project',
1747
1747
  },
1748
1748
  },
1749
+ release: {
1750
+ describe: 'Manage project releases.',
1751
+ create: {
1752
+ describe: 'Create a release for a project build.',
1753
+ verboseDescribe: `Create a release for a project build\n\nReleases mark a deployed build with an auto-generated semantic version tag (e.g. v1.0.0). The build must have been successfully deployed before it can be released.\n\nBy default, the latest deployed build is used. Use ${uiCommandReference('--build')} to specify a different build ID.`,
1754
+ confirmPrompt: (projectName, buildId) => `Create a release for project ${chalk.bold(projectName)} using build ${chalk.bold(String(buildId))}?`,
1755
+ success: (releaseTag, buildId) => `Release ${chalk.bold(releaseTag)} created for build ${chalk.bold(String(buildId))}.`,
1756
+ cancelled: 'Release creation cancelled.',
1757
+ errors: {
1758
+ projectNotFound: (accountId, projectName) => `The project ${chalk.bold(projectName)} does not exist in account ${uiAccountDescription(accountId)}. Run ${uiCommandReference('hs project upload')} to upload your project files to HubSpot.`,
1759
+ noDeployedBuild: `No deployed build found for this project. Run ${uiCommandReference('hs project deploy')} first.`,
1760
+ buildNotFound: (buildId, projectName) => `Build ${chalk.bold(String(buildId))} was not found for project ${chalk.bold(projectName)}. Run ${uiCommandReference('hs project list-builds')} to view existing builds or ${uiCommandReference('hs project deploy')} to deploy a build first.`,
1761
+ buildNotDeployed: (buildId) => `Build ${chalk.bold(String(buildId))} has not been deployed. Run ${uiCommandReference('hs project deploy')} first.`,
1762
+ },
1763
+ options: {
1764
+ build: 'Build ID to release. Defaults to the latest deployed build.',
1765
+ force: 'Skip confirmation prompt.',
1766
+ },
1767
+ examples: {
1768
+ default: 'Create a release for the latest deployed build',
1769
+ withBuild: 'Create a release for a specific build',
1770
+ },
1771
+ },
1772
+ list: {
1773
+ describe: 'List releases for the current project.',
1774
+ verboseDescribe: `List releases for the current project\n\nDisplays all releases in sorted order, newest first. Includes the version tag, build ID, and creation date. Use ${uiCommandReference('--limit')} to control how many results are shown.`,
1775
+ noReleases: 'No releases found for this project.',
1776
+ showingReleases: (count, projectName) => `Showing ${count} release${count === 1 ? '' : 's'} for ${chalk.bold(projectName)}:`,
1777
+ continueOrExitPrompt: 'Press <enter> to load more, or ctrl+c to exit',
1778
+ errors: {
1779
+ projectNotFound: (accountId, projectName) => `The project ${chalk.bold(projectName)} does not exist in account ${uiAccountDescription(accountId)}. Run ${uiCommandReference('hs project upload')} to upload your project files to HubSpot.`,
1780
+ },
1781
+ options: {
1782
+ limit: 'Number of releases to show',
1783
+ },
1784
+ examples: {
1785
+ default: 'List releases for the current project',
1786
+ withLimit: 'Show only the 5 most recent releases',
1787
+ },
1788
+ },
1789
+ info: {
1790
+ describe: 'Show details about a specific release.',
1791
+ verboseDescribe: `Show details about a specific release\n\nDisplays the release tag, build ID, creation date, and list of components included in the release. Use ${uiCommandReference('--json')} for machine-readable output.`,
1792
+ releaseDetails: (releaseTag, projectName) => `Release ${chalk.bold(releaseTag)} for ${chalk.bold(projectName)}:`,
1793
+ components: 'Components:',
1794
+ noComponents: 'No components found for this release.',
1795
+ moreReleasesHint: `Not all releases are shown. Use ${uiCommandReference('--tag')} to look up an older release directly.`,
1796
+ selectRelease: (projectName) => `Select a release for ${chalk.bold(projectName)}`,
1797
+ errors: {
1798
+ releaseNotFound: (releaseTag, projectName) => `Release ${chalk.bold(releaseTag)} was not found for project ${chalk.bold(projectName)}. Verify the project has been uploaded and the release tag exists. Run ${uiCommandReference('hs project release list')} to view existing releases.`,
1799
+ noReleases: 'No releases found for this project.',
1800
+ },
1801
+ options: {
1802
+ tag: 'Release tag to look up (e.g. v1.0.0)',
1803
+ },
1804
+ examples: {
1805
+ default: 'Show details about a specific release',
1806
+ json: 'Output release details as JSON',
1807
+ },
1808
+ },
1809
+ },
1749
1810
  listBuilds: {
1750
1811
  describe: "List the project's builds.",
1751
1812
  continueOrExitPrompt: 'Press <enter> to load more, or ctrl+c to exit',
@@ -3432,6 +3493,9 @@ export const lib = {
3432
3493
  updatingPackageJsonWorkspaces: (packageJsonPath) => `Updating package.json workspaces in archive: ${packageJsonPath}`,
3433
3494
  updatedWorkspaces: (workspaces) => ` Updated workspaces: ${workspaces}`,
3434
3495
  updatedFileDependency: (packageName, relativePath) => ` Updated dependencies.${packageName}: file:${relativePath}`,
3496
+ lintPackagesNotConfigured: (packageRoot) => `Project lint: lint packages not installed for ${chalk.bold(packageRoot)}. Run ${uiCommandReference('hs project lint')} to install them.`,
3497
+ lintConfigNotFound: (packageRoot) => `Project lint: ESLint configuration not found for ${chalk.bold(packageRoot)}. Run ${uiCommandReference('hs project lint')} to create an ESLint config.`,
3498
+ lintHubSpotRulesNotActive: (packageRoot) => `Project lint: HubSpot ESLint rules not active for ${chalk.bold(packageRoot)}. Configure ${chalk.bold('@hubspot/eslint-config-ui-extensions')} — see ${uiLink('setup instructions', 'https://www.npmjs.com/package/@hubspot/eslint-config-ui-extensions')}.`,
3435
3499
  npmAuditClean: (packageRoot) => `npm audit: No npm dependency issues found for ${chalk.bold(packageRoot)}`,
3436
3500
  npmAuditIssues: (packageRoot, details) => `npm audit: security issues found for ${chalk.bold(packageRoot)}: ${details}`,
3437
3501
  npmAuditNpmUnavailable: (packageRoot) => `npm audit: skipped for ${chalk.bold(packageRoot)} (npm not available in PATH)`,
package/lib/commonOpts.js CHANGED
@@ -76,7 +76,6 @@ export function addJSONOutputOptions(yargs) {
76
76
  alias: 'format-output-as-json',
77
77
  describe: lib.commonOpts.options.jsonOutput,
78
78
  type: 'boolean',
79
- hidden: true,
80
79
  });
81
80
  }
82
81
  // Remove this once we've upgraded to yargs 18.0.0
@@ -1,3 +1,4 @@
1
+ import { LoadedProjectConfig } from './config.js';
1
2
  export declare const REQUIRED_PACKAGES_AND_MIN_VERSIONS: {
2
3
  readonly eslint: "9.0.0";
3
4
  readonly '@eslint/js': "9.0.0";
@@ -23,6 +24,9 @@ export declare function hasEslintConfig(directory: string): boolean;
23
24
  export declare function hasDeprecatedEslintConfig(directory: string): boolean;
24
25
  export declare function getDeprecatedEslintConfigFiles(directory: string): string[];
25
26
  export declare function createEslintConfig(directory: string, platformVersion?: string | null): Promise<string>;
27
+ export declare function getUieLintablePackageJsonLocations(projectConfig: LoadedProjectConfig): Promise<string[]>;
28
+ export declare const HUBSPOT_UI_EXTENSIONS_RULE_PREFIX = "@hubspot/ui-extensions/";
29
+ export declare function isHubSpotEslintConfigActive(directory: string): Promise<boolean>;
26
30
  export declare function lintPackagesInDirectory(directory: string, projectDir?: string): Promise<{
27
31
  success: boolean;
28
32
  output: string;
@@ -10,7 +10,8 @@ import { uiLogger } from '../ui/logger.js';
10
10
  import { clearPackageJsonCache, safeGetPackageJsonCached, } from '../npm/packageJson.js';
11
11
  import { debugError } from '../errorHandlers/index.js';
12
12
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
- import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../constants.js';
13
+ 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';
14
15
  export const REQUIRED_PACKAGES_AND_MIN_VERSIONS = {
15
16
  eslint: '9.0.0',
16
17
  '@eslint/js': '9.0.0',
@@ -40,6 +41,11 @@ const DEPRECATED_ESLINT_CONFIG_FILES = [
40
41
  '.eslintrc.json',
41
42
  '.eslintrc',
42
43
  ];
44
+ const UIE_COMPONENTS = [
45
+ Components[CARDS_KEY],
46
+ Components[SETTINGS_KEY],
47
+ Components[PAGES_KEY],
48
+ ];
43
49
  export const LINT_SCRIPTS = {
44
50
  lint: 'eslint .',
45
51
  'lint:fix': 'eslint . --fix',
@@ -163,6 +169,38 @@ export async function createEslintConfig(directory, platformVersion) {
163
169
  throw error;
164
170
  }
165
171
  }
172
+ export async function getUieLintablePackageJsonLocations(projectConfig) {
173
+ if (!projectConfig.projectDir || !projectConfig.projectConfig?.srcDir) {
174
+ return [];
175
+ }
176
+ const srcDirAbsolute = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
177
+ const uiePackageDirPrefixes = UIE_COMPONENTS.map(component => path.join(srcDirAbsolute, component.parentComponent
178
+ ? Components[component.parentComponent].dir
179
+ : '', component.dir));
180
+ const allLocations = await getProjectPackageJsonLocations(projectConfig.projectDir);
181
+ return allLocations.filter(location => {
182
+ const resolvedLocation = path.resolve(location);
183
+ return uiePackageDirPrefixes.some(prefix => resolvedLocation.startsWith(prefix));
184
+ });
185
+ }
186
+ export const HUBSPOT_UI_EXTENSIONS_RULE_PREFIX = '@hubspot/ui-extensions/';
187
+ function getEnvironmentWithoutNpmConfig() {
188
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toLowerCase().startsWith('npm_config_')));
189
+ }
190
+ export async function isHubSpotEslintConfigActive(directory) {
191
+ const exec = util.promisify(execAsync);
192
+ try {
193
+ const { stdout } = await exec('npx eslint --print-config ./Component.tsx', {
194
+ cwd: directory,
195
+ });
196
+ const config = JSON.parse(stdout);
197
+ const rules = config.rules ?? {};
198
+ return Object.keys(rules).some(rule => rule.startsWith(HUBSPOT_UI_EXTENSIONS_RULE_PREFIX));
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
166
204
  export async function lintPackagesInDirectory(directory, projectDir) {
167
205
  const displayPath = projectDir
168
206
  ? path.relative(projectDir, directory)
@@ -173,6 +211,7 @@ export async function lintPackagesInDirectory(directory, projectDir) {
173
211
  const { stdout, stderr } = await exec(lintCommand, {
174
212
  cwd: directory,
175
213
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
214
+ env: getEnvironmentWithoutNpmConfig(),
176
215
  });
177
216
  let output = `\n${displayPath}:\n`;
178
217
  if (stdout && stdout.trim()) {
@@ -18,6 +18,7 @@ import { walk } from '@hubspot/local-dev-lib/fs';
18
18
  import { LEGACY_CONFIG_FILES } from '../constants.js';
19
19
  import { archiveWorkspacesAndDependencies, getPackageJsonPathsToUpdate, getLockfilePathsToUpdate, } from './workspaces.js';
20
20
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
21
+ import { validateLintConfigOnUpload } from './validateLintConfigOnUpload.js';
21
22
  import { runNpmAuditsBeforeProjectUpload } from './npmAuditOnUpload.js';
22
23
  async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
23
24
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
@@ -61,6 +62,14 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
61
62
  workspaceMappings = await collectWorkspaceDirectories(parsedPackageJsons);
62
63
  fileDependencyMappings = await collectFileDependencies(parsedPackageJsons);
63
64
  }
65
+ if (isUploadCommand && !skipValidation) {
66
+ await validateLintConfigOnUpload({
67
+ srcDir,
68
+ projectDir,
69
+ parsedPackageJsons,
70
+ isLegacyPlatform: isLegacyProject(projectConfig.platformVersion),
71
+ });
72
+ }
64
73
  if (isUploadCommand && !skipNpmAudit) {
65
74
  await runNpmAuditsBeforeProjectUpload({
66
75
  srcDir,
@@ -0,0 +1,9 @@
1
+ import type { ParsedPackageJson } from '@hubspot/project-parsing-lib/workspaces';
2
+ type ValidateLintConfigOnUploadArgs = {
3
+ srcDir: string;
4
+ projectDir: string;
5
+ parsedPackageJsons: ParsedPackageJson[];
6
+ isLegacyPlatform: boolean;
7
+ };
8
+ export declare function validateLintConfigOnUpload({ srcDir, projectDir, parsedPackageJsons, isLegacyPlatform, }: ValidateLintConfigOnUploadArgs): Promise<void>;
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import path from 'path';
2
+ import { lib } from '../../lang/en.js';
3
+ import { uiLogger } from '../ui/logger.js';
4
+ import { areAllLintPackagesInstalled, hasEslintConfig, isHubSpotEslintConfigActive, } from './uieLinting.js';
5
+ export async function validateLintConfigOnUpload({ srcDir, projectDir, parsedPackageJsons, isLegacyPlatform, }) {
6
+ const lintRoots = new Set();
7
+ if (isLegacyPlatform) {
8
+ lintRoots.add(srcDir);
9
+ }
10
+ else {
11
+ for (const { dir } of parsedPackageJsons) {
12
+ lintRoots.add(dir);
13
+ }
14
+ if (lintRoots.size === 0) {
15
+ lintRoots.add(srcDir);
16
+ }
17
+ }
18
+ let hasAnyOutput = false;
19
+ for (const lintRoot of lintRoots) {
20
+ const relativeRoot = path.relative(projectDir, lintRoot) || '.';
21
+ let warnMessage;
22
+ if (!areAllLintPackagesInstalled(lintRoot)) {
23
+ warnMessage =
24
+ lib.projectUpload.handleProjectUpload.lintPackagesNotConfigured(relativeRoot);
25
+ }
26
+ else if (!hasEslintConfig(lintRoot)) {
27
+ warnMessage =
28
+ lib.projectUpload.handleProjectUpload.lintConfigNotFound(relativeRoot);
29
+ }
30
+ else if (!(await isHubSpotEslintConfigActive(lintRoot))) {
31
+ warnMessage =
32
+ lib.projectUpload.handleProjectUpload.lintHubSpotRulesNotActive(relativeRoot);
33
+ }
34
+ if (warnMessage) {
35
+ if (!hasAnyOutput) {
36
+ uiLogger.log('');
37
+ hasAnyOutput = true;
38
+ }
39
+ uiLogger.warn(warnMessage);
40
+ }
41
+ }
42
+ if (hasAnyOutput) {
43
+ uiLogger.log('');
44
+ }
45
+ }
@@ -1,11 +1,15 @@
1
1
  import archiver from 'archiver';
2
- import { WorkspaceMapping, FileDependencyMapping } from '@hubspot/project-parsing-lib/workspaces';
2
+ import { WorkspaceMapping, FileDependencyMapping, FileDependencyKind, LocalDependencyProtocol } from '@hubspot/project-parsing-lib/workspaces';
3
+ export type FileDepArchiveEntry = {
4
+ archivePath: string;
5
+ protocol: LocalDependencyProtocol;
6
+ };
3
7
  /**
4
8
  * Result of archiving workspaces and file dependencies
5
9
  */
6
10
  export type WorkspaceArchiveResult = {
7
11
  packageWorkspaces: Map<string, string[]>;
8
- packageFileDeps: Map<string, Map<string, string>>;
12
+ packageFileDeps: Map<string, Map<string, FileDepArchiveEntry>>;
9
13
  };
10
14
  /**
11
15
  * Generates a short hash of the input string for use in workspace paths.
@@ -24,10 +28,14 @@ export declare function shortHash(input: string): string;
24
28
  export declare function toPosixPath(p: string): string;
25
29
  /**
26
30
  * Determines the archive path for an external workspace or file: dependency.
27
- * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
28
- * The hash prevents collisions between different directories with the same basename.
31
+ *
32
+ * For directories, produces `_workspaces/<basename>-<hash>`.
33
+ * For tarballs, produces `_workspaces/<basename-no-ext>-<hash>/<original-basename>`,
34
+ * so the rewritten package.json reference still ends in the original filename.
35
+ *
36
+ * The hash prevents collisions between different paths with the same basename.
29
37
  */
30
- export declare function computeExternalArchivePath(absolutePath: string): string;
38
+ export declare function computeExternalArchivePath(absolutePath: string, kind?: FileDependencyKind): string;
31
39
  /**
32
40
  * Updates package.json files in the archive to reflect new workspace and file: dependency paths.
33
41
  *
@@ -38,7 +46,7 @@ export declare function computeExternalArchivePath(absolutePath: string): string
38
46
  * Only external file: dependencies appear in packageFileDeps; internal ones
39
47
  * keep their original file: references and are left untouched.
40
48
  */
41
- export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string, string>>): Promise<void>;
49
+ export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string, FileDepArchiveEntry>>): Promise<void>;
42
50
  export declare function rewriteLockfileForExternalDeps(lockfileContent: Record<string, unknown>, pathMappings: Array<{
43
51
  oldPath: string;
44
52
  newPath: string;
@@ -27,14 +27,35 @@ export function toPosixPath(p) {
27
27
  }
28
28
  return p.replaceAll(path.sep, path.posix.sep);
29
29
  }
30
+ /**
31
+ * Strips the longest matching tarball extension (.tar.gz, .tgz, .tar)
32
+ * from a file basename. Returns the input unchanged if no extension matches.
33
+ */
34
+ function stripTarballExtension(basename) {
35
+ const lower = basename.toLowerCase();
36
+ for (const ext of ['.tar.gz', '.tgz', '.tar']) {
37
+ if (lower.endsWith(ext)) {
38
+ return basename.slice(0, basename.length - ext.length);
39
+ }
40
+ }
41
+ return basename;
42
+ }
30
43
  /**
31
44
  * Determines the archive path for an external workspace or file: dependency.
32
- * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
33
- * The hash prevents collisions between different directories with the same basename.
45
+ *
46
+ * For directories, produces `_workspaces/<basename>-<hash>`.
47
+ * For tarballs, produces `_workspaces/<basename-no-ext>-<hash>/<original-basename>`,
48
+ * so the rewritten package.json reference still ends in the original filename.
49
+ *
50
+ * The hash prevents collisions between different paths with the same basename.
34
51
  */
35
- export function computeExternalArchivePath(absolutePath) {
52
+ export function computeExternalArchivePath(absolutePath, kind = 'directory') {
36
53
  const resolved = path.resolve(absolutePath);
37
54
  const name = path.basename(resolved);
55
+ if (kind === 'tarball') {
56
+ const nameNoExt = stripTarballExtension(name);
57
+ return path.posix.join('_workspaces', `${nameNoExt}-${shortHash(resolved)}`, name);
58
+ }
38
59
  return path.posix.join('_workspaces', `${name}-${shortHash(resolved)}`);
39
60
  }
40
61
  /**
@@ -121,25 +142,25 @@ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
121
142
  return { externalArchivePaths, packageWorkspaceEntries };
122
143
  }
123
144
  /**
124
- * Archives file: dependencies and returns mapping information.
145
+ * Archives file: and link: dependencies and returns mapping information.
125
146
  *
126
- * Internal file: dependencies (inside srcDir) are skipped — their original
127
- * `file:` references in package.json remain valid after upload.
147
+ * Internal dependencies (inside srcDir) are skipped — their original
148
+ * references in package.json remain valid after upload.
128
149
  *
129
- * External file: dependencies are archived to `_workspaces/<name>-<hash>`
130
- * and tracked in the returned map so package.json can be rewritten.
150
+ * External directory dependencies are archived to `_workspaces/<name>-<hash>`.
151
+ * External tarball dependencies are archived to
152
+ * `_workspaces/<name-no-ext>-<hash>/<original-basename>` so the rewritten
153
+ * reference still ends in the original filename.
131
154
  */
132
155
  async function archiveFileDependencies(archive, srcDir, fileDependencyMappings, externalArchivePaths) {
133
156
  const packageFileDeps = new Map();
134
157
  const toArchive = [];
135
158
  for (const mapping of fileDependencyMappings) {
136
- const { packageName, localPath, sourcePackageJsonPath } = mapping;
159
+ const { packageName, localPath, sourcePackageJsonPath, kind, protocol } = mapping;
137
160
  if (isInsideSrcDir(localPath, srcDir)) {
138
- // Internal: original file: reference stays unchanged, nothing to do
139
161
  continue;
140
162
  }
141
- // External: archive to _workspaces/<name>-<hash>
142
- const archivePath = computeExternalArchivePath(localPath);
163
+ const archivePath = computeExternalArchivePath(localPath, kind);
143
164
  const resolvedPath = path.resolve(localPath);
144
165
  if (!packageFileDeps.has(sourcePackageJsonPath)) {
145
166
  packageFileDeps.set(sourcePackageJsonPath, new Map());
@@ -148,23 +169,27 @@ async function archiveFileDependencies(archive, srcDir, fileDependencyMappings,
148
169
  const relativeArchivePath = toPosixPath(path.relative(relPkgJsonDir, archivePath));
149
170
  packageFileDeps
150
171
  .get(sourcePackageJsonPath)
151
- .set(packageName, relativeArchivePath);
152
- // Only archive each unique path once
172
+ .set(packageName, { archivePath: relativeArchivePath, protocol });
153
173
  if (!externalArchivePaths.has(resolvedPath)) {
154
174
  externalArchivePaths.set(resolvedPath, archivePath);
155
- toArchive.push({ localPath, archivePath, packageName });
175
+ toArchive.push({ localPath, archivePath, packageName, kind });
156
176
  }
157
177
  }
158
- // Fetch packable files in parallel (I/O optimization)
159
- const withPackableFiles = await Promise.all(toArchive.map(async (item) => ({
178
+ const directoryItems = toArchive.filter(item => item.kind === 'directory');
179
+ const tarballItems = toArchive.filter(item => item.kind === 'tarball');
180
+ // getPackableFiles only applies to directory deps; tarballs are a single file.
181
+ const directoriesWithPackableFiles = await Promise.all(directoryItems.map(async (item) => ({
160
182
  ...item,
161
183
  packableFiles: await getPackableFiles(item.localPath),
162
184
  })));
163
- // Archive directories sequentially (archiver requires sequential operations)
164
- for (const { localPath, archivePath, packageName, packableFiles, } of withPackableFiles) {
185
+ for (const { localPath, archivePath, packageName, packableFiles, } of directoriesWithPackableFiles) {
165
186
  uiLogger.log(lib.projectUpload.handleProjectUpload.fileDependencyIncluded(packageName, localPath, archivePath));
166
187
  archive.directory(localPath, archivePath, createWorkspaceFileFilter(packableFiles));
167
188
  }
189
+ for (const { localPath, archivePath, packageName } of tarballItems) {
190
+ uiLogger.log(lib.projectUpload.handleProjectUpload.fileDependencyIncluded(packageName, localPath, archivePath));
191
+ archive.file(localPath, { name: archivePath });
192
+ }
168
193
  return packageFileDeps;
169
194
  }
170
195
  /**
@@ -213,14 +238,18 @@ export async function updatePackageJsonInArchive(archive, srcDir, packageWorkspa
213
238
  uiLogger.debug(lib.projectUpload.handleProjectUpload.updatingPackageJsonWorkspaces(relativePackageJsonPath));
214
239
  uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedWorkspaces(workspaceEntries.join(', ')));
215
240
  }
216
- // Update external file: dependencies; internal ones are left untouched
241
+ // Update external file: and link: dependencies; internal ones are left untouched.
242
+ // The protocol prefix (file: vs link:) is preserved from the original spec.
217
243
  const fileDeps = packageFileDeps.get(packageJsonPath);
218
244
  if (fileDeps && fileDeps.size > 0 && packageJson.dependencies) {
219
- for (const [packageName, archivePath] of fileDeps.entries()) {
220
- if (packageJson.dependencies[packageName]?.startsWith('file:')) {
221
- packageJson.dependencies[packageName] = `file:${archivePath}`;
245
+ for (const [packageName, { archivePath, protocol },] of fileDeps.entries()) {
246
+ const current = packageJson.dependencies[packageName];
247
+ if (typeof current === 'string' &&
248
+ (current.startsWith('file:') || current.startsWith('link:'))) {
249
+ const newValue = `${protocol}:${archivePath}`;
250
+ packageJson.dependencies[packageName] = newValue;
222
251
  modified = true;
223
- uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedFileDependency(packageName, archivePath));
252
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedFileDependency(packageName, newValue));
224
253
  }
225
254
  }
226
255
  }
@@ -248,11 +277,24 @@ export function rewriteLockfileForExternalDeps(lockfileContent, pathMappings) {
248
277
  typeof value === 'object' &&
249
278
  value !== null) {
250
279
  const entry = value;
251
- if (entry.link === true && typeof entry.resolved === 'string') {
252
- const mapping = pathMappings.find(m => m.oldPath === entry.resolved);
253
- if (mapping) {
254
- newPackages[key] = { ...entry, resolved: mapping.newPath };
255
- }
280
+ if (typeof entry.resolved !== 'string')
281
+ continue;
282
+ // Symlink entries (directory deps with link:true) store resolved as a
283
+ // bare relative path. Tarball entries store resolved as a "file:" URL.
284
+ const resolved = entry.resolved;
285
+ const filePrefix = 'file:';
286
+ const isFileUrl = resolved.startsWith(filePrefix);
287
+ const resolvedPath = isFileUrl
288
+ ? resolved.slice(filePrefix.length)
289
+ : resolved;
290
+ const mapping = pathMappings.find(m => m.oldPath === resolvedPath);
291
+ if (mapping) {
292
+ newPackages[key] = {
293
+ ...entry,
294
+ resolved: isFileUrl
295
+ ? `${filePrefix}${mapping.newPath}`
296
+ : mapping.newPath,
297
+ };
256
298
  }
257
299
  }
258
300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.1.2-experimental.2",
3
+ "version": "8.1.3-experimental.0",
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",
@@ -10,8 +10,8 @@
10
10
  "!**/__tests__/**"
11
11
  ],
12
12
  "dependencies": {
13
- "@hubspot/local-dev-lib": "5.7.0",
14
- "@hubspot/project-parsing-lib": "0.16.0",
13
+ "@hubspot/local-dev-lib": "5.7.1",
14
+ "@hubspot/project-parsing-lib": "0.2.2-experimental.0",
15
15
  "@hubspot/serverless-dev-runtime": "7.0.7",
16
16
  "@hubspot/ui-extensions-dev-server": "2.0.7",
17
17
  "@inquirer/prompts": "7.1.0",
@@ -37,7 +37,7 @@
37
37
  "table": "6.9.0",
38
38
  "tmp": "0.2.4",
39
39
  "update-notifier": "7.3.1",
40
- "ws": "^8.18.2",
40
+ "ws": "8.20.0",
41
41
  "yargs": "17.7.2",
42
42
  "yargs-parser": "21.1.1"
43
43
  },