@hubspot/cli 8.0.3-experimental.0 → 8.0.3-experimental.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lang/en.d.ts CHANGED
@@ -3179,6 +3179,14 @@ export declare const lib: {
3179
3179
  fileFiltered: (filename: string) => string;
3180
3180
  legacyFileDetected: (filename: string, platformVersion: string) => string;
3181
3181
  projectDoesNotExist: (accountId: number) => string;
3182
+ workspaceIncluded: (workspaceDir: string, archivePath: string) => string;
3183
+ fileDependencyIncluded: (packageName: string, localPath: string, archivePath: string) => string;
3184
+ malformedPackageJson: (packageJsonPath: string, error: string) => string;
3185
+ workspaceCollision: (archivePath: string, workspaceDir: string, existingWorkspace: string) => string;
3186
+ fileDependencyAlreadyIncluded: (packageName: string, archivePath: string) => string;
3187
+ updatingPackageJsonWorkspaces: (packageJsonPath: string) => string;
3188
+ updatedWorkspaces: (workspaces: string) => string;
3189
+ updatedFileDependency: (packageName: string, relativePath: string) => string;
3182
3190
  };
3183
3191
  };
3184
3192
  importData: {
package/lang/en.js CHANGED
@@ -3202,6 +3202,14 @@ export const lib = {
3202
3202
  fileFiltered: (filename) => `Ignore rule triggered for "${filename}"`,
3203
3203
  legacyFileDetected: (filename, platformVersion) => `The ${chalk.bold(filename)} file is not supported on platform version ${chalk.bold(platformVersion)} and will be ignored.`,
3204
3204
  projectDoesNotExist: (accountId) => `Upload cancelled. Run ${uiCommandReference('hs project upload')} again to create the project in ${uiAccountDescription(accountId)}.`,
3205
+ workspaceIncluded: (workspaceDir, archivePath) => `Including workspace: ${workspaceDir} → ${archivePath}`,
3206
+ fileDependencyIncluded: (packageName, localPath, archivePath) => `Including file: dependency ${packageName}: ${localPath} → ${archivePath}`,
3207
+ malformedPackageJson: (packageJsonPath, error) => `Skipping malformed package.json at ${packageJsonPath}: ${error}`,
3208
+ workspaceCollision: (archivePath, workspaceDir, existingWorkspace) => `Workspace collision: ${archivePath} from ${workspaceDir} and ${existingWorkspace}`,
3209
+ fileDependencyAlreadyIncluded: (packageName, archivePath) => `file: dependency ${packageName} already included as workspace: ${archivePath}`,
3210
+ updatingPackageJsonWorkspaces: (packageJsonPath) => `Updating package.json workspaces in archive: ${packageJsonPath}`,
3211
+ updatedWorkspaces: (workspaces) => ` Updated workspaces: ${workspaces}`,
3212
+ updatedFileDependency: (packageName, relativePath) => ` Updated dependencies.${packageName}: file:${relativePath}`,
3205
3213
  },
3206
3214
  },
