@hubspot/local-dev-lib 5.8.2 → 5.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api/appsDev.d.ts CHANGED
@@ -5,5 +5,6 @@ export declare function fetchPublicAppDeveloperTestAccountInstallData(appId: num
5
5
  export declare function fetchPublicAppProductionInstallCounts(appId: number, accountId: number): HubSpotPromise<PublicAppInstallCounts>;
6
6
  export declare function fetchPublicAppMetadata(appId: number, accountId: number): HubSpotPromise<PublicApp>;
7
7
  export declare function installStaticAuthAppOnTestAccount(appId: number, accountId: number, scopeGroupIds: number[]): HubSpotPromise<void>;
8
+ export declare function installStaticAuthAppOnCurrentAccount(appId: number, accountId: number, scopeGroupIds: number[]): HubSpotPromise<void>;
8
9
  export declare function fetchAppMetadataByUid(appUid: string, accountId: number): HubSpotPromise<PublicApp>;
9
10
  export declare function fetchAppMetadataBySourceId(projectId: number, appUid: string, accountId: number): HubSpotPromise<PublicApp>;
package/api/appsDev.js CHANGED
@@ -31,6 +31,16 @@ export function installStaticAuthAppOnTestAccount(appId, accountId, scopeGroupId
31
31
  },
32
32
  });
33
33
  }
