@hubspot/cli 8.1.0-experimental.0 → 8.1.1-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.
@@ -1,4 +1,4 @@
1
- import { AccountArgs, CommonArgs, ConfigArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectListArgs = CommonArgs & ConfigArgs & AccountArgs;
1
+ import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ type ProjectListArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs;
3
3
  declare const projectListCommand: YargsCommandModule<unknown, ProjectListArgs>;
4
4
  export default projectListCommand;
@@ -52,6 +52,7 @@ const builder = makeYargsBuilder(projectListBuilder, command, describe, {
52
52
  useGlobalOptions: true,
53
53
  useConfigOptions: true,
54
54
  useAccountOptions: true,
55
+ useEnvironmentOptions: true,
55
56
  });
56
57
  const projectListCommand = {
57
58
  command,
@@ -4,6 +4,7 @@ export type ProjectUploadArgs = CommonArgs & JSONOutputArgs & {
4
4
  message: string;
5
5
  m: string;
6
6
  skipValidation: boolean;
7
+ skipNpmAudit: boolean;
7
8
  profile?: string;
8
9
  };
9
10
  declare const projectUploadCommand: YargsCommandModule<unknown, ProjectUploadArgs>;
@@ -18,7 +18,7 @@ import { projectProfilePrompt } from '../../lib/prompts/projectProfilePrompt.js'
18
18
  const command = 'upload';
19
19
  const describe = commands.project.upload.describe;
20
20
  async function handler(args) {
21
- const { forceCreate, message, derivedAccountId, skipValidation, formatOutputAsJson, profile: profileOption, useEnv: useEnvOption, exit, addUsageMetadata, } = args;
21
+ const { forceCreate, message, derivedAccountId, skipValidation, skipNpmAudit, formatOutputAsJson, profile: profileOption, useEnv: useEnvOption, exit, addUsageMetadata, } = args;
22
22
  const jsonOutput = {};
23
23
  const { projectConfig, projectDir } = await getProjectConfig();
24
24
  try {
@@ -66,6 +66,7 @@ async function handler(args) {
66
66
  isUploadCommand: true,
67
67
  sendIR: !isLegacyProject(projectConfig.platformVersion),
68
68
  skipValidation,
69
+ skipNpmAudit,
69
70
  profile: profileName,
70
71
  });
71
72
  if (uploadError) {
@@ -127,6 +128,11 @@ function projectUploadBuilder(yargs) {
127
128
  hidden: true,
128
129
  default: false,
129
130
  },
131
+ 'skip-npm-audit': {
132
+ describe: commands.project.upload.options.skipNpmAudit.describe,
133
+ type: 'boolean',
134
+ default: false,
135
+ },
130
136
  profile: {
131
137
  type: 'string',
132
138
  alias: 'p',
package/lang/en.d.ts CHANGED
@@ -1826,6 +1826,9 @@ export declare const commands: {
1826
1826
  profile: {
1827
1827
  describe: string;
1828
1828
  };
1829
+ skipNpmAudit: {
1830
+ describe: string;
1831
+ };
1829
1832
  };
1830
1833
  };
1831
1834
  watch: {
@@ -3089,6 +3092,9 @@ export declare const lib: {
3089
3092
  process: {
3090
3093
  exitDebug: (signal: string) => string;
3091
3094
  };
3095
+ handlerLogFile: {
3096
+ saved: (filePath: string) => string;
3097
+ };
3092
3098
  DevServerManager: {
3093
3099
  portConflict: (port: string) => string;
3094
3100
  notInitialized: string;
@@ -3391,6 +3397,10 @@ export declare const lib: {
3391
3397
  updatingPackageJsonWorkspaces: (packageJsonPath: string) => string;
3392
3398
  updatedWorkspaces: (workspaces: string) => string;
3393
3399
  updatedFileDependency: (packageName: string, relativePath: string) => string;
3400
+ npmAuditClean: (packageRoot: string) => string;
3401
+ npmAuditIssues: (packageRoot: string, details: string) => string;
3402
+ npmAuditNpmUnavailable: (packageRoot: string) => string;
3403
+ npmAuditNonZeroExit: (packageRoot: string, exitCode: number) => string;
3394
3404
  };
3395
3405
  };
3396
3406
  importData: {
package/lang/en.js CHANGED
@@ -1843,6 +1843,9 @@ export const commands = {
1843
1843
  profile: {
1844
1844
  describe: 'Profile to target for this upload',
1845
1845
  },
1846
+ skipNpmAudit: {
1847
+ describe: 'Skip the npm audit security check before uploading',
1848
+ },
1846
1849
  },
1847
1850
  },
1848
1851
  watch: {
@@ -3115,6 +3118,9 @@ export const lib = {
3115
3118
  process: {
3116
3119
  exitDebug: (signal) => `Attempting to gracefully exit. Triggered by ${signal}`,
3117
3120
  },
3121
+ handlerLogFile: {
3122
+ saved: (filePath) => `Debug logs can be viewed at ${filePath}`,
3123
+ },
3118
3124
  DevServerManager: {
3119
3125
  portConflict: (port) => `The port ${port} is already in use.`,
3120
3126
  notInitialized: 'The Dev Server Manager must be initialized before it is started.',
@@ -3417,6 +3423,10 @@ export const lib = {
3417
3423
  updatingPackageJsonWorkspaces: (packageJsonPath) => `Updating package.json workspaces in archive: ${packageJsonPath}`,
3418
3424
  updatedWorkspaces: (workspaces) => ` Updated workspaces: ${workspaces}`,
3419
3425
  updatedFileDependency: (packageName, relativePath) => ` Updated dependencies.${packageName}: file:${relativePath}`,
3426
+ npmAuditClean: (packageRoot) => `npm audit: No npm dependency issues found for ${chalk.bold(packageRoot)}`,
3427
+ npmAuditIssues: (packageRoot, details) => `npm audit: security issues found for ${chalk.bold(packageRoot)}: ${details}`,
3428
+ npmAuditNpmUnavailable: (packageRoot) => `npm audit: skipped for ${chalk.bold(packageRoot)} (npm not available in PATH)`,
3429
+ npmAuditNonZeroExit: (packageRoot, exitCode) => `npm audit: ${chalk.bold(packageRoot)} exited with code ${exitCode}`,
3420
3430
  },
3421
3431
  },
3422
3432
  importData: {
@@ -0,0 +1,29 @@
1
+ export type ConfigType = 'local' | 'global';
2
+ export type ExecutionSource = 'ci' | 'mcp' | 'user';
3
+ export type UsageTrackingMeta = {
4
+ action?: string;
5
+ os?: string;
6
+ nodeVersion?: string;
7
+ nodeMajorVersion?: string;
8
+ version?: string;
9
+ command?: string;
10
+ authType?: string;
11
+ step?: string;
12
+ assetType?: string;
13
+ mode?: string;
14
+ type?: string | number;
15
+ file?: boolean;
16
+ successful?: boolean;
17
+ configType?: ConfigType;
18
+ executionSource?: ExecutionSource;
19
+ platformVersion?: string;
20
+ executionTime?: number;
21
+ };
22
+ export type UsageTrackingRequest = {
23
+ portalId?: number;
24
+ accountId?: number;
25
+ eventName: string;
26
+ eventClass: string;
27
+ meta: UsageTrackingMeta;
28
+ };
29
+ export declare function sendUsageEvent(request: UsageTrackingRequest): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { http } from '@hubspot/local-dev-lib/http';
2
+ import { http as unauthedHttp } from '@hubspot/local-dev-lib/http/unauthed';
3
+ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
4
+ const USAGE_PATH = 'local/dev/tools/proxy/v1/usage';
5
+ const USAGE_AUTHENTICATED_PATH = `${USAGE_PATH}/authenticated`;
6
+ export async function sendUsageEvent(request) {
7
+ const { accountId } = request;
8
+ if (accountId) {
9
+ try {
10
+ const account = getConfigAccountById(accountId);
11
+ if (account?.authType === 'personalaccesskey') {
12
+ await http.post(accountId, {
13
+ url: USAGE_AUTHENTICATED_PATH,
14
+ data: request,
15
+ });
16
+ return;
17
+ }
18
+ }
19
+ catch (_e) { }
20
+ }
21
+ try {
22
+ await unauthedHttp.post({
23
+ url: USAGE_PATH,
24
+ data: request,
25
+ });
26
+ }
27
+ catch (_e) { }
28
+ }
@@ -0,0 +1,10 @@
1
+ import type { ParsedPackageJson } from '@hubspot/project-parsing-lib/workspaces';
2
+ export declare function summarizeNpmAuditJson(source: string): string | null;
3
+ type RunNpmAuditsBeforeProjectUploadArgs = {
4
+ srcDir: string;
5
+ projectDir: string;
6
+ parsedPackageJsons: ParsedPackageJson[];
7
+ isLegacyPlatform: boolean;
8
+ };
9
+ export declare function runNpmAuditsBeforeProjectUpload({ srcDir, projectDir, parsedPackageJsons, isLegacyPlatform, }: RunNpmAuditsBeforeProjectUploadArgs): Promise<void>;
10
+ export {};
@@ -0,0 +1,73 @@
1
+ import path from 'path';
2
+ import { runNpmAuditJson } from '@hubspot/ui-extensions-dev-server';
3
+ import { lib } from '../../lang/en.js';
4
+ import { uiLogger } from '../ui/logger.js';
5
+ export function summarizeNpmAuditJson(source) {
6
+ try {
7
+ const data = JSON.parse(source);
8
+ const errorMessage = data.error?.message ?? data.error?.summary;
9
+ if (errorMessage) {
10
+ return errorMessage;
11
+ }
12
+ const v = data.metadata?.vulnerabilities;
13
+ if (!v) {
14
+ return null;
15
+ }
16
+ const total = v.total ?? 0;
17
+ if (total === 0) {
18
+ return null;
19
+ }
20
+ const severityOrder = [
21
+ 'critical',
22
+ 'high',
23
+ 'moderate',
24
+ 'low',
25
+ 'info',
26
+ ];
27
+ const parts = severityOrder
28
+ .filter(severity => (v[severity] ?? 0) > 0)
29
+ .map(severity => `${v[severity]} ${severity}`);
30
+ return `${total} total (${parts.join(', ')})`;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export async function runNpmAuditsBeforeProjectUpload({ srcDir, projectDir, parsedPackageJsons, isLegacyPlatform, }) {
37
+ const auditRoots = new Set();
38
+ if (isLegacyPlatform) {
39
+ auditRoots.add(srcDir);
40
+ }
41
+ else {
42
+ for (const { dir } of parsedPackageJsons) {
43
+ auditRoots.add(dir);
44
+ }
45
+ if (auditRoots.size === 0) {
46
+ auditRoots.add(srcDir);
47
+ }
48
+ }
49
+ const auditRootArray = [...auditRoots];
50
+ const results = await Promise.all(auditRootArray.map(auditRoot => runNpmAuditJson(auditRoot)));
51
+ for (let i = 0; i < auditRootArray.length; i++) {
52
+ const auditRoot = auditRootArray[i];
53
+ const result = results[i];
54
+ if (result.skipped) {
55
+ continue;
56
+ }
57
+ const relativeRoot = path.relative(projectDir, auditRoot) || '.';
58
+ const summary = summarizeNpmAuditJson(result.source);
59
+ if (result.exitCode === 127) {
60
+ uiLogger.warn(lib.projectUpload.handleProjectUpload.npmAuditNpmUnavailable(relativeRoot));
61
+ continue;
62
+ }
63
+ if (summary) {
64
+ uiLogger.warn(lib.projectUpload.handleProjectUpload.npmAuditIssues(relativeRoot, summary));
65
+ continue;
66
+ }
67
+ if (result.exitCode !== 0) {
68
+ uiLogger.warn(lib.projectUpload.handleProjectUpload.npmAuditNonZeroExit(relativeRoot, result.exitCode));
69
+ continue;
70
+ }
71
+ uiLogger.success(lib.projectUpload.handleProjectUpload.npmAuditClean(relativeRoot));
72
+ }
73
+ }
@@ -16,9 +16,10 @@ type HandleProjectUploadArg<T> = {
16
16
  isUploadCommand?: boolean;
17
17
  sendIR?: boolean;
18
18
  skipValidation?: boolean;
19
+ skipNpmAudit?: boolean;
19
20
  profile?: string;
20
21
  };
21
- export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
22
+ export declare function handleProjectUpload<T>({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage, forceCreate, isUploadCommand, sendIR, skipValidation, skipNpmAudit, }: HandleProjectUploadArg<T>): Promise<ProjectUploadResult<T>>;
22
23
  export declare function validateSourceDirectory(srcDir: string, projectConfig: ProjectConfig, projectDir: string): Promise<void>;
23
24
  export declare function validateNoHSMetaMismatch(srcDir: string, projectConfig: ProjectConfig): Promise<void>;
24
25
  type HandleTranslateArg = {
@@ -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 { runNpmAuditsBeforeProjectUpload } from './npmAuditOnUpload.js';
21
22
  async function uploadProjectFiles(accountId, projectName, filePath, uploadMessage, platformVersion, intermediateRepresentation) {
22
23
  const accountIdentifier = uiAccountDescription(accountId) || `${accountId}`;
23
24
  SpinniesManager.add('upload', {
@@ -44,7 +45,7 @@ async function uploadProjectFiles(accountId, projectName, filePath, uploadMessag
44
45
  }
45
46
  return { buildId, error };
46
47
  }
47
- export async function handleProjectUpload({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage = '', forceCreate = false, isUploadCommand = false, sendIR = false, skipValidation = false, }) {
48
+ export async function handleProjectUpload({ accountId, projectConfig, projectDir, callbackFunc, profile, uploadMessage = '', forceCreate = false, isUploadCommand = false, sendIR = false, skipValidation = false, skipNpmAudit = false, }) {
48
49
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
49
50
  await validateSourceDirectory(srcDir, projectConfig, projectDir);
50
51
  await validateNoHSMetaMismatch(srcDir, projectConfig);
@@ -54,11 +55,20 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
54
55
  // Versions <= 2025.1 do not support the new npm workspaces bundling behavior.
55
56
  let workspaceMappings = [];
56
57
  let fileDependencyMappings = [];
58
+ let parsedPackageJsons = [];
57
59
  if (!isLegacyProject(projectConfig.platformVersion)) {
58
- const parsedPackageJsons = await findAndParsePackageJsonFiles(srcDir);
60
+ parsedPackageJsons = await findAndParsePackageJsonFiles(srcDir);
59
61
  workspaceMappings = await collectWorkspaceDirectories(parsedPackageJsons);
60
62
  fileDependencyMappings = await collectFileDependencies(parsedPackageJsons);
61
63
  }
64
+ if (isUploadCommand && !skipNpmAudit) {
65
+ await runNpmAuditsBeforeProjectUpload({
66
+ srcDir,
67
+ projectDir,
68
+ parsedPackageJsons,
69
+ isLegacyPlatform: isLegacyProject(projectConfig.platformVersion),
70
+ });
71
+ }
62
72
  const output = fs.createWriteStream(tempFile.name);
63
73
  const archive = archiver('zip');
64
74
  const result = new Promise((resolve, reject) => output.on('close', async function () {
@@ -123,7 +133,7 @@ export async function handleProjectUpload({ accountId, projectConfig, projectDir
123
133
  return ignored ? false : file;
124
134
  });
125
135
  // Archive workspaces and file: dependencies
126
- await archiveWorkspacesAndDependencies(archive, srcDir, workspaceMappings, fileDependencyMappings);
136
+ await archiveWorkspacesAndDependencies(archive, srcDir, projectDir, workspaceMappings, fileDependencyMappings);
127
137
  archive.finalize();
128
138
  return result;
129
139
  }
@@ -12,16 +12,6 @@ export type WorkspaceArchiveResult = {
12
12
  * Uses SHA256 truncated to 8 hex characters (4 billion possibilities).
13
13
  */
14
14
  export declare function shortHash(input: string): string;
15
- /**
16
- * Converts native path separators to POSIX forward slashes.
17
- *
18
- * Zip entry names and npm workspace globs are POSIX-only. On Windows,
19
- * `path.relative` returns backslash-separated paths; archiver normalizes
20
- * its appended entry names to forward slashes but its filter callback
21
- * receives forward-slashed names too. Without this normalization, lookups
22
- * in our exclusion Sets miss on Windows and a file gets archived twice.
23
- */
24
- export declare function toPosixPath(p: string): string;
25
15
  /**
26
16
  * Determines the archive path for an external workspace or file: dependency.
27
17
  * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
@@ -49,4 +39,4 @@ export declare function getLockfilePathsToUpdate(srcDir: string, workspaceMappin
49
39
  * Main orchestration function that handles archiving of workspaces and file dependencies.
50
40
  * This is the clean integration point for upload.ts.
51
41
  */
52
- export declare function archiveWorkspacesAndDependencies(archive: archiver.Archiver, srcDir: string, workspaceMappings: WorkspaceMapping[], fileDependencyMappings: FileDependencyMapping[]): Promise<WorkspaceArchiveResult>;
42
+ export declare function archiveWorkspacesAndDependencies(archive: archiver.Archiver, srcDir: string, projectDir: string, workspaceMappings: WorkspaceMapping[], fileDependencyMappings: FileDependencyMapping[]): Promise<WorkspaceArchiveResult>;
@@ -12,21 +12,6 @@ import { lib } from '../../lang/en.js';
12
12
  export function shortHash(input) {
13
13
  return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
14
14
  }
15
- /**
16
- * Converts native path separators to POSIX forward slashes.
17
- *
18
- * Zip entry names and npm workspace globs are POSIX-only. On Windows,
19
- * `path.relative` returns backslash-separated paths; archiver normalizes
20
- * its appended entry names to forward slashes but its filter callback
21
- * receives forward-slashed names too. Without this normalization, lookups
22
- * in our exclusion Sets miss on Windows and a file gets archived twice.
23
- */
24
- export function toPosixPath(p) {
25
- if (path.sep === path.posix.sep) {
26
- return p;
27
- }
28
- return p.replaceAll(path.sep, path.posix.sep);
29
- }
30
15
  /**
31
16
  * Determines the archive path for an external workspace or file: dependency.
32
17
  * Produces `_workspaces/<basename>-<hash>` with no subdirectory.
@@ -35,7 +20,7 @@ export function toPosixPath(p) {
35
20
  export function computeExternalArchivePath(absolutePath) {
36
21
  const resolved = path.resolve(absolutePath);
37
22
  const name = path.basename(resolved);
38
- return path.posix.join('_workspaces', `${name}-${shortHash(resolved)}`);
23
+ return path.join('_workspaces', `${name}-${shortHash(resolved)}`);
39
24
  }
40
25
  /**
41
26
  * Returns true if dir is inside srcDir (i.e. it will already be included
@@ -86,7 +71,7 @@ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
86
71
  if (isInsideSrcDir(workspaceDir, srcDir)) {
87
72
  // Internal: already in archive from srcDir walk.
88
73
  // Store the relative path from the package.json directory so npm can resolve it.
89
- const relPath = toPosixPath(path.relative(path.dirname(sourcePackageJsonPath), path.resolve(workspaceDir)));
74
+ const relPath = path.relative(path.dirname(sourcePackageJsonPath), path.resolve(workspaceDir));
90
75
  packageWorkspaceEntries.get(sourcePackageJsonPath).push(relPath);
91
76
  }
92
77
  else {
@@ -104,7 +89,7 @@ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
104
89
  externalsToArchive.push({ dir: workspaceDir, archivePath });
105
90
  }
106
91
  const relPkgJsonDir = path.relative(srcDir, path.dirname(sourcePackageJsonPath));
107
- const relativeEntry = toPosixPath(path.relative(relPkgJsonDir, archivePath));
92
+ const relativeEntry = path.relative(relPkgJsonDir, archivePath);
108
93
  packageWorkspaceEntries.get(sourcePackageJsonPath).push(relativeEntry);
109
94
  }
110
95
  }
@@ -145,7 +130,7 @@ async function archiveFileDependencies(archive, srcDir, fileDependencyMappings,
145
130
  packageFileDeps.set(sourcePackageJsonPath, new Map());
146
131
  }
147
132
  const relPkgJsonDir = path.relative(srcDir, path.dirname(sourcePackageJsonPath));
148
- const relativeArchivePath = toPosixPath(path.relative(relPkgJsonDir, archivePath));
133
+ const relativeArchivePath = path.relative(relPkgJsonDir, archivePath);
149
134
  packageFileDeps
150
135
  .get(sourcePackageJsonPath)
151
136
  .set(packageName, relativeArchivePath);
@@ -187,7 +172,7 @@ export async function updatePackageJsonInArchive(archive, srcDir, packageWorkspa
187
172
  if (!fs.existsSync(packageJsonPath)) {
188
173
  continue;
189
174
  }
190
- const relativePackageJsonPath = toPosixPath(path.relative(srcDir, packageJsonPath));
175
+ const relativePackageJsonPath = path.relative(srcDir, packageJsonPath);
191
176
  let rawContent;
192
177
  try {
193
178
  rawContent = fs.readFileSync(packageJsonPath, 'utf8');
@@ -276,11 +261,11 @@ export function rewriteLockfileForExternalDeps(lockfileContent, pathMappings) {
276
261
  export function getPackageJsonPathsToUpdate(srcDir, workspaceMappings, fileDependencyMappings) {
277
262
  const paths = new Set();
278
263
  for (const { sourcePackageJsonPath } of workspaceMappings) {
279
- paths.add(toPosixPath(path.relative(srcDir, sourcePackageJsonPath)));
264
+ paths.add(path.relative(srcDir, sourcePackageJsonPath));
280
265
  }
281
266
  for (const { localPath, sourcePackageJsonPath } of fileDependencyMappings) {
282
267
  if (!isInsideSrcDir(localPath, srcDir)) {
283
- paths.add(toPosixPath(path.relative(srcDir, sourcePackageJsonPath)));
268
+ paths.add(path.relative(srcDir, sourcePackageJsonPath));
284
269
  }
285
270
  }
286
271
  return paths;
@@ -305,7 +290,7 @@ export function getLockfilePathsToUpdate(srcDir, workspaceMappings, fileDependen
305
290
  for (const dir of dirsWithExternalDeps) {
306
291
  const lockfilePath = path.join(dir, 'package-lock.json');
307
292
  if (fs.existsSync(lockfilePath)) {
308
- paths.add(toPosixPath(path.relative(srcDir, lockfilePath)));
293
+ paths.add(path.relative(srcDir, lockfilePath));
309
294
  }
310
295
  }
311
296
  return paths;
@@ -334,12 +319,12 @@ async function rewriteLockfilesInArchive(archive, srcDir, externalArchivePaths,
334
319
  const pathMappings = [];
335
320
  for (const [absoluteExternalPath, archivePath] of externalArchivePaths) {
336
321
  pathMappings.push({
337
- oldPath: toPosixPath(path.relative(dir, absoluteExternalPath)),
338
- newPath: toPosixPath(path.relative(dir, path.join(srcDir, archivePath))),
322
+ oldPath: path.relative(dir, absoluteExternalPath),
323
+ newPath: path.relative(dir, path.join(srcDir, archivePath)),
339
324
  });
340
325
  }
341
326
  const rewritten = rewriteLockfileForExternalDeps(lockfileContent, pathMappings);
342
- const relativeLockfilePath = toPosixPath(path.relative(srcDir, lockfilePath));
327
+ const relativeLockfilePath = path.relative(srcDir, lockfilePath);
343
328
  uiLogger.debug(lib.projectUpload.handleProjectUpload.updatingLockfile(relativeLockfilePath));
344
329
  archive.append(JSON.stringify(rewritten, null, 2), {
345
330
  name: relativeLockfilePath,
@@ -351,7 +336,7 @@ async function rewriteLockfilesInArchive(archive, srcDir, externalArchivePaths,
351
336
  * Main orchestration function that handles archiving of workspaces and file dependencies.
352
337
  * This is the clean integration point for upload.ts.
353
338
  */
354
- export async function archiveWorkspacesAndDependencies(archive, srcDir, workspaceMappings, fileDependencyMappings) {
339
+ export async function archiveWorkspacesAndDependencies(archive, srcDir, projectDir, workspaceMappings, fileDependencyMappings) {
355
340
  // Archive workspace directories (internal ones are skipped, externals are copied)
356
341
  const { externalArchivePaths, packageWorkspaceEntries } = await archiveWorkspaceDirectories(archive, srcDir, workspaceMappings);
357
342
  // Archive external file: dependencies (internals are skipped)
@@ -1,29 +1,19 @@
1
- export type UsageTrackingMeta = {
2
- action?: string;
3
- os?: string;
4
- nodeVersion?: string;
5
- nodeMajorVersion?: string;
6
- version?: string;
7
- command?: string;
8
- authType?: string;
9
- step?: string;
10
- assetType?: string;
11
- mode?: string;
12
- type?: string | number;
13
- file?: boolean;
14
- successful?: boolean;
15
- };
1
+ import { type UsageTrackingMeta, type ConfigType, type ExecutionSource } from './api/usageTracking.js';
2
+ export type { UsageTrackingMeta, ConfigType, ExecutionSource, } from './api/usageTracking.js';
16
3
  export declare const EventClass: {
17
4
  USAGE: string;
18
5
  INTERACTION: string;
19
6
  VIEW: string;
20
7
  ACTIVATION: string;
21
8
  };
22
- export declare function getNodeVersionData(): {
9
+ export declare function getExecutionEnvironmentMeta(): {
10
+ os: string;
23
11
  nodeVersion: string;
24
12
  nodeMajorVersion: string;
13
+ version: string;
14
+ configType?: ConfigType;
15
+ executionSource: ExecutionSource;
25
16
  };
26
- export declare function getPlatform(): string;
27
17
  export declare function trackCommandUsage(command: string, meta?: UsageTrackingMeta, accountId?: number): Promise<void>;
28
18
  export declare function trackHelpUsage(command: string): Promise<void>;
29
19
  export declare function trackConvertFieldsUsage(command: string): Promise<void>;
@@ -1,10 +1,10 @@
1
- import { trackUsage } from '@hubspot/local-dev-lib/trackUsage';
2
- import { getConfigAccountById, getConfig } from '@hubspot/local-dev-lib/config';
1
+ import { getConfig, getConfigAccountById, getConfigFilePath, getGlobalConfigFilePath, } from '@hubspot/local-dev-lib/config';
3
2
  import { API_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
4
3
  import { uiLogger } from './ui/logger.js';
5
4
  import { pkg } from './jsonLoader.js';
6
5
  import { debugError } from './errorHandlers/index.js';
7
6
  import { isUsageTrackingDisableFlagSet } from './middleware/usageTrackingMiddleware.js';
7
+ import { sendUsageEvent, } from './api/usageTracking.js';
8
8
  const version = pkg.version;
9
9
  const usageTrackingDiabled = 'Usage tracking is disabled via the --disable-usage-tracking flag, not sending usage events';
10
10
  export const EventClass = {
@@ -13,13 +13,13 @@ export const EventClass = {
13
13
  VIEW: 'VIEW',
14
14
  ACTIVATION: 'ACTIVATION',
15
15
  };
16
- export function getNodeVersionData() {
16
+ function getNodeVersionData() {
17
17
  return {
18
18
  nodeVersion: process.version,
19
19
  nodeMajorVersion: (process.version || '').split('.')[0],
20
20
  };
21
21
  }
22
- export function getPlatform() {
22
+ function getPlatform() {
23
23
  switch (process.platform) {
24
24
  case 'darwin':
25
25
  return 'macos';
@@ -29,6 +29,34 @@ export function getPlatform() {
29
29
  return process.platform;
30
30
  }
31
31
  }
32
+ function getConfigType() {
33
+ try {
34
+ return getConfigFilePath() === getGlobalConfigFilePath()
35
+ ? 'global'
36
+ : 'local';
37
+ }
38
+ catch (_e) {
39
+ return undefined;
40
+ }
41
+ }
42
+ function getExecutionSource() {
43
+ if (process.env.HUBSPOT_MCP_AI_AGENT) {
44
+ return 'mcp';
45
+ }
46
+ if (process.env.CI) {
47
+ return 'ci';
48
+ }
49
+ return 'user';
50
+ }
51
+ export function getExecutionEnvironmentMeta() {
52
+ return {
53
+ os: getPlatform(),
54
+ ...getNodeVersionData(),
55
+ version,
56
+ configType: getConfigType(),
57
+ executionSource: getExecutionSource(),
58
+ };
59
+ }
32
60
  export async function trackCommandUsage(command, meta = {}, accountId) {
33
61
  if (isUsageTrackingDisableFlagSet()) {
34
62
  uiLogger.debug(usageTrackingDiabled);
@@ -130,39 +158,25 @@ async function trackCliInteraction({ action, accountId, command, authType, meta
130
158
  if (config?.allowUsageTracking === false) {
131
159
  return;
132
160
  }
161
+ if (isUsageTrackingDisableFlagSet()) {
162
+ uiLogger.debug(usageTrackingDiabled);
163
+ return;
164
+ }
133
165
  const usageTrackingEvent = {
134
166
  action,
135
- os: getPlatform(),
136
- ...getNodeVersionData(),
137
- version,
138
167
  command,
139
168
  authType,
169
+ ...getExecutionEnvironmentMeta(),
140
170
  ...meta,
141
171
  };
142
- if (process.env.HUBSPOT_MCP_AI_AGENT) {
143
- try {
144
- await trackUsage('cli-interaction', EventClass.INTERACTION, {
145
- ...usageTrackingEvent,
146
- action: 'cli-mcp-server',
147
- type: process.env.HUBSPOT_MCP_AI_AGENT,
148
- }, accountId);
149
- uiLogger.debug('Sent AI usage tracking command event:', {
150
- ...usageTrackingEvent,
151
- action: 'cli-mcp-server',
152
- type: process.env.HUBSPOT_MCP_AI_AGENT,
153
- });
154
- }
155
- catch (error) {
156
- debugError(error);
157
- }
158
- }
159
- if (isUsageTrackingDisableFlagSet()) {
160
- uiLogger.debug(usageTrackingDiabled);
161
- return;
162
- }
163
172
  try {
164
173
  uiLogger.debug('Sent usage tracking command event:', usageTrackingEvent);
165
- await trackUsage('cli-interaction', EventClass.INTERACTION, usageTrackingEvent, accountId);
174
+ await sendUsageEvent({
175
+ eventName: 'cli-interaction',
176
+ eventClass: EventClass.INTERACTION,
177
+ meta: usageTrackingEvent,
178
+ accountId,
179
+ });
166
180
  }
167
181
  catch (error) {
168
182
  debugError(error);
@@ -1,6 +1,9 @@
1
+ import os from 'os';
2
+ import path from 'path';
1
3
  import { getConfig } from '@hubspot/local-dev-lib/config';
2
4
  import { getStateValue, setStateValue, } from '@hubspot/local-dev-lib/config/state';
3
5
  import { STATE_FLAGS } from '@hubspot/local-dev-lib/constants/config';
6
+ import { logger as ldlLogger } from '@hubspot/local-dev-lib/logger';
4
7
  import { trackCommandUsage as _trackCommandUsage } from '../usageTracking.js';
5
8
  import { pkg } from '../jsonLoader.js';
6
9
  import { uiLogger } from '../ui/logger.js';
@@ -8,6 +11,7 @@ import { lib } from '../../lang/en.js';
8
11
  import { EXIT_CODES } from '../enums/exitCodes.js';
9
12
  import { isPromptExitError } from '../errors/PromptExitError.js';
10
13
  import { debugError } from '../errorHandlers/index.js';
14
+ const HANDLER_LOG_DIR = path.join(os.homedir(), '.hscli', 'logs');
11
15
  function logUsageTrackingMessage(isJsonOutput) {
12
16
  if (isJsonOutput) {
13
17
  return;
@@ -30,6 +34,7 @@ function logUsageTrackingMessage(isJsonOutput) {
30
34
  }
31
35
  export function makeYargsHandlerWithUsageTracking(trackingName, handler) {
32
36
  return async (args) => {
37
+ const startTime = Date.now();
33
38
  const meta = {};
34
39
  let trackingFired = false;
35
40
  const trackingArgs = args;
@@ -45,6 +50,7 @@ export function makeYargsHandlerWithUsageTracking(trackingName, handler) {
45
50
  try {
46
51
  const { accountId: overrideAccountId, ...trackingMeta } = meta;
47
52
  trackingMeta.successful = successful;
53
+ trackingMeta.executionTime = Date.now() - startTime;
48
54
  await _trackCommandUsage(trackingName, trackingMeta, overrideAccountId ?? args.derivedAccountId);
49
55
  }
50
56
  catch (_e) { }
@@ -68,12 +74,31 @@ export function makeYargsHandlerWithUsageTracking(trackingName, handler) {
68
74
  process.removeListener('SIGINT', onSigint);
69
75
  process.removeListener('SIGINT', onForcedExit);
70
76
  };
77
+ const jsonArgs = args;
78
+ const isJsonOutput = Boolean(jsonArgs.json || jsonArgs.formatOutputAsJson);
79
+ const writeFailureLogFile = () => {
80
+ // Skip in JSON output modes so the side effect + stderr message don't
81
+ // interfere with structured output consumers.
82
+ if (isJsonOutput) {
83
+ return;
84
+ }
85
+ const savedPath = ldlLogger.writeBufferedLogsToFile({
86
+ dir: HANDLER_LOG_DIR,
87
+ filenamePrefix: trackingName,
88
+ });
89
+ if (savedPath) {
90
+ uiLogger.log('');
91
+ uiLogger.error(lib.handlerLogFile.saved(savedPath));
92
+ }
93
+ };
71
94
  trackingArgs.exit = async (code) => {
72
95
  await trackCommandUsageAndRemoveListeners(code !== EXIT_CODES.ERROR);
96
+ if (code === EXIT_CODES.ERROR) {
97
+ writeFailureLogFile();
98
+ }
73
99
  return process.exit(code);
74
100
  };
75
- const jsonArgs = args;
76
- logUsageTrackingMessage(Boolean(jsonArgs.json || jsonArgs.formatOutputAsJson));
101
+ logUsageTrackingMessage(isJsonOutput);
77
102
  try {
78
103
  await handler(trackingArgs);
79
104
  }
@@ -87,6 +112,7 @@ export function makeYargsHandlerWithUsageTracking(trackingName, handler) {
87
112
  }
88
113
  else {
89
114
  debugError(e);
115
+ writeFailureLogFile();
90
116
  return process.exit(EXIT_CODES.ERROR);
91
117
  }
92
118
  }
@@ -1,6 +1,6 @@
1
- import { trackUsage } from '@hubspot/local-dev-lib/trackUsage';
2
- import { EventClass, getNodeVersionData, getPlatform, } from '../../lib/usageTracking.js';
1
+ import { EventClass, getExecutionEnvironmentMeta, } from '../../lib/usageTracking.js';
3
2
  import { getConfig, getConfigDefaultAccountIfExists, } from '@hubspot/local-dev-lib/config';
3
+ import { sendUsageEvent } from '../../lib/api/usageTracking.js';
4
4
  export async function trackToolUsage(toolName, meta) {
5
5
  const config = getConfig();
6
6
  if (config?.allowUsageTracking === false) {
@@ -8,15 +8,19 @@ export async function trackToolUsage(toolName, meta) {
8
8
  }
9
9
  const usageTrackingEvent = {
10
10
  action: 'cli-mcp-tool-invocation',
11
- os: getPlatform(),
12
- ...getNodeVersionData(),
13
11
  command: toolName,
14
12
  type: process.env.HUBSPOT_MCP_AI_AGENT,
13
+ ...getExecutionEnvironmentMeta(),
15
14
  ...meta,
16
15
  };
17
16
  const accountId = getConfigDefaultAccountIfExists()?.accountId || undefined;
18
17
  try {
19
- await trackUsage('cli-interaction', EventClass.INTERACTION, usageTrackingEvent, accountId);
18
+ await sendUsageEvent({
19
+ eventName: 'cli-interaction',
20
+ eventClass: EventClass.INTERACTION,
21
+ meta: usageTrackingEvent,
22
+ accountId,
23
+ });
20
24
  }
21
- catch (error) { }
25
+ catch (_error) { }
22
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.1.0-experimental.0",
3
+ "version": "8.1.1-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,10 +10,10 @@
10
10
  "!**/__tests__/**"
11
11
  ],
12
12
  "dependencies": {
13
- "@hubspot/local-dev-lib": "5.6.0",
13
+ "@hubspot/local-dev-lib": "5.7.0",
14
14
  "@hubspot/project-parsing-lib": "0.16.0",
15
15
  "@hubspot/serverless-dev-runtime": "7.0.7",
16
- "@hubspot/ui-extensions-dev-server": "2.0.5",
16
+ "@hubspot/ui-extensions-dev-server": "2.0.7",
17
17
  "@inquirer/prompts": "7.1.0",
18
18
  "@modelcontextprotocol/sdk": "1.29.0",
19
19
  "archiver": "7.0.1",