3207
3215
  importData: {
@@ -13,6 +13,7 @@ import { walk } from '@hubspot/local-dev-lib/fs';
13
13
  import { uploadProject } from '@hubspot/local-dev-lib/api/projects';
14
14
  import { ensureProjectExists } from '../ensureProjectExists.js';
15
15
  import { projectContainsHsMetaFiles } from '@hubspot/project-parsing-lib/projects';
16
+ import { findAndParsePackageJsonFiles, collectWorkspaceDirectories, collectFileDependencies, } from '@hubspot/project-parsing-lib/workspaces';
16
17
  import { shouldIgnoreFile } from '@hubspot/local-dev-lib/ignoreRules';
17
18
  import { getConfigAccountIfExists } from '@hubspot/local-dev-lib/config';
18
19
  // Mock dependencies
@@ -22,6 +23,11 @@ vi.mock('@hubspot/local-dev-lib/fs');
22
23
  vi.mock('@hubspot/local-dev-lib/api/projects');
23
24
  vi.mock('../ensureProjectExists.js');
24
25
  vi.mock('@hubspot/project-parsing-lib/projects');
26
+ vi.mock('@hubspot/project-parsing-lib/workspaces', () => ({
27
+ findAndParsePackageJsonFiles: vi.fn(),
28
+ collectWorkspaceDirectories: vi.fn(),
29
+ collectFileDependencies: vi.fn(),
30
+ }));
25
31
  vi.mock('@hubspot/local-dev-lib/ignoreRules');
26
32
  vi.mock('@hubspot/local-dev-lib/config');
27
33
  vi.mock('archiver');
@@ -122,6 +128,10 @@ describe('lib/projects/upload', () => {
122
128
  vi.mocked(shouldIgnoreFile).mockReturnValue(false);
123
129
  vi.mocked(projectContainsHsMetaFiles).mockResolvedValue(false);
124
130
  vi.mocked(isV2Project).mockReturnValue(false);
131
+ // Mock workspace functions to return empty arrays
132
+ vi.mocked(findAndParsePackageJsonFiles).mockResolvedValue([]);
133
+ vi.mocked(collectWorkspaceDirectories).mockResolvedValue([]);
134
+ vi.mocked(collectFileDependencies).mockResolvedValue([]);
125
135
  vi.mocked(tmp.fileSync).mockReturnValue({
126
136
  name: path.join(tempDir, 'test.zip'),
127
137
  fd: 1,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { computeExternalArchivePath, shortHash, updatePackageJsonInArchive, } from '../workspaces.js';
5
+ describe('computeExternalArchivePath', () => {
6
+ it('places external workspace in _workspaces/ with basename-hash', () => {
7
+ const localPath = '/Users/test/company-libs/utils';
8
+ const result = computeExternalArchivePath(localPath);
9
+ const expectedHash = shortHash(path.resolve(localPath));
10
+ expect(result).toBe(path.join('_workspaces', `utils-${expectedHash}`));
11
+ });
12
+ it('does not include an external/ subdirectory', () => {
13
+ const result = computeExternalArchivePath('/Users/test/libs/utils');
14
+ expect(result).not.toContain('external');
15
+ expect(result.startsWith('_workspaces')).toBe(true);
16
+ });
17
+ it('produces different paths for different directories with same basename', () => {
18
+ const path1 = computeExternalArchivePath('/Users/test/project-a/utils');
19
+ const path2 = computeExternalArchivePath('/Users/test/project-b/utils');
20
+ expect(path1).not.toBe(path2);
21
+ expect(path1).toContain('utils-');
22
+ expect(path2).toContain('utils-');
23
+ });
24
+ it('is deterministic', () => {
25
+ const localPath = '/Users/test/libs/utils';
26
+ expect(computeExternalArchivePath(localPath)).toBe(computeExternalArchivePath(localPath));
27
+ });
28
+ it('produces paths matching _workspaces/<name>-[8 hex chars]', () => {
29
+ const result = computeExternalArchivePath('/Users/test/libs/utils');
30
+ // Normalize path separators for cross-platform compatibility
31
+ const normalized = result.replace(/\\/g, '/');
32
+ expect(normalized).toMatch(/_workspaces\/utils-[a-f0-9]{8}$/);
33
+ });
34
+ it('uses the last path segment as basename', () => {
35
+ const result = computeExternalArchivePath('/Users/test/libs/@company/shared-utils');
36
+ expect(result).toContain('shared-utils-');
37
+ const normalized = result.replace(/\\/g, '/');
38
+ expect(normalized).toMatch(/_workspaces\/shared-utils-[a-f0-9]{8}$/);
39
+ });
40
+ it('never produces paths with .. segments', () => {
41
+ const testCases = [
42
+ '/Users/other/libs/utils',
43
+ '/completely/different/path',
44
+ '/Users/test/other-project/shared',
45
+ ];
46
+ testCases.forEach(localPath => {
47
+ expect(computeExternalArchivePath(localPath)).not.toContain('..');
48
+ });
49
+ });
50
+ });
51
+ describe('shortHash', () => {
52
+ it('produces 8-character hex string', () => {
53
+ const hash = shortHash('/some/path');
54
+ expect(hash).toMatch(/^[a-f0-9]{8}$/);
55
+ });
56
+ it('is deterministic', () => {
57
+ const input = '/Users/test/workspace';
58
+ expect(shortHash(input)).toBe(shortHash(input));
59
+ });
60
+ it('produces different hashes for different inputs', () => {
61
+ const hash1 = shortHash('/path/a');
62
+ const hash2 = shortHash('/path/b');
63
+ expect(hash1).not.toBe(hash2);
64
+ });
65
+ });
66
+ describe('updatePackageJsonInArchive', () => {
67
+ const srcDir = '/project/src';
68
+ function createMockArchive() {
69
+ const appended = [];
70
+ const mock = {
71
+ append: (content, opts) => {
72
+ appended.push({ content, name: opts.name });
73
+ return mock;
74
+ },
75
+ };
76
+ return {
77
+ archive: mock,
78
+ getAppended: () => appended,
79
+ };
80
+ }
81
+ it('writes external workspace entries as absolute archive paths', async () => {
82
+ const packageJsonPath = '/project/src/app/functions/package.json';
83
+ const originalPackageJson = {
84
+ name: 'my-app',
85
+ workspaces: ['../../packages/utils'],
86
+ dependencies: {},
87
+ };
88
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
89
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
90
+ const { archive, getAppended } = createMockArchive();
91
+ const packageWorkspaces = new Map();
92
+ packageWorkspaces.set(packageJsonPath, [
93
+ '/_workspaces/packages-utils-a1b2c3d4',
94
+ '/_workspaces/packages-core-e5f6a7b8',
95
+ ]);
96
+ await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
97
+ const appended = getAppended();
98
+ expect(appended).toHaveLength(1);
99
+ const written = JSON.parse(appended[0].content);
100
+ expect(written.workspaces).toEqual([
101
+ '/_workspaces/packages-utils-a1b2c3d4',
102
+ '/_workspaces/packages-core-e5f6a7b8',
103
+ ]);
104
+ vi.restoreAllMocks();
105
+ });
106
+ it('preserves internal workspace entries as relative paths', async () => {
107
+ const packageJsonPath = '/project/src/app/functions/package.json';
108
+ const originalPackageJson = {
109
+ name: 'my-app',
110
+ workspaces: ['../packages/utils'],
111
+ };
112
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
113
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
114
+ const { archive, getAppended } = createMockArchive();
115
+ const packageWorkspaces = new Map();
116
+ packageWorkspaces.set(packageJsonPath, ['../packages/utils']);
117
+ await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
118
+ const written = JSON.parse(getAppended()[0].content);
119
+ expect(written.workspaces).toEqual(['../packages/utils']);
120
+ vi.restoreAllMocks();
121
+ });
122
+ it('writes mixed internal and external workspace entries', async () => {
123
+ const packageJsonPath = '/project/src/app/functions/package.json';
124
+ const originalPackageJson = {
125
+ name: 'my-app',
126
+ workspaces: ['../packages/utils', '/_workspaces/logger-a1b2c3d4'],
127
+ };
128
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
129
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
130
+ const { archive, getAppended } = createMockArchive();
131
+ const packageWorkspaces = new Map();
132
+ packageWorkspaces.set(packageJsonPath, [
133
+ '../packages/utils',
134
+ '/_workspaces/logger-a1b2c3d4',
135
+ ]);
136
+ await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
137
+ const written = JSON.parse(getAppended()[0].content);
138
+ expect(written.workspaces).toEqual([
139
+ '../packages/utils',
140
+ '/_workspaces/logger-a1b2c3d4',
141
+ ]);
142
+ vi.restoreAllMocks();
143
+ });
144
+ it('rewrites external file: dependencies as absolute archive paths', async () => {
145
+ const packageJsonPath = '/project/src/app/functions/package.json';
146
+ const originalPackageJson = {
147
+ name: 'my-app',
148
+ dependencies: {
149
+ '@company/logger': 'file:../../external/logger',
150
+ react: '^18.0.0',
151
+ },
152
+ };
153
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
154
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
155
+ const { archive, getAppended } = createMockArchive();
156
+ const packageFileDeps = new Map();
157
+ packageFileDeps.set(packageJsonPath, new Map([['@company/logger', '_workspaces/logger-a1b2c3d4']]));
158
+ await updatePackageJsonInArchive(archive, srcDir, new Map(), packageFileDeps);
159
+ const appended = getAppended();
160
+ expect(appended).toHaveLength(1);
161
+ const written = JSON.parse(appended[0].content);
162
+ expect(written.dependencies['@company/logger']).toBe('file:/_workspaces/logger-a1b2c3d4');
163
+ expect(written.dependencies['react']).toBe('^18.0.0');
164
+ vi.restoreAllMocks();
165
+ });
166
+ it('leaves internal file: dependencies untouched when not in packageFileDeps', async () => {
167
+ const originalPackageJson = {
168
+ name: 'my-app',
169
+ dependencies: {
170
+ '@internal/utils': 'file:../packages/utils',
171
+ react: '^18.0.0',
172
+ },
173
+ };
174
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
175
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
176
+ const { archive, getAppended } = createMockArchive();
177
+ await updatePackageJsonInArchive(archive, srcDir, new Map(), new Map());
178
+ // Nothing to update, so no package.json should be appended
179
+ expect(getAppended()).toHaveLength(0);
180
+ vi.restoreAllMocks();
181
+ });
182
+ it('uses same absolute path regardless of package.json depth', async () => {
183
+ const shallowPath = '/project/src/package.json';
184
+ const deepPath = '/project/src/app/functions/nested/package.json';
185
+ const archivePath = '/_workspaces/utils-a1b2c3d4';
186
+ const makePackageJson = () => ({
187
+ name: 'test',
188
+ workspaces: ['placeholder'],
189
+ });
190
+ vi.spyOn(fs, 'existsSync').mockReturnValue(true);
191
+ const { archive: archive1, getAppended: getAppended1 } = createMockArchive();
192
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(makePackageJson()));
193
+ const workspaces1 = new Map();
194
+ workspaces1.set(shallowPath, [archivePath]);
195
+ await updatePackageJsonInArchive(archive1, srcDir, workspaces1, new Map());
196
+ const { archive: archive2, getAppended: getAppended2 } = createMockArchive();
197
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(makePackageJson()));
198
+ const workspaces2 = new Map();
199
+ workspaces2.set(deepPath, [archivePath]);
200
+ await updatePackageJsonInArchive(archive2, srcDir, workspaces2, new Map());
201
+ const written1 = JSON.parse(getAppended1()[0].content);
202
+ const written2 = JSON.parse(getAppended2()[0].content);
203
+ expect(written1.workspaces).toEqual(['/_workspaces/utils-a1b2c3d4']);
204
+ expect(written2.workspaces).toEqual(['/_workspaces/utils-a1b2c3d4']);
205
+ vi.restoreAllMocks();
206
+ });
207
+ });
@@ -6,6 +6,7 @@ import { uploadProject } from '@hubspot/local-dev-lib/api/projects';
6
6
  import { shouldIgnoreFile } from '@hubspot/local-dev-lib/ignoreRules';
7
7
  import { isTranslationError, translate, } from '@hubspot/project-parsing-lib/translate';
8
8
  import { projectContainsHsMetaFiles } from '@hubspot/project-parsing-lib/projects';
9
+ import { findAndParsePackageJsonFiles, collectWorkspaceDirectories, collectFileDependencies, } from '@hubspot/project-parsing-lib/workspaces';
9
10
  import SpinniesManager from '../ui/SpinniesManager.js';
10
11
  import { uiAccountDescription } from '../ui/index.js';
11
12
  import { logError } from '../errorHandlers/index.js';
@@ -18,6 +19,7 @@ import { EXIT_CODES } from '../enums/exitCodes.js';
18
19
  import ProjectValidationError from '../errors/ProjectValidationError.js';
19
20
  import { walk } from '@hubspot/local-dev-lib/fs';
20
21
  import { LEGACY_CONFIG_FILES } from '../constants.js';
22
+ import { archiveWorkspacesAndDependencies } from './workspaces.js';
21
23
  async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
22
24
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
23
25
  SpinniesManager.add('upload', {
@@ -62,6 +64,11 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
62
64
  }
63
65
  const tempFile = tmp.fileSync({ postfix: '.zip' });
64
66
  uiLogger.debug(lib.projectUpload.handleProjectUpload.compressing(tempFile.name));
67
+ // Find and parse all package.json files once (avoids duplicate filesystem walks)
68
+ const parsedPackageJsons = await findAndParsePackageJsonFiles(srcDir);
69
+ // Collect workspace directories and file: dependencies from parsed data
70
+ const workspaceMappings = await collectWorkspaceDirectories(parsedPackageJsons);
71
+ const fileDependencyMappings = await collectFileDependencies(parsedPackageJsons);
65
72
  const output = fs.createWriteStream(tempFile.name);
66
73
  const archive = archiver('zip');
67
74
  const result = new Promise(resolve => output.on('close', async function () {
@@ -114,6 +121,8 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
114
121
  }
115
122
  return ignored ? false : file;
116
123
  });
124
+ // Archive workspaces and file: dependencies
125
+ await archiveWorkspacesAndDependencies(archive, srcDir, projectDir, workspaceMappings, fileDependencyMappings);
117
126
  archive.finalize();
118
127
  return result;
119
128
  }
@@ -0,0 +1,36 @@
1
+ import archiver from 'archiver';
2
+ import { WorkspaceMapping, FileDependencyMapping } from '@hubspot/project-parsing-lib/workspaces';
3
+ /**
4
+ * Result of archiving workspaces and file dependencies
5
+ */
6
+ export type WorkspaceArchiveResult = {
7
+ packageWorkspaces: Map<string, string[]>;
8
+ packageFileDeps: Map<string, Map<string, string>>;
9
+ };
10
+ /**
11
+ * Generates a short hash of the input string for use in workspace paths.
12
+ * Uses SHA256 truncated to 8 hex characters (4 billion possibilities).
13
+ */
14
+ export declare function shortHash(input: string): string;
15
+ /**
16
+ * Determines the archive path for an external workspace or file: dependency.
17
+ * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
18
+ * The hash prevents collisions between different directories with the same basename.
19
+ */
20
+ export declare function computeExternalArchivePath(absolutePath: string): string;
21
+ /**
22
+ * Updates package.json files in the archive to reflect new workspace and file: dependency paths.
23
+ *
24
+ * Workspace entries in packageWorkspaces are already in final form:
25
+ * - Internal workspaces: relative paths (e.g. "../packages/utils")
26
+ * - External workspaces: absolute paths (e.g. "/_workspaces/logger-abc")
27
+ *
28
+ * Only external file: dependencies appear in packageFileDeps; internal ones
29
+ * keep their original file: references and are left untouched.
30
+ */
31
+ export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string, string>>): Promise<void>;
32
+ /**
33
+ * Main orchestration function that handles archiving of workspaces and file dependencies.
34
+ * This is the clean integration point for upload.ts.
35
+ */
36
+ export declare function archiveWorkspacesAndDependencies(archive: archiver.Archiver, srcDir: string, projectDir: string, workspaceMappings: WorkspaceMapping[], fileDependencyMappings: FileDependencyMapping[]): Promise<WorkspaceArchiveResult>;
@@ -0,0 +1,224 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { shouldIgnoreFile } from '@hubspot/local-dev-lib/ignoreRules';
5
+ import { getPackableFiles, } from '@hubspot/project-parsing-lib/workspaces';
6
+ import { uiLogger } from '../ui/logger.js';
7
+ import { lib } from '../../lang/en.js';
8
+ /**
9
+ * Generates a short hash of the input string for use in workspace paths.
10
+ * Uses SHA256 truncated to 8 hex characters (4 billion possibilities).
11
+ */
12
+ export function shortHash(input) {
13
+ return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
14
+ }
15
+ /**
16
+ * Determines the archive path for an external workspace or file: dependency.
17
+ * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
18
+ * The hash prevents collisions between different directories with the same basename.
19
+ */
20
+ export function computeExternalArchivePath(absolutePath) {
21
+ const resolved = path.resolve(absolutePath);
22
+ const name = path.basename(resolved);
23
+ return path.join('_workspaces', `${name}-${shortHash(resolved)}`);
24
+ }
25
+ /**
26
+ * Returns true if dir is inside srcDir (i.e. it will already be included
27
+ * in the archive from the srcDir walk and must not be copied again).
28
+ */
29
+ function isInsideSrcDir(dir, srcDir) {
30
+ const rel = path.relative(path.resolve(srcDir), path.resolve(dir));
31
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
32
+ }
33
+ /**
34
+ * Creates a file filter function for workspace archiving.
35
+ * Filters files based on packable files list and ignore rules.
36
+ */
37
+ function createWorkspaceFileFilter(packableFiles) {
38
+ return (file) => {
39
+ if (packableFiles.size > 0 && !packableFiles.has(file.name)) {
40
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.fileFiltered(file.name));
41
+ return false;
42
+ }
43
+ const ignored = shouldIgnoreFile(file.name, true);
44
+ if (ignored) {
45
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.fileFiltered(file.name));
46
+ return false;
47
+ }
48
+ return file;
49
+ };
50
+ }
51
+ /**
52
+ * Archives workspace directories and returns mapping information.
53
+ *
54
+ * Internal workspaces (inside srcDir) are not archived — they are already
55
+ * included via the srcDir walk. Their relative paths (from the package.json
56
+ * directory to the workspace directory) are stored directly in the entries.
57
+ *
58
+ * External workspaces (outside srcDir) are copied to `_workspaces/<name>-<hash>`
59
+ * and their absolute archive paths (`/_workspaces/<name>-<hash>`) are stored.
60
+ */
61
+ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
62
+ const externalArchivePaths = new Map(); // resolvedDir -> archivePath
63
+ const archivePathToDir = new Map(); // archivePath -> resolvedDir (collision detection)
64
+ const packageWorkspaceEntries = new Map();
65
+ const externalsToArchive = [];
66
+ for (const mapping of workspaceMappings) {
67
+ const { workspaceDir, sourcePackageJsonPath } = mapping;
68
+ if (!packageWorkspaceEntries.has(sourcePackageJsonPath)) {
69
+ packageWorkspaceEntries.set(sourcePackageJsonPath, []);
70
+ }
71
+ if (isInsideSrcDir(workspaceDir, srcDir)) {
72
+ // Internal: already in archive from srcDir walk.
73
+ // Store the relative path from the package.json directory so npm can resolve it.
74
+ const relPath = path.relative(path.dirname(sourcePackageJsonPath), path.resolve(workspaceDir));
75
+ packageWorkspaceEntries.get(sourcePackageJsonPath).push(relPath);
76
+ }
77
+ else {
78
+ // External: archive to _workspaces/<name>-<hash>.
79
+ const archivePath = computeExternalArchivePath(workspaceDir);
80
+ const resolvedDir = path.resolve(workspaceDir);
81
+ // Detect hash collisions (different dirs mapping to the same archive path)
82
+ const existing = archivePathToDir.get(archivePath);
83
+ if (existing && existing !== resolvedDir) {
84
+ throw new Error(lib.projectUpload.handleProjectUpload.workspaceCollision(archivePath, workspaceDir, existing));
85
+ }
86
+ if (!externalArchivePaths.has(resolvedDir)) {
87
+ externalArchivePaths.set(resolvedDir, archivePath);
88
+ archivePathToDir.set(archivePath, resolvedDir);
89
+ externalsToArchive.push({ dir: workspaceDir, archivePath });
90
+ }
91
+ // Store absolute archive path so DFS can locate it in the project tree
92
+ packageWorkspaceEntries
93
+ .get(sourcePackageJsonPath)
94
+ .push(`/${archivePath}`);
95
+ }
96
+ }
97
+ // Fetch packable files in parallel (I/O optimization)
98
+ const withPackableFiles = await Promise.all(externalsToArchive.map(async (item) => ({
99
+ ...item,
100
+ packableFiles: await getPackableFiles(item.dir),
101
+ })));
102
+ // Archive directories sequentially (archiver requires sequential operations)
103
+ for (const { dir, archivePath, packableFiles } of withPackableFiles) {
104
+ uiLogger.log(lib.projectUpload.handleProjectUpload.workspaceIncluded(dir, archivePath));
105
+ archive.directory(dir, archivePath, createWorkspaceFileFilter(packableFiles));
106
+ }
107
+ return { externalArchivePaths, packageWorkspaceEntries };
108
+ }
109
+ /**
110
+ * Archives file: dependencies and returns mapping information.
111
+ *
112
+ * Internal file: dependencies (inside srcDir) are skipped — their original
113
+ * `file:` references in package.json remain valid after upload.
114
+ *
115
+ * External file: dependencies are archived to `_workspaces/<name>-<hash>`
116
+ * and tracked in the returned map so package.json can be rewritten.
117
+ */
118
+ async function archiveFileDependencies(archive, srcDir, fileDependencyMappings, externalArchivePaths) {
119
+ const packageFileDeps = new Map();
120
+ const toArchive = [];
121
+ for (const mapping of fileDependencyMappings) {
122
+ const { packageName, localPath, sourcePackageJsonPath } = mapping;
123
+ if (isInsideSrcDir(localPath, srcDir)) {
124
+ // Internal: original file: reference stays unchanged, nothing to do
125
+ continue;
126
+ }
127
+ // External: archive to _workspaces/<name>-<hash>
128
+ const archivePath = computeExternalArchivePath(localPath);
129
+ const resolvedPath = path.resolve(localPath);
130
+ if (!packageFileDeps.has(sourcePackageJsonPath)) {
131
+ packageFileDeps.set(sourcePackageJsonPath, new Map());
132
+ }
133
+ packageFileDeps.get(sourcePackageJsonPath).set(packageName, archivePath);
134
+ // Only archive each unique path once
135
+ if (!externalArchivePaths.has(resolvedPath)) {
136
+ externalArchivePaths.set(resolvedPath, archivePath);
137
+ toArchive.push({ localPath, archivePath, packageName });
138
+ }
139
+ }
140
+ // Fetch packable files in parallel (I/O optimization)
141
+ const withPackableFiles = await Promise.all(toArchive.map(async (item) => ({
142
+ ...item,
143
+ packableFiles: await getPackableFiles(item.localPath),
144
+ })));
145
+ // Archive directories sequentially (archiver requires sequential operations)
146
+ for (const { localPath, archivePath, packageName, packableFiles, } of withPackableFiles) {
147
+ uiLogger.log(lib.projectUpload.handleProjectUpload.fileDependencyIncluded(packageName, localPath, archivePath));
148
+ archive.directory(localPath, archivePath, createWorkspaceFileFilter(packableFiles));
149
+ }
150
+ return packageFileDeps;
151
+ }
152
+ /**
153
+ * Updates package.json files in the archive to reflect new workspace and file: dependency paths.
154
+ *
155
+ * Workspace entries in packageWorkspaces are already in final form:
156
+ * - Internal workspaces: relative paths (e.g. "../packages/utils")
157
+ * - External workspaces: absolute paths (e.g. "/_workspaces/logger-abc")
158
+ *
159
+ * Only external file: dependencies appear in packageFileDeps; internal ones
160
+ * keep their original file: references and are left untouched.
161
+ */
162
+ export async function updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, packageFileDeps) {
163
+ // Collect all package.json paths that need updating
164
+ const allPackageJsonPaths = new Set([
165
+ ...packageWorkspaces.keys(),
166
+ ...packageFileDeps.keys(),
167
+ ]);
168
+ for (const packageJsonPath of allPackageJsonPaths) {
169
+ if (!fs.existsSync(packageJsonPath)) {
170
+ continue;
171
+ }
172
+ let packageJson;
173
+ try {
174
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
175
+ }
176
+ catch (e) {
177
+ uiLogger.warn(lib.projectUpload.handleProjectUpload.malformedPackageJson(packageJsonPath, e instanceof Error ? e.message : String(e)));
178
+ continue;
179
+ }
180
+ const relativePackageJsonPath = path.relative(srcDir, packageJsonPath);
181
+ let modified = false;
182
+ // Update workspaces field — entries are already in their final form
183
+ const workspaceEntries = packageWorkspaces.get(packageJsonPath);
184
+ if (workspaceEntries && packageJson.workspaces) {
185
+ packageJson.workspaces = workspaceEntries;
186
+ modified = true;
187
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.updatingPackageJsonWorkspaces(relativePackageJsonPath));
188
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedWorkspaces(workspaceEntries.join(', ')));
189
+ }
190
+ // Update external file: dependencies; internal ones are left untouched
191
+ const fileDeps = packageFileDeps.get(packageJsonPath);
192
+ if (fileDeps && fileDeps.size > 0 && packageJson.dependencies) {
193
+ for (const [packageName, archivePath] of fileDeps.entries()) {
194
+ if (packageJson.dependencies[packageName]?.startsWith('file:')) {
195
+ packageJson.dependencies[packageName] = `file:/${archivePath}`;
196
+ modified = true;
197
+ uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedFileDependency(packageName, `/${archivePath}`));
198
+ }
199
+ }
200
+ }
201
+ if (modified) {
202
+ // Add modified package.json to archive (will replace the original)
203
+ archive.append(JSON.stringify(packageJson, null, 2), {
204
+ name: relativePackageJsonPath,
205
+ });
206
+ }
207
+ }
208
+ // Ensure all append operations are queued before finalize is called
209
+ // Use setImmediate to yield control and let archiver process the queue
210
+ await new Promise(resolve => setImmediate(resolve));
211
+ }
212
+ /**
213
+ * Main orchestration function that handles archiving of workspaces and file dependencies.
214
+ * This is the clean integration point for upload.ts.
215
+ */
216
+ export async function archiveWorkspacesAndDependencies(archive, srcDir, projectDir, workspaceMappings, fileDependencyMappings) {
217
+ // Archive workspace directories (internal ones are skipped, externals are copied)
218
+ const { externalArchivePaths, packageWorkspaceEntries } = await archiveWorkspaceDirectories(archive, srcDir, workspaceMappings);
219
+ // Archive external file: dependencies (internals are skipped)
220
+ const packageFileDeps = await archiveFileDependencies(archive, srcDir, fileDependencyMappings, externalArchivePaths);
221
+ // Update package.json files with new paths
222
+ await updatePackageJsonInArchive(archive, srcDir, packageWorkspaceEntries, packageFileDeps);
223
+ return { packageWorkspaces: packageWorkspaceEntries, packageFileDeps };
224
+ }
@@ -5,26 +5,9 @@ declare const inputSchemaZodObject: z.ZodObject<{
5
5
  absoluteProjectPath: z.ZodString;
6
6
  absoluteCurrentWorkingDirectory: z.ZodString;
7
7
  addApp: z.ZodBoolean;
8
- distribution: z.ZodOptional<z.ZodEnum<{
9
- marketplace: "marketplace";
10
- private: "private";
11
- }>>;
12
- auth: z.ZodOptional<z.ZodEnum<{
13
- oauth: "oauth";
14
- static: "static";
15
- }>>;
16
- features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
17
- card: "card";
18
- settings: "settings";
19
- "app-event": "app-event";
20
- page: "page";
21
- "workflow-action-tool": "workflow-action-tool";
22
- webhooks: "webhooks";
23
- "workflow-action": "workflow-action";
24
- "app-function": "app-function";
25
- "app-object": "app-object";
26
- scim: "scim";
27
- }>>>;
8
+ distribution: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"marketplace">, z.ZodLiteral<"private">]>>;
9
+ auth: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"static">, z.ZodLiteral<"oauth">]>>;
10
+ features: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"card">, z.ZodLiteral<"settings">, z.ZodLiteral<"app-function">, z.ZodLiteral<"webhooks">, z.ZodLiteral<"workflow-action">, z.ZodLiteral<"workflow-action-tool">, z.ZodLiteral<"app-object">, z.ZodLiteral<"app-event">, z.ZodLiteral<"scim">, z.ZodLiteral<"page">]>>>;
28
11
  }, z.core.$strip>;