34
+ export function installStaticAuthAppOnCurrentAccount(appId, accountId, scopeGroupIds) {
35
+ return http.post(accountId, {
36
+ url: `${APPS_HUBLETS_API_PATH}/current-account`,
37
+ data: {
38
+ appId,
39
+ targetInstallPortalId: accountId,
40
+ scopeGroupIds,
41
+ },
42
+ });
43
+ }
34
44
  export function fetchAppMetadataByUid(appUid, accountId) {
35
45
  return http.get(accountId, {
36
46
  url: `${APPS_DEV_API_PATH}/full/portal/sourceId`,
@@ -5,6 +5,7 @@ const TEST_ACCOUNTS_API_PATH_V3 = 'integrators/test-portals/v3';
5
5
  export function fetchDeveloperTestAccounts(accountId) {
6
6
  return http.get(accountId, {
7
7
  url: TEST_ACCOUNTS_API_PATH,
8
+ timeout: SANDBOX_TIMEOUT,
8
9
  });
9
10
  }
10
11
  export function createDeveloperTestAccount(accountId, accountInfo) {
@@ -1,5 +1,5 @@
1
1
  import { AxiosResponse } from 'axios';
2
- import { FileMapperNode, FileMapperOptions, FileTree } from '../types/Files.js';
2
+ import { DirectoryMetaNode, FileMapperNode, FileMapperOptions, FileTree } from '../types/Files.js';
3
3
  import { HubSpotPromise } from '../types/Http.js';
4
4
  export declare function createFileMapperNodeFromStreamResponse(filePath: string, response: Partial<AxiosResponse>): FileMapperNode;
5
5
  export declare function upload(accountId: number, src: string, dest: string, options?: FileMapperOptions): HubSpotPromise<void>;
@@ -10,3 +10,4 @@ export declare function downloadDefault(accountId: number, filepath: string, opt
10
10
  export declare function deleteFile(accountId: number, filePath: string): HubSpotPromise<void>;
11
11
  export declare function moveFile(accountId: number, srcPath: string, destPath: string): HubSpotPromise<void>;
12
12
  export declare function getDirectoryContentsByPath(accountId: number, path: string): HubSpotPromise<FileMapperNode>;
13
+ export declare function getDirectoryMetaByPath(accountId: number, path: string, options?: FileMapperOptions): HubSpotPromise<DirectoryMetaNode>;
package/api/fileMapper.js CHANGED
@@ -89,3 +89,10 @@ export function getDirectoryContentsByPath(accountId, path) {
89
89
  url: `${FILE_MAPPER_API_PATH}/meta/${path}`,
90
90
  });
91
91
  }
92
+ // getDirectoryContentsByPath has the wrong return type for /meta/ — children are strings, not FileMapperNodes
93
+ export function getDirectoryMetaByPath(accountId, path, options = {}) {
94
+ return http.get(accountId, {
95
+ url: `${FILE_MAPPER_API_PATH}/meta/${path.split('/').map(encodeURIComponent).join('/')}`,
96
+ ...options,
97
+ });
98
+ }
package/config/state.js CHANGED
@@ -7,6 +7,7 @@ import { STATE_FLAGS } from '../constants/config.js';
7
7
  const i18nKey = 'config.state';
8
8
  const DEFAULT_STATE = {
9
9
  [STATE_FLAGS.MCP_TOTAL_TOOL_CALLS]: 0,
10
+ [STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT]: undefined,
10
11
  [STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION]: undefined,
11
12
  };
12
13
  function ensureCLIDirectory() {
@@ -56,6 +56,7 @@ export declare const HUBSPOT_ACCOUNT_TYPE_STRINGS: {
56
56
  };
57
57
  export declare const STATE_FLAGS: {
58
58
  readonly MCP_TOTAL_TOOL_CALLS: "mcpTotalToolCalls";
59
+ readonly MCP_PROMOTION_LAST_SHOWN_AT: "mcpPromotionLastShownAt";
59
60
  readonly USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION: "usageTrackingMessageLastShowVersion";
60
61
  };
61
62
  export declare const CONFIG_FLAGS: {
@@ -59,6 +59,7 @@ export const HUBSPOT_ACCOUNT_TYPE_STRINGS = {
59
59
  };
60
60
  export const STATE_FLAGS = {
61
61
  MCP_TOTAL_TOOL_CALLS: 'mcpTotalToolCalls',
62
+ MCP_PROMOTION_LAST_SHOWN_AT: 'mcpPromotionLastShownAt',
62
63
  USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION: 'usageTrackingMessageLastShowVersion',
63
64
  };
64
65
  export const CONFIG_FLAGS = {
@@ -3,10 +3,15 @@ export declare function isPathToFile(filepath: string): boolean;
3
3
  export declare function isPathToModule(filepath: string): boolean;
4
4
  export declare function isPathToRoot(filepath: string): boolean;
5
5
  export declare function isPathToHubspot(filepath: string): boolean;
6
- export declare function getFileMapperQueryValues(cmsPublishMode?: CmsPublishMode | null, { staging, assetVersion }?: FileMapperInputOptions): FileMapperOptions;
6
+ export declare function getFileMapperQueryValues(cmsPublishMode?: CmsPublishMode | null, { staging, assetVersion, timeout }?: FileMapperInputOptions): FileMapperOptions;
7
7
  export declare function getTypeDataFromPath(src: string): PathTypeData;
8
8
  export declare function recurseFolder(node: FileMapperNode, callback: RecursiveFileMapperCallback, filepath?: string, depth?: number): boolean;
9
9
  export declare function writeUtimes(accountId: number, filepath: string, node: FileMapperNode): Promise<void>;
10
+ /**
11
+ * @deprecated Use downloadFileOrFolder instead. This function fetches the
12
+ * entire directory tree in a single request, which times out for large
13
+ * directories.
14
+ */
10
15
  export declare function fetchFolderFromApi(accountId: number, src: string, cmsPublishMode?: CmsPublishMode, options?: FileMapperInputOptions): Promise<FileMapperNode>;
11
16
  /**
12
17
  * Fetch a file/folder and write to local file system.
package/lib/fileMapper.js CHANGED
@@ -4,16 +4,13 @@ import path from 'path';
4
4
  import PQueue from 'p-queue';
5
5
  import { getCwd, getExt, convertToLocalFileSystemPath, isAllowedExtension, } from './path.js';
6
6
  import { logger } from './logger.js';
7
- import { fetchFileStream, download, downloadDefault, } from '../api/fileMapper.js';
7
+ import { fetchFileStream, download, downloadDefault, getDirectoryMetaByPath, } from '../api/fileMapper.js';
8
8
  import { MODULE_EXTENSION, FUNCTIONS_EXTENSION, JSR_ALLOWED_EXTENSIONS, } from '../constants/extensions.js';
9
9
  import { CMS_PUBLISH_MODE } from '../constants/files.js';
10
10
  import { isTimeoutError } from '../errors/index.js';
11
11
  import { i18n } from '../utils/lang.js';
12
12
  import { FileSystemError } from '../models/FileSystemError.js';
13
13
  const i18nKey = 'lib.fileMapper';
14
- const queue = new PQueue({
15
- concurrency: 10,
16
- });
17
14
  export function isPathToFile(filepath) {
18
15
  const ext = getExt(filepath);
19
16
  return !!ext && ext !== MODULE_EXTENSION && ext !== FUNCTIONS_EXTENSION;
@@ -37,13 +34,14 @@ function useApiBuffer(cmsPublishMode) {
37
34
  return cmsPublishMode === CMS_PUBLISH_MODE.draft;
38
35
  }
39
36
  // Determines API param based on publish mode and options
40
- export function getFileMapperQueryValues(cmsPublishMode, { staging, assetVersion } = {}) {
37
+ export function getFileMapperQueryValues(cmsPublishMode, { staging, assetVersion, timeout } = {}) {
41
38
  return {
42
39
  params: {
43
40
  buffer: useApiBuffer(cmsPublishMode),
44
41
  environmentId: staging ? 2 : 1,
45
42
  version: assetVersion,
46
43
  },
44
+ ...(timeout !== undefined && { timeout }),
47
45
  };
48
46
  }
49
47
  // Determines version number to log based on input.options
@@ -141,40 +139,6 @@ async function fetchAndWriteFileStream(accountId, srcPath, filepath, cmsPublishM
141
139
  const node = await fetchFileStream(accountId, srcPath, filepath, getFileMapperQueryValues(cmsPublishMode, options));
142
140
  await writeUtimes(accountId, filepath, node);
143
141
  }
144
- // Writes an individual file or folder (not recursive). If file source is missing, the
145
- //file is fetched.
146
- async function writeFileMapperNode(accountId, filepath, node, cmsPublishMode, options = {}) {
147
- const localFilepath = convertToLocalFileSystemPath(path.resolve(filepath));
148
- if (await skipExisting(localFilepath, options.overwrite)) {
149
- logger.log(i18n(`${i18nKey}.skippedExisting`, {
150
- filepath: localFilepath,
151
- }));
152
- return true;
153
- }
154
- if (!node.folder) {
155
- try {
156
- await fetchAndWriteFileStream(accountId, node.path, localFilepath, cmsPublishMode, options);
157
- return true;
158
- }
159
- catch (err) {
160
- return false;
161
- }
162
- }
163
- try {
164
- await fs.ensureDir(localFilepath);
165
- logger.log(i18n(`${i18nKey}.wroteFolder`, {
166
- filepath: localFilepath,
167
- }));
168
- }
169
- catch (err) {
170
- throw new FileSystemError({ cause: err }, {
171
- filepath: localFilepath,
172
- accountId,
173
- operation: 'write',
174
- });
175
- }
176
- return true;
177
- }
178
142
  async function downloadFile(accountId, src, destPath, cmsPublishMode, options = {}) {
179
143
  const { isFile, isHubspot } = getTypeDataFromPath(src);
180
144
  if (!isFile) {
@@ -201,7 +165,6 @@ async function downloadFile(accountId, src, destPath, cmsPublishMode, options =
201
165
  }
202
166
  const localFsPath = convertToLocalFileSystemPath(filepath);
203
167
  await fetchAndWriteFileStream(accountId, src, localFsPath, cmsPublishMode, options);
204
- await queue.onIdle();
205
168
  logger.success(i18n(`${i18nKey}.completedFetch`, {
206
169
  src,
207
170
  version: getAssetVersionIdentifier(options.assetVersion, src),
@@ -218,6 +181,11 @@ async function downloadFile(accountId, src, destPath, cmsPublishMode, options =
218
181
  }
219
182
  }
220
183
  }
184
+ /**
185
+ * @deprecated Use downloadFileOrFolder instead. This function fetches the
186
+ * entire directory tree in a single request, which times out for large
187
+ * directories.
188
+ */
221
189
  export async function fetchFolderFromApi(accountId, src, cmsPublishMode, options = {}) {
222
190
  const { isRoot, isFolder, isHubspot } = getTypeDataFromPath(src);
223
191
  if (!isFolder) {
@@ -233,44 +201,82 @@ export async function fetchFolderFromApi(accountId, src, cmsPublishMode, options
233
201
  logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId }));
234
202
  return node;
235
203
  }
204
+ async function queueFolderTree(accountId, src, localPath, directoryNode, ctx) {
205
+ const { cmsPublishMode, options, failedPaths, queue } = ctx;
206
+ if (!directoryNode.folder)
207
+ return;
208
+ const { isRoot } = getTypeDataFromPath(src);
209
+ let children = directoryNode.children || [];
210
+ if (isRoot) {
211
+ children = ['@hubspot', ...children];
212
+ }
213
+ try {
214
+ await fs.ensureDir(localPath);
215
+ }
216
+ catch (err) {
217
+ throw new FileSystemError({ cause: err }, { filepath: localPath, accountId, operation: 'write' });
218
+ }
219
+ logger.log(i18n(`${i18nKey}.wroteFolder`, { filepath: localPath }));
220
+ const queryValues = getFileMapperQueryValues(cmsPublishMode, options);
221
+ const queueFileDownload = (remotePath, destPath) => {
222
+ queue.add(async () => {
223
+ try {
224
+ await fetchAndWriteFileStream(accountId, remotePath, destPath, cmsPublishMode, options);
225
+ }
226
+ catch (err) {
227
+ failedPaths.add(remotePath);
228
+ logger.debug(i18n(`${i18nKey}.errors.failedToFetchFile`, {
229
+ src: remotePath,
230
+ dest: destPath,
231
+ }));
232
+ }
233
+ });
234
+ };
235
+ for (const childName of children) {
236
+ const childRemotePath = isRoot ? childName : `${src}/${childName}`;
237
+ const childLocalPath = convertToLocalFileSystemPath(path.join(localPath, childName));
238
+ if (isAllowedExtension(childRemotePath, [...JSR_ALLOWED_EXTENSIONS])) {
239
+ queueFileDownload(childRemotePath, childLocalPath);
240
+ }
241
+ else {
242
+ const { data: childNode } = await getDirectoryMetaByPath(accountId, childRemotePath, queryValues);
243
+ if (childNode) {
244
+ if (childNode.folder) {
245
+ await queueFolderTree(accountId, childRemotePath, childLocalPath, childNode, ctx);
246
+ }
247
+ else {
248
+ queueFileDownload(childRemotePath, childLocalPath);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
236
254
  async function downloadFolder(accountId, src, destPath, cmsPublishMode, options = {}) {
255
+ src = src.length > 1 ? src.replace(/\/+$/, '') : src;
256
+ const dest = path.resolve(destPath);
257
+ const { isRoot } = getTypeDataFromPath(src);
258
+ const metaPath = isRoot ? '/' : src;
259
+ const queryValues = getFileMapperQueryValues(cmsPublishMode, options);
260
+ const failedPaths = new Set();
261
+ const queue = new PQueue({ concurrency: 10 });
237
262
  try {
238
- const node = await fetchFolderFromApi(accountId, src, cmsPublishMode, options);
239
- if (!node) {
263
+ const { data: rootMeta } = await getDirectoryMetaByPath(accountId, metaPath, queryValues);
264
+ if (!rootMeta)
240
265
  return;
241
- }
242
- const dest = path.resolve(destPath);
266
+ logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId }));
243
267
  const rootPath = dest === getCwd()
244
- ? convertToLocalFileSystemPath(path.resolve(dest, node.name))
268
+ ? convertToLocalFileSystemPath(path.resolve(dest, rootMeta.name || path.basename(src)))
245
269
  : dest;
246
- let success = true;
247
- recurseFolder(node, (childNode, filepath) => {
248
- queue.add(async () => {
249
- const succeeded = await writeFileMapperNode(accountId, filepath || '', childNode, cmsPublishMode, options);
250
- if (succeeded === false) {
251
- success = false;
252
- logger.debug(i18n(`${i18nKey}.errors.failedToFetchFile`, {
253
- src: childNode.path,
254
- dest: filepath || '',
255
- }));
256
- }
257
- });
258
- return success;
259
- }, rootPath);
270
+ await queueFolderTree(accountId, src, rootPath, rootMeta, {
271
+ cmsPublishMode,
272
+ options,
273
+ failedPaths,
274
+ queue,
275
+ });
260
276
  await queue.onIdle();
261
- if (success) {
262
- logger.success(i18n(`${i18nKey}.completedFolderFetch`, {
263
- src,
264
- version: getAssetVersionIdentifier(options.assetVersion, src),
265
- dest,
266
- }));
267
- }
268
- else {
269
- // TODO: Fix this exception. It is triggering the catch block so this error is being rewritten
270
- throw new Error(i18n(`${i18nKey}.errors.incompleteFetch`, { src }));
271
- }
272
277
  }
273
278
  catch (err) {
279
+ queue.clear();
274
280
  const error = err;
275
281
  if (isTimeoutError(error)) {
276
282
  throw new Error(i18n(`${i18nKey}.errors.assetTimeout`), { cause: error });
@@ -279,6 +285,14 @@ async function downloadFolder(accountId, src, destPath, cmsPublishMode, options
279
285
  throw new Error(i18n(`${i18nKey}.errors.failedToFetchFolder`, { src, dest: destPath }), { cause: err });
280
286
  }
281
287
  }
288
+ if (failedPaths.size > 0) {
289
+ throw new Error(i18n(`${i18nKey}.errors.incompleteFetch`, { src }));
290
+ }
291
+ logger.success(i18n(`${i18nKey}.completedFolderFetch`, {
292
+ src,
293
+ version: getAssetVersionIdentifier(options.assetVersion, src),
294
+ dest,
295
+ }));
282
296
  }
283
297
  /**
284
298
  * Fetch a file/folder and write to local file system.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/local-dev-lib",
3
- "version": "5.8.2",
3
+ "version": "5.9.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "Provides library functionality for HubSpot local development tooling, including the HubSpot CLI",
6
6
  "files": [
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/HubSpot/hubspot-local-dev-lib"
12
+ "url": "git+https://github.com/HubSpot/hubspot-local-dev-lib.git"
13
13
  },
14
14
  "publishConfig": {
15
15
  "registry": "https://registry.npmjs.org/",
package/types/Config.d.ts CHANGED
@@ -29,6 +29,7 @@ export type GitInclusionResult = {
29
29
  export type ConfigFlag = ValueOf<typeof CONFIG_FLAGS>;
30
30
  export type HubSpotState = {
31
31
  [STATE_FLAGS.MCP_TOTAL_TOOL_CALLS]: number;
32
+ [STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT]?: string;
32
33
  [STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION]?: string;
33
34
  };
34
35
  export type HubSpotConfigErrorType = ValueOf<typeof HUBSPOT_CONFIG_ERROR_TYPES>;
package/types/Files.d.ts CHANGED
@@ -18,6 +18,14 @@ export type FileMapperNode = {
18
18
  folder: boolean;
19
19
  children: Array<FileMapperNode>;
20
20
  };
21
+ export type DirectoryMetaNode = {
22
+ name: string;
23
+ path: string;
24
+ folder: boolean;
25
+ children: Array<string>;
26
+ createdAt?: number;
27
+ updatedAt?: number;
28
+ };
21
29
  export type CmsPublishMode = ValueOf<typeof CMS_PUBLISH_MODE>;
22
30
  export type FileMapperOptions = Omit<HttpOptions, 'url'>;
23
31
  export type FileMapperInputOptions = {