29
12
  export type AddFeatureInputSchema = z.infer<typeof inputSchemaZodObject>;
30
13
  export declare class AddFeatureToProjectTool extends Tool<AddFeatureInputSchema> {
@@ -15,13 +15,17 @@ const inputSchema = {
15
15
  .boolean()
16
16
  .describe('Should an app be added? If there is no app in the project, an app must be added to add a feature'),
17
17
  distribution: z
18
- .enum([APP_DISTRIBUTION_TYPES.MARKETPLACE, APP_DISTRIBUTION_TYPES.PRIVATE])
19
- .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. ')
20
- .optional(),
18
+ .optional(z.union([
19
+ z.literal(APP_DISTRIBUTION_TYPES.MARKETPLACE),
20
+ z.literal(APP_DISTRIBUTION_TYPES.PRIVATE),
21
+ ]))
22
+ .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. '),
21
23
  auth: z
22
- .enum([APP_AUTH_TYPES.STATIC, APP_AUTH_TYPES.OAUTH])
23
- .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Static uses a static non changing authentication token, and is only available for private distribution. ')
24
- .optional(),
24
+ .optional(z.union([
25
+ z.literal(APP_AUTH_TYPES.STATIC),
26
+ z.literal(APP_AUTH_TYPES.OAUTH),
27
+ ]))
28
+ .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Static uses a static non changing authentication token, and is only available for private distribution. '),
25
29
  features,
26
30
  };
27
31
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -5,30 +5,10 @@ declare const inputSchemaZodObject: z.ZodObject<{
5
5
  absoluteCurrentWorkingDirectory: z.ZodString;
6
6
  name: z.ZodOptional<z.ZodString>;
7
7
  destination: z.ZodString;
8
- projectBase: z.ZodEnum<{
9
- app: "app";
10
- empty: "empty";
11
- }>;
12
- distribution: z.ZodOptional<z.ZodEnum<{
13
- marketplace: "marketplace";
14
- private: "private";
15
- }>>;
16
- auth: z.ZodOptional<z.ZodEnum<{
17
- oauth: "oauth";
18
- static: "static";
19
- }>>;
20
- features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
21
- card: "card";
22
- settings: "settings";
23
- "app-event": "app-event";
24
- page: "page";
25
- "workflow-action-tool": "workflow-action-tool";
26
- webhooks: "webhooks";
27
- "workflow-action": "workflow-action";
28
- "app-function": "app-function";
29
- "app-object": "app-object";
30
- scim: "scim";
31
- }>>>;
8
+ projectBase: z.ZodUnion<readonly [z.ZodLiteral<"empty">, z.ZodLiteral<"app">]>;
9
+ distribution: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"marketplace">, z.ZodLiteral<"private">]>>;
10
+ auth: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"static">, z.ZodLiteral<"oauth">]>>>;
11
+ features: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"card">, z.ZodLiteral<"settings">, z.ZodLiteral<"app-function">, z.ZodLiteral<"webhooks">, z.ZodLiteral<"workflow-action">, z.ZodLiteral<"workflow-action-tool">, z.ZodLiteral<"app-object">, z.ZodLiteral<"app-event">, z.ZodLiteral<"scim">, z.ZodLiteral<"page">]>>>;
32
12
  }, z.core.$strip>;
33
13
  export type CreateProjectInputSchema = z.infer<typeof inputSchemaZodObject>;
34
14
  export declare class CreateProjectTool extends Tool<CreateProjectInputSchema> {
@@ -18,14 +18,19 @@ const inputSchema = {
18
18
  .string()
19
19
  .describe('DO NOT use the current directory unless the user has explicitly stated to do so. Relative path to the directory the project will be created in.'),
20
20
  projectBase: z
21
- .enum([EMPTY_PROJECT, PROJECT_WITH_APP])
21
+ .union([z.literal(EMPTY_PROJECT), z.literal(PROJECT_WITH_APP)])
22
22
  .describe('Empty will create an empty project, and app will create a project with an app inside of it.'),
23
23
  distribution: z
24
- .enum([APP_DISTRIBUTION_TYPES.MARKETPLACE, APP_DISTRIBUTION_TYPES.PRIVATE])
25
- .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. ')
26
- .optional(),
24
+ .optional(z.union([
25
+ z.literal(APP_DISTRIBUTION_TYPES.MARKETPLACE),
26
+ z.literal(APP_DISTRIBUTION_TYPES.PRIVATE),
27
+ ]))
28
+ .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Private is used if you do not wish to distribute your app on the HubSpot marketplace. '),
27
29
  auth: z
28
- .enum([APP_AUTH_TYPES.STATIC, APP_AUTH_TYPES.OAUTH])
30
+ .optional(z.union([
31
+ z.literal(APP_AUTH_TYPES.STATIC),
32
+ z.literal(APP_AUTH_TYPES.OAUTH),
33
+ ]))
29
34
  .describe('If not specified by the user, DO NOT choose for them. This cannot be changed after a project is uploaded. Static uses a static non changing authentication token, and is only available for private distribution. ')
30
35
  .optional(),
31
36
  features,
@@ -12,15 +12,18 @@ const inputSchema = {
12
12
  absoluteCurrentWorkingDirectory,
13
13
  appId: z
14
14
  .string()
15
- .describe('The numeric app ID as a string (e.g., "3003909"). Must contain only digits. Use get-apps-info to find available app IDs.'),
15
+ .regex(/^\d+$/, 'App ID must be a numeric string')
16
+ .describe('The numeric app ID as a string (e.g., "3003909"). Use get-apps-info to find available app IDs.'),
16
17
  startDate: z
17
18
  .string()
18
- .describe('Start date for the usage patterns query in YYYY-MM-DD format (e.g., 2025-01-01).')
19
- .optional(),
19
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be in YYYY-MM-DD format')
20
+ .optional()
21
+ .describe('Start date for the usage patterns query in ISO 8601 format (e.g., 2025-01-01).'),
20
22
  endDate: z
21
23
  .string()
22
- .describe('End date for the usage patterns query in YYYY-MM-DD format (e.g., 2025-12-31).')
23
- .optional(),
24
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'End date must be in YYYY-MM-DD format')
25
+ .optional()
26
+ .describe('End date for the usage patterns query in ISO 8601 format (e.g., 2025-12-31).'),
24
27
  };
25
28
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
26
29
  const inputSchemaZodObject = z.object({ ...inputSchema });
@@ -5,12 +5,12 @@ declare const inputSchemaZodObject: z.ZodObject<{
5
5
  absoluteProjectPath: z.ZodString;
6
6
  absoluteCurrentWorkingDirectory: z.ZodString;
7
7
  buildId: z.ZodNumber;
8
- logLevel: z.ZodOptional<z.ZodEnum<{
8
+ logLevel: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
9
9
  ERROR: "ERROR";
10
10
  WARN: "WARN";
11
11
  INFO: "INFO";
12
12
  ALL: "ALL";
13
- }>>;
13
+ }>>>;
14
14
  }, z.core.$strip>;
15
15
  export type GetBuildLogsInputSchema = z.infer<typeof inputSchemaZodObject>;
16
16
  export declare class GetBuildLogsTool extends Tool<GetBuildLogsInputSchema> {
@@ -18,8 +18,9 @@ const inputSchema = {
18
18
  .describe('Build ID to fetch logs for. Use get-build-status to find recent build IDs.'),
19
19
  logLevel: z
20
20
  .enum(['ERROR', 'WARN', 'INFO', 'ALL'])
21
- .describe('Filter logs by level. ERROR: Show only errors, WARN: Show only warnings, INFO: Show only info, ALL: Show all logs. Defaults to ALL if not specified.')
22
- .optional(),
21
+ .optional()
22
+ .default('ALL')
23
+ .describe('Filter logs by level. ERROR: Show only errors, WARN: Show only warnings, INFO: Show only info, ALL: Show all logs.'),
23
24
  };
24
25
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
26
  const inputSchemaZodObject = z.object({ ...inputSchema });
@@ -84,7 +85,7 @@ export class GetBuildLogsTool extends Tool {
84
85
  if (allLogs.length === 0) {
85
86
  return formatTextContents(absoluteCurrentWorkingDirectory, `No logs found for build #${buildId} in project '${projectName}'.`);
86
87
  }
87
- const filteredLogs = filterLogsByLevel(allLogs, logLevel || 'ALL');
88
+ const filteredLogs = filterLogsByLevel(allLogs, logLevel);
88
89
  let output;
89
90
  if (filteredLogs.length === 0) {
90
91
  // No logs match filter, show all logs instead
@@ -5,7 +5,7 @@ declare const inputSchemaZodObject: z.ZodObject<{
5
5
  absoluteProjectPath: z.ZodString;
6
6
  absoluteCurrentWorkingDirectory: z.ZodString;
7
7
  buildId: z.ZodOptional<z.ZodNumber>;
8
- limit: z.ZodOptional<z.ZodNumber>;
8
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
9
9
  }, z.core.$strip>;
10
10
  export type GetBuildStatusInputSchema = z.infer<typeof inputSchemaZodObject>;
11
11
  export declare class GetBuildStatusTool extends Tool<GetBuildStatusInputSchema> {
@@ -19,8 +19,9 @@ const inputSchema = {
19
19
  .describe('Optional: Specific build ID to inspect. If omitted, shows recent builds to help identify the latest build.'),
20
20
  limit: z
21
21
  .number()
22
- .describe('Number of recent builds to fetch when buildId is not specified. Defaults to 3 if not specified.')
23
- .optional(),
22
+ .optional()
23
+ .default(3)
24
+ .describe('Number of recent builds to fetch when buildId is not specified.'),
24
25
  };
25
26
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
26
27
  const inputSchemaZodObject = z.object({ ...inputSchema });
@@ -126,7 +127,7 @@ export class GetBuildStatusTool extends Tool {
126
127
  }
127
128
  else {
128
129
  const response = await fetchProjectBuilds(accountId, projectName, {
129
- limit: limit || 3,
130
+ limit,
130
131
  });
131
132
  const { results } = response.data;
132
133
  if (!results || results.length === 0) {
@@ -3,12 +3,7 @@ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.
3
3
  import { z } from 'zod';
4
4
  declare const inputSchemaZodObject: z.ZodObject<{
5
5
  absoluteCurrentWorkingDirectory: z.ZodString;
6
- command: z.ZodOptional<z.ZodEnum<{
7
- "hs auth": "hs auth";
8
- "hs project create": "hs project create";
9
- "hs project upload": "hs project upload";
10
- "hs init": "hs init";
11
- }>>;
6
+ command: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"hs init">, z.ZodLiteral<"hs auth">, z.ZodLiteral<"hs project create">, z.ZodLiteral<"hs project upload">]>>;
12
7
  }, z.core.$strip>;
13
8
  type InputSchemaType = z.infer<typeof inputSchemaZodObject>;
14
9
  export declare class GuidedWalkthroughTool extends Tool<InputSchemaType> {
@@ -14,7 +14,12 @@ const nextCommands = {
14
14
  const inputSchema = {
15
15
  absoluteCurrentWorkingDirectory,
16
16
  command: z
17
- .enum(['hs init', 'hs auth', 'hs project create', 'hs project upload'])
17
+ .union([
18
+ z.literal('hs init'),
19
+ z.literal('hs auth'),
20
+ z.literal('hs project create'),
21
+ z.literal('hs project upload'),
22
+ ])
18
23
  .describe('The command to learn more about. Start with `hs init`')
19
24
  .optional(),
20
25
  };
@@ -1,17 +1,6 @@
1
1
  import z from 'zod';
2
2
  export declare const absoluteProjectPath: z.ZodString;
3
3
  export declare const absoluteCurrentWorkingDirectory: z.ZodString;
4
- export declare const features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
5
- card: "card";
6
- settings: "settings";
7
- "app-event": "app-event";
8
- page: "page";
9
- "workflow-action-tool": "workflow-action-tool";
10
- webhooks: "webhooks";
11
- "workflow-action": "workflow-action";
12
- "app-function": "app-function";
13
- "app-object": "app-object";
14
- scim: "scim";
15
- }>>>;
4
+ export declare const features: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"card">, z.ZodLiteral<"settings">, z.ZodLiteral<"app-function">, z.ZodLiteral<"webhooks">, z.ZodLiteral<"workflow-action">, z.ZodLiteral<"workflow-action-tool">, z.ZodLiteral<"app-object">, z.ZodLiteral<"app-event">, z.ZodLiteral<"scim">, z.ZodLiteral<"page">]>>>;
16
5
  export declare const docsSearchQuery: z.ZodString;
17
6
  export declare const docUrl: z.ZodString;
@@ -6,19 +6,23 @@ export const absoluteCurrentWorkingDirectory = z
6
6
  .string()
7
7
  .describe('The absolute path to the current working directory.');
8
8
  export const features = z
9
- .array(z.enum([
10
- 'card',
11
- 'settings',
12
- 'app-function',
13
- 'webhooks',
14
- 'workflow-action',
15
- 'workflow-action-tool',
16
- 'app-object',
17
- 'app-event',
18
- 'scim',
19
- 'page',
9
+ .array(z.union([
10
+ z.literal('card'),
11
+ z.literal('settings'),
12
+ z
13
+ .literal('app-function')
14
+ .describe('Also known as a public serverless function'),
15
+ z.literal('webhooks'),
16
+ z
17
+ .literal('workflow-action')
18
+ .describe('Also known as a custom workflow action.'),
19
+ z.literal('workflow-action-tool').describe('Also known as agent tools.'),
20
+ z.literal('app-object'),
21
+ z.literal('app-event'),
22
+ z.literal('scim'),
23
+ z.literal('page'),
20
24
  ]))
21
- .describe('The features to include in the project, multiple options can be selected. "app-function" is also known as a public serverless function. "workflow-action" is also known as a custom workflow action. "workflow-action-tool" is also known as agent tools.')
25
+ .describe('The features to include in the project, multiple options can be selected')
22
26
  .optional();
23
27
  export const docsSearchQuery = z
24
28
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.0.3-experimental.0",
3
+ "version": "8.0.3-experimental.1",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
@@ -8,7 +8,7 @@
8
8
  "dependencies": {
9
9
  "@hubspot/cms-dev-server": "1.2.16",
10
10
  "@hubspot/local-dev-lib": "5.1.1",
11
- "@hubspot/project-parsing-lib": "0.12.0",
11
+ "@hubspot/project-parsing-lib": "0.2.0-experimental.0",
12
12
  "@hubspot/serverless-dev-runtime": "7.0.7",
13
13
  "@hubspot/ui-extensions-dev-server": "1.1.8",
14
14
  "@inquirer/prompts": "7.1.0",