@hubspot/cli 8.13.0 → 8.14.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.
Files changed (35) hide show
  1. package/commands/cms/function/server.js +14 -7
  2. package/commands/project/dev/index.js +5 -0
  3. package/commands/project/dev/unifiedFlow.js +8 -0
  4. package/commands/project/listBuilds.d.ts +3 -2
  5. package/commands/project/listBuilds.js +37 -6
  6. package/commands/project/upload.js +5 -1
  7. package/commands/project/watch.js +3 -2
  8. package/lang/en.d.ts +17 -0
  9. package/lang/en.js +21 -2
  10. package/lib/cms/serverlessDevRuntime.d.ts +1 -0
  11. package/lib/cms/serverlessDevRuntime.js +72 -0
  12. package/lib/constants.d.ts +3 -0
  13. package/lib/constants.js +3 -0
  14. package/lib/jsonOutput.d.ts +73 -0
  15. package/lib/jsonOutput.js +64 -0
  16. package/lib/projects/create/v2.js +1 -0
  17. package/lib/projects/localDev/LocalDevLogger.d.ts +4 -0
  18. package/lib/projects/localDev/LocalDevLogger.js +18 -0
  19. package/lib/projects/localDev/LocalDevProcess.d.ts +11 -1
  20. package/lib/projects/localDev/LocalDevProcess.js +87 -5
  21. package/lib/projects/localDev/LocalDevState.d.ts +7 -1
  22. package/lib/projects/localDev/LocalDevState.js +19 -1
  23. package/lib/projects/localDev/LocalDevWatcher.js +1 -1
  24. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  25. package/lib/projects/localDev/LocalDevWebsocketServer.js +23 -1
  26. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +2 -1
  27. package/lib/projects/localDev/localDevWebsocketServerUtils.js +3 -0
  28. package/lib/projects/release.js +1 -1
  29. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -0
  30. package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -0
  31. package/mcp-server/tools/project/constants.d.ts +1 -0
  32. package/mcp-server/tools/project/constants.js +2 -1
  33. package/package.json +12 -11
  34. package/types/LocalDev.d.ts +7 -0
  35. package/types/Yargs.d.ts +1 -0
@@ -1,18 +1,25 @@
1
1
  import { uiLogger } from '../../../lib/ui/logger.js';
2
- // This package is not typed, so we need to use require
3
- import { start as startTestServer } from '@hubspot/serverless-dev-runtime';
4
2
  import { commands } from '../../../lang/en.js';
3
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
4
+ import { startServerlessDevRuntime } from '../../../lib/cms/serverlessDevRuntime.js';
5
+ import { logError } from '../../../lib/errorHandlers/index.js';
5
6
  import { makeWrappedYargsHandler } from '../../../lib/yargs/makeWrappedYargsHandler.js';
6
7
  import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
7
8
  const command = 'server <path>';
8
9
  const describe = undefined;
9
10
  async function handler(args) {
10
- const { path: functionPath, derivedAccountId } = args;
11
+ const { path: functionPath, derivedAccountId, exit } = args;
11
12
  uiLogger.debug(commands.cms.subcommands.function.subcommands.server.debug.startingServer(functionPath));
12
- startTestServer({
13
- accountId: derivedAccountId,
14
- ...args,
15
- });
13
+ try {
14
+ await startServerlessDevRuntime({
15
+ accountId: derivedAccountId,
16
+ ...args,
17
+ });
18
+ }
19
+ catch (e) {
20
+ logError(e);
21
+ return exit(EXIT_CODES.ERROR);
22
+ }
16
23
  }
17
24
  function functionServerBuilder(yargs) {
18
25
  yargs.positional('path', {
@@ -110,6 +110,11 @@ function projectDevBuilder(yargs) {
110
110
  description: commands.project.dev.options.port,
111
111
  default: LOCAL_DEV_DEFAULT_PORT,
112
112
  });
113
+ yargs.option('auto-upload', {
114
+ type: 'boolean',
115
+ description: commands.project.dev.options.autoUpload,
116
+ default: false,
117
+ });
113
118
  yargs.example([['$0 project dev', commands.project.dev.examples.default]]);
114
119
  yargs.conflicts('profile', 'testing-account');
115
120
  yargs.conflicts('profile', 'project-account');
@@ -155,6 +155,13 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
155
155
  noLogs: true,
156
156
  });
157
157
  let project = uploadedProject;
158
+ // Fail fast before the slower project checks below.
159
+ const autoDeployEnabled = (project?.deployedBuild ?? project?.latestBuild)?.isAutoDeployEnabled ??
160
+ true;
161
+ if (args.autoUpload && !autoDeployEnabled) {
162
+ uiLogger.error(commands.project.dev.errors.autoUploadRequiresAutoDeploy);
163
+ return exit(EXIT_CODES.ERROR);
164
+ }
158
165
  if (projectExists && project) {
159
166
  await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes, exit, args.profile);
160
167
  }
@@ -189,6 +196,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
189
196
  projectData: project,
190
197
  env,
191
198
  actions: { exit },
199
+ autoUploadEnabled: Boolean(args.autoUpload) && autoDeployEnabled,
192
200
  });
193
201
  const websocketServer = new LocalDevWebsocketServer(localDevProcess, args.debug);
194
202
  const watcher = new LocalDevWatcher(localDevProcess);
@@ -1,5 +1,6 @@
1
- import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- export type ProjectListBuildsArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & {
1
+ import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
+ import { ProjectBuildsListJsonOutput } from '../../lib/jsonOutput.js';
3
+ export type ProjectListBuildsArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & JSONOutputArgs<ProjectBuildsListJsonOutput> & {
3
4
  project?: string;
4
5
  limit?: number;
5
6
  };
@@ -5,6 +5,7 @@ import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/con
5
5
  import { getProjectDetailUrl } from '../../lib/projects/urls.js';
6
6
  import moment from 'moment';
7
7
  import { promptUser } from '../../lib/prompts/promptUtils.js';
8
+ import { isPromptExitError } from '../../lib/errors/PromptExitError.js';
8
9
  import { uiLogger } from '../../lib/ui/logger.js';
9
10
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
10
11
  import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler.js';
@@ -12,6 +13,7 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
12
13
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
13
14
  import { commands } from '../../lang/en.js';
14
15
  import { renderTable } from '../../ui/render.js';
16
+ import { ProjectBuildsListSchema, mapBuildToJsonOutput, } from '../../lib/jsonOutput.js';
15
17
  const command = 'list-builds';
16
18
  const describe = commands.project.listBuilds.describe;
17
19
  async function fetchAndDisplayBuilds(accountId, project, options) {
@@ -40,7 +42,7 @@ async function fetchAndDisplayBuilds(accountId, project, options) {
40
42
  });
41
43
  renderTable(['Build ID', 'Status', 'Completed', 'Duration', 'Details'], builds);
42
44
  }
43
- if (options && options.after) {
45
+ if (options.after) {
44
46
  if (results.length > 0) {
45
47
  uiLogger.log(commands.project.listBuilds.showingNextBuilds(results.length, project.name));
46
48
  }
@@ -48,7 +50,8 @@ async function fetchAndDisplayBuilds(accountId, project, options) {
48
50
  else {
49
51
  uiLogger.log(commands.project.listBuilds.showingRecentBuilds(results.length, project.name, uiLink(commands.project.listBuilds.viewAllBuildsLink, getProjectDetailUrl(project.name, accountId))));
50
52
  }
51
- if (paging && paging.next) {
53
+ const canPromptForMore = options.limit === undefined && Boolean(process.stdin.isTTY);
54
+ if (paging?.next?.after && canPromptForMore) {
52
55
  await promptUser({
53
56
  name: 'more',
54
57
  message: commands.project.listBuilds.continueOrExitPrompt,
@@ -60,7 +63,7 @@ async function fetchAndDisplayBuilds(accountId, project, options) {
60
63
  }
61
64
  }
62
65
  async function handler(args) {
63
- const { project: projectFlagValue, limit, derivedAccountId, exit } = args;
66
+ const { project: projectFlagValue, limit, derivedAccountId, exit, json: formatOutputAsJson, addJsonOutput, } = args;
64
67
  let projectName = projectFlagValue;
65
68
  if (!projectName) {
66
69
  const { projectConfig, projectDir } = await getProjectConfig();
@@ -78,9 +81,25 @@ async function handler(args) {
78
81
  }
79
82
  try {
80
83
  const { data: project } = await fetchProject(derivedAccountId, projectName);
81
- await fetchAndDisplayBuilds(derivedAccountId, project, { limit });
84
+ if (formatOutputAsJson) {
85
+ const { data: { results, paging }, } = await fetchProjectBuilds(derivedAccountId, project.name, { limit });
86
+ addJsonOutput({
87
+ projectName: project.name,
88
+ deployedBuildId: project.deployedBuildId,
89
+ results: results.map(build => mapBuildToJsonOutput(build, project.deployedBuildId)),
90
+ paging: paging?.next?.after
91
+ ? { next: { after: paging.next.after } }
92
+ : undefined,
93
+ });
94
+ }
95
+ else {
96
+ await fetchAndDisplayBuilds(derivedAccountId, project, { limit });
97
+ }
82
98
  }
83
99
  catch (e) {
100
+ if (isPromptExitError(e)) {
101
+ return exit(e.exitCode);
102
+ }
84
103
  if (isHubSpotHttpError(e) && e.status === 404) {
85
104
  uiLogger.error(commands.project.listBuilds.errors.projectNotFound(projectName));
86
105
  }
@@ -90,6 +109,7 @@ async function handler(args) {
90
109
  projectName,
91
110
  }));
92
111
  }
112
+ return exit(EXIT_CODES.ERROR);
93
113
  }
94
114
  return exit(EXIT_CODES.SUCCESS);
95
115
  }
@@ -101,11 +121,19 @@ function projectListBuildsBuilder(yargs) {
101
121
  },
102
122
  limit: {
103
123
  describe: commands.project.listBuilds.options.limit.describe,
104
- type: 'string',
124
+ type: 'number',
105
125
  },
106
126
  });
107
127
  yargs.example([
108
128
  ['$0 project list-builds', commands.project.listBuilds.examples.default],
129
+ [
130
+ '$0 project list-builds --limit=5',
131
+ commands.project.listBuilds.examples.withLimit,
132
+ ],
133
+ [
134
+ '$0 project list-builds --json',
135
+ commands.project.listBuilds.examples.json,
136
+ ],
109
137
  ]);
110
138
  return yargs;
111
139
  }
@@ -114,11 +142,14 @@ const builder = makeYargsBuilder(projectListBuildsBuilder, command, describe, {
114
142
  useConfigOptions: true,
115
143
  useAccountOptions: true,
116
144
  useEnvironmentOptions: true,
145
+ useJSONOutputOptions: true,
117
146
  });
118
147
  const projectListBuildsCommand = {
119
148
  command,
120
149
  describe,
121
- handler: makeWrappedYargsHandler('project-list-builds', handler),
150
+ handler: makeWrappedYargsHandler('project-list-builds', handler, {
151
+ jsonOutputSchema: ProjectBuildsListSchema,
152
+ }),
122
153
  builder,
123
154
  };
124
155
  export default projectListBuildsCommand;
@@ -17,6 +17,7 @@ import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler
17
17
  import { UploadSchema, } from '../../lib/jsonOutput.js';
18
18
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
19
19
  import { projectProfilePrompt } from '../../lib/prompts/projectProfilePrompt.js';
20
+ import { uiDeprecatedTag } from '../../lib/ui/index.js';
20
21
  import { showMcpPromotionNudge } from '../../lib/mcp/promotion.js';
21
22
  import { PLATFORM_VERSIONS } from '@hubspot/project-parsing-lib/constants';
22
23
  const command = 'upload';
@@ -37,6 +38,9 @@ async function handlePreview(accountId, projectId, buildId, targetPortalId) {
37
38
  }
38
39
  async function handler(args) {
39
40
  const { force = false, forceCreate = false, message, derivedAccountId, skipValidation, skipNpmAudit, skipAutoDeploy, formatOutputAsJson, profile: profileOption, useEnv: useEnvOption, preview, target: targetPortalId, exit, addUsageMetadata, addJsonOutput, } = args;
41
+ if (forceCreate) {
42
+ uiDeprecatedTag(commands.project.upload.logs.forceCreateDeprecated);
43
+ }
40
44
  const { projectConfig, projectDir } = await getProjectConfig();
41
45
  try {
42
46
  validateProjectConfig(projectConfig, projectDir);
@@ -114,7 +118,7 @@ async function handler(args) {
114
118
  (!result.buildResult.isAutoDeployEnabled || preview || skipAutoDeploy)) {
115
119
  uiLogger.log(chalk.bold(commands.project.upload.logs.buildSucceeded(result.buildId)));
116
120
  if (!preview) {
117
- if (meetsMinimumPlatformVersion(result.buildResult.platformVersion, PLATFORM_VERSIONS.v2026_09_BETA)) {
121
+ if (meetsMinimumPlatformVersion(result.buildResult.platformVersion, PLATFORM_VERSIONS.v2027_03_BETA)) {
118
122
  const releaseCommand = `hs project release create --build=${result.buildId}`;
119
123
  uiLogger.log(commands.project.upload.logs.releaseManagementRequired(releaseCommand));
120
124
  }
@@ -14,9 +14,9 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
14
14
  import { handleKeypress, handleExit } from '../../lib/process.js';
15
15
  import { makeWrappedYargsHandler } from '../../lib/yargs/makeWrappedYargsHandler.js';
16
16
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
17
- import { uiDeprecatedTag } from '../../lib/ui/index.js';
17
+ import { uiCommandRelocatedMessage, uiCommandRenamedDescription, } from '../../lib/ui/index.js';
18
18
  const command = 'watch';
19
- const describe = uiDeprecatedTag(commands.project.watch.describe, false);
19
+ const describe = uiCommandRenamedDescription(commands.project.watch.describe, 'hs project dev');
20
20
  async function handleBuildStatus(accountId, projectName, buildId) {
21
21
  const { isAutoDeployEnabled, deployStatusTaskLocator } = await pollBuildStatus(accountId, projectName, buildId, null);
22
22
  if (isAutoDeployEnabled && deployStatusTaskLocator) {
@@ -57,6 +57,7 @@ function handleUserInput(accountId, projectName, currentBuildId, exit) {
57
57
  }
58
58
  async function handler(args) {
59
59
  const { initialUpload, derivedAccountId, exit } = args;
60
+ uiCommandRelocatedMessage('hs project dev');
60
61
  const { projectConfig, projectDir } = await getProjectConfig();
61
62
  if (!projectConfig || !projectDir) {
62
63
  uiLogger.error(commands.project.watch.errors.projectConfigNotFound);
package/lang/en.d.ts CHANGED
@@ -1529,6 +1529,7 @@ export declare const commands: {
1529
1529
  noRunnableComponents: string;
1530
1530
  accountNotCombined: string;
1531
1531
  localDevAlreadyRunning: string;
1532
+ autoUploadRequiresAutoDeploy: string;
1532
1533
  };
1533
1534
  examples: {
1534
1535
  default: string;
@@ -1541,6 +1542,7 @@ export declare const commands: {
1541
1542
  projectAccount: string;
1542
1543
  testingAccount: string;
1543
1544
  port: string;
1545
+ autoUpload: string;
1544
1546
  };
1545
1547
  };
1546
1548
  create: {
@@ -1842,6 +1844,8 @@ export declare const commands: {
1842
1844
  };
1843
1845
  examples: {
1844
1846
  default: string;
1847
+ withLimit: string;
1848
+ json: string;
1845
1849
  };
1846
1850
  };
1847
1851
  logs: {
@@ -1901,6 +1905,7 @@ export declare const commands: {
1901
1905
  withPreview: string;
1902
1906
  };
1903
1907
  logs: {
1908
+ forceCreateDeprecated: string;
1904
1909
  buildSucceeded: (buildId: number) => string;
1905
1910
  readyToGoLive: string;
1906
1911
  runCommand: (command: string) => string;
@@ -3375,6 +3380,7 @@ export declare const lib: {
3375
3380
  learnMoreLocalDevServer: string;
3376
3381
  running: (projectName: string, accountIdentifier: string) => string;
3377
3382
  quitHelper: string;
3383
+ autoUploadEnabled: string;
3378
3384
  viewProjectLink: (name: string, accountId: number) => string;
3379
3385
  viewLocalDevUILink: (accountId: number, showWelcomeScreen: boolean) => string;
3380
3386
  localDevUIAutoMessage: (accountId: number, showWelcomeScreen: boolean) => string;
@@ -3466,6 +3472,10 @@ export declare const lib: {
3466
3472
  LocalDevProcess: {
3467
3473
  projectConfigMismatch: string;
3468
3474
  uploadInitiated: string;
3475
+ autoUploadScheduled: (filePath: string) => string;
3476
+ autoUploadTriggered: string;
3477
+ autoUploadInProgress: string;
3478
+ autoUploadToggled: (enabled: boolean) => string;
3469
3479
  deployInitiated: string;
3470
3480
  uploadFailed: string;
3471
3481
  deployFailed: string;
@@ -4494,6 +4504,13 @@ export declare const lib: {
4494
4504
  copyingProjectFilesFailed: string;
4495
4505
  };
4496
4506
  };
4507
+ cms: {
4508
+ serverlessDevRuntime: {
4509
+ installStarted: (targetVersion: string) => string;
4510
+ installSucceeded: string;
4511
+ installFailed: string;
4512
+ };
4513
+ };
4497
4514
  theme: {
4498
4515
  cmsDevServerProcess: {
4499
4516
  installStarted: (targetVersion: string) => string;
package/lang/en.js CHANGED
@@ -1545,6 +1545,7 @@ export const commands = {
1545
1545
  noRunnableComponents: `No supported components were found in this project. Run ${uiCommandReference('hs project add')} to see a list of available components and add one to your project.`,
1546
1546
  accountNotCombined: `\nLocal development of unified apps is currently only compatible with accounts that are opted into the unified apps beta. Make sure that this account is opted in or switch accounts using ${uiCommandReference('hs account use')}.`,
1547
1547
  localDevAlreadyRunning: `Another ${uiCommandReference('hs project dev')} process is already running. To proceed with local development of this project, stop the existing process and re-run ${uiCommandReference('hs project dev')}.`,
1548
+ autoUploadRequiresAutoDeploy: `${uiCommandReference('hs project dev --auto-upload')} requires auto-deploy to be enabled for this project, since auto-upload deploys every change. Enable auto-deploy in your project settings, or run ${uiCommandReference('hs project dev')} without it.`,
1548
1549
  },
1549
1550
  examples: {
1550
1551
  default: 'Start local dev for the current project',
@@ -1557,6 +1558,7 @@ export const commands = {
1557
1558
  projectAccount: 'The id of the account to upload your project to. Must be used with --testing-account.',
1558
1559
  testingAccount: 'The id of the account to install apps and test on. Must be used with --project-account.',
1559
1560
  port: `The port for the local dev server. Defaults to ${LOCAL_DEV_DEFAULT_PORT}.`,
1561
+ autoUpload: 'Automatically upload local changes to HubSpot as you edit files, instead of uploading manually from the Local Dev Panel.',
1560
1562
  },
1561
1563
  },
1562
1564
  create: {
@@ -1858,6 +1860,8 @@ export const commands = {
1858
1860
  },
1859
1861
  examples: {
1860
1862
  default: 'List the builds for the current project',
1863
+ withLimit: 'List the five most recent builds for the current project',
1864
+ json: 'Output the builds for the current project as JSON',
1861
1865
  },
1862
1866
  },
1863
1867
  logs: {
@@ -1917,6 +1921,7 @@ export const commands = {
1917
1921
  withPreview: 'Upload and preview the build on a target portal',
1918
1922
  },
1919
1923
  logs: {
1924
+ forceCreateDeprecated: `The ${uiCommandReference('--force-create')} flag is deprecated. Use ${uiCommandReference('--force')} instead.`,
1920
1925
  buildSucceeded: (buildId) => `Build #${buildId} succeeded\n`,
1921
1926
  readyToGoLive: '🚀 Ready to take your project live?',
1922
1927
  runCommand: (command) => `Run \`${uiCommandReference(command)}\``,
@@ -3400,6 +3405,7 @@ export const lib = {
3400
3405
  learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands'),
3401
3406
  running: (projectName, accountIdentifier) => chalk.hex(UI_COLORS.SORBET)(`Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`),
3402
3407
  quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`,
3408
+ autoUploadEnabled: `${chalk.bold('Auto-upload is on.')} Changes you make locally will be uploaded to HubSpot automatically.`,
3403
3409
  viewProjectLink: (name, accountId) => uiLink('View project in HubSpot', getProjectDetailUrl(name, accountId) || ''),
3404
3410
  viewLocalDevUILink: (accountId, showWelcomeScreen) => uiLink('View local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen)),
3405
3411
  localDevUIAutoMessage: (accountId, showWelcomeScreen) => `Opening your ${uiLink('local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen))}...`,
@@ -3490,8 +3496,14 @@ export const lib = {
3490
3496
  },
3491
3497
  LocalDevProcess: {
3492
3498
  projectConfigMismatch: `Unable to upload project. The project config has been modified since starting ${uiCommandReference('hs project dev')}.`,
3493
- uploadInitiated: 'Project upload initiated from Local Dev UI.',
3494
- deployInitiated: 'Project deploy initiated from Local Dev UI.',
3499
+ uploadInitiated: 'Project upload initiated.',
3500
+ autoUploadScheduled: (filePath) => `Auto-upload: detected change to ${filePath}, upload scheduled.`,
3501
+ autoUploadTriggered: 'Auto-upload: debounce elapsed, uploading now.',
3502
+ autoUploadInProgress: 'Auto-upload: an upload is already running, will re-run after it finishes.',
3503
+ autoUploadToggled: (enabled) => enabled
3504
+ ? `${chalk.bold('Auto-upload turned on')} from the Local Dev Panel. Changes you make locally will be uploaded to HubSpot automatically.`
3505
+ : `${chalk.bold('Auto-upload turned off')} from the Local Dev Panel. Upload manually from the Local Dev Panel to push your changes.`,
3506
+ deployInitiated: 'Project deploy initiated from the Local Dev Panel.',
3495
3507
  uploadFailed: 'Project upload failed. To proceed with local development, fix any necessary errors, then re-upload your project.',
3496
3508
  deployFailed: 'Project deploy failed. To proceed with local development, fix any necessary errors, then re-deploy your project.',
3497
3509
  uploadSuccess: 'Project upload completed successfully. Resuming local dev...',
@@ -4531,6 +4543,13 @@ export const lib = {
4531
4543
  copyingProjectFilesFailed: 'Unable to copy migrated project files',
4532
4544
  },
4533
4545
  },
4546
+ cms: {
4547
+ serverlessDevRuntime: {
4548
+ installStarted: (targetVersion) => `Installing serverless-dev-runtime ${targetVersion}...`,
4549
+ installSucceeded: 'serverless-dev-runtime setup complete',
4550
+ installFailed: 'Failed to install serverless-dev-runtime',
4551
+ },
4552
+ },
4534
4553
  theme: {
4535
4554
  cmsDevServerProcess: {
4536
4555
  installStarted: (targetVersion) => `Installing cms-dev-server ${targetVersion}...`,
@@ -0,0 +1 @@
1
+ export declare function startServerlessDevRuntime(options: any): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { spawn } from 'child_process';
2
+ import { createRequire } from 'module';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import SpinniesManager from '../ui/SpinniesManager.js';
7
+ import { lib } from '../../lang/en.js';
8
+ const TARGET_SERVERLESS_RUNTIME_VERSION = '7.0.7';
9
+ const CACHE_DIR = path.join(os.homedir(), '.hscli', '.serverless-runtime-cache');
10
+ async function ensureServerlessRuntimeInstalled() {
11
+ const packageJsonPath = path.join(CACHE_DIR, 'node_modules', '@hubspot', 'serverless-dev-runtime', 'package.json');
12
+ let needsInstall = true;
13
+ if (fs.existsSync(packageJsonPath)) {
14
+ try {
15
+ const installed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
16
+ if (installed.version === TARGET_SERVERLESS_RUNTIME_VERSION) {
17
+ needsInstall = false;
18
+ }
19
+ }
20
+ catch {
21
+ needsInstall = true;
22
+ }
23
+ }
24
+ if (!needsInstall) {
25
+ return;
26
+ }
27
+ SpinniesManager.init({ succeedColor: 'white' });
28
+ SpinniesManager.add('serverless-runtime-install', {
29
+ text: lib.cms.serverlessDevRuntime.installStarted(TARGET_SERVERLESS_RUNTIME_VERSION),
30
+ });
31
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
32
+ const nodeModulesDir = path.join(CACHE_DIR, 'node_modules');
33
+ if (fs.existsSync(nodeModulesDir)) {
34
+ fs.rmSync(nodeModulesDir, { recursive: true, force: true });
35
+ }
36
+ await new Promise((resolve, reject) => {
37
+ const installProcess = spawn('npm', [
38
+ 'install',
39
+ `@hubspot/serverless-dev-runtime@${TARGET_SERVERLESS_RUNTIME_VERSION}`,
40
+ '--production',
41
+ '--no-save',
42
+ '--loglevel=error',
43
+ ], { cwd: CACHE_DIR, stdio: 'ignore' });
44
+ installProcess.on('close', code => {
45
+ if (code === 0) {
46
+ SpinniesManager.succeed('serverless-runtime-install', {
47
+ text: lib.cms.serverlessDevRuntime.installSucceeded,
48
+ });
49
+ resolve();
50
+ }
51
+ else {
52
+ SpinniesManager.fail('serverless-runtime-install', {
53
+ text: lib.cms.serverlessDevRuntime.installFailed,
54
+ });
55
+ reject(new Error(lib.cms.serverlessDevRuntime.installFailed));
56
+ }
57
+ });
58
+ installProcess.on('error', error => {
59
+ SpinniesManager.fail('serverless-runtime-install', {
60
+ text: lib.cms.serverlessDevRuntime.installFailed,
61
+ });
62
+ reject(error);
63
+ });
64
+ });
65
+ }
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ export async function startServerlessDevRuntime(options) {
68
+ await ensureServerlessRuntimeInstalled();
69
+ const requireFromCache = createRequire(path.join(CACHE_DIR, 'package.json'));
70
+ const { start } = requireFromCache('@hubspot/serverless-dev-runtime');
71
+ start(options);
72
+ }
@@ -82,6 +82,7 @@ export declare const FEATURES: {
82
82
  readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
83
83
  readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
84
84
  readonly AGENT_TOOLS: "ThirdPartyAgentTools";
85
+ readonly APP_ACTIONS: "Developers:AppActions";
85
86
  };
86
87
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
87
88
  UPLOAD_SUCCESS: string;
@@ -93,6 +94,7 @@ export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
93
94
  UPDATE_PROJECT_DATA: string;
94
95
  UPDATE_UPLOAD_WARNINGS: string;
95
96
  DEV_SERVERS_STARTED: string;
97
+ UPLOAD_IN_PROGRESS: string;
96
98
  };
97
99
  export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
98
100
  UPLOAD: string;
@@ -101,6 +103,7 @@ export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
101
103
  APP_INSTALL_SUCCESS: string;
102
104
  APP_INSTALL_INITIATED: string;
103
105
  APP_INSTALL_FAILURE: string;
106
+ SET_AUTO_UPLOAD: string;
104
107
  };
105
108
  export declare const APP_INSTALLATION_STATES: {
106
109
  readonly NOT_INSTALLED: "NOT_INSTALLED";
package/lib/constants.js CHANGED
@@ -74,6 +74,7 @@ export const FEATURES = {
74
74
  APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
75
75
  THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
76
76
  AGENT_TOOLS: 'ThirdPartyAgentTools',
77
+ APP_ACTIONS: 'Developers:AppActions',
77
78
  };
78
79
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
79
80
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -85,6 +86,7 @@ export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
85
86
  UPDATE_PROJECT_DATA: 'server:updateProjectData',
86
87
  UPDATE_UPLOAD_WARNINGS: 'server:updateUploadWarnings',
87
88
  DEV_SERVERS_STARTED: 'server:devServersStarted',
89
+ UPLOAD_IN_PROGRESS: 'server:uploadInProgress',
88
90
  };
89
91
  export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
90
92
  UPLOAD: 'client:upload',
@@ -93,6 +95,7 @@ export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
93
95
  APP_INSTALL_SUCCESS: 'client:installSuccess',
94
96
  APP_INSTALL_INITIATED: 'client:installInitiated',
95
97
  APP_INSTALL_FAILURE: 'client:installFailure',
98
+ SET_AUTO_UPLOAD: 'client:setAutoUpload',
96
99
  };
97
100
  export const APP_INSTALLATION_STATES = {
98
101
  NOT_INSTALLED: 'NOT_INSTALLED',
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { Build } from '@hubspot/local-dev-lib/types/Build';
2
3
  import { Release } from '../api/releases.js';
3
4
  declare const ReleaseComponentSchema: z.ZodObject<{
4
5
  buildType: z.ZodString;
@@ -35,6 +36,74 @@ export declare const ReleaseListSchema: z.ZodObject<{
35
36
  }, z.core.$strip>;
36
37
  }, z.core.$strip>>;
37
38
  }, z.core.$strip>;
39
+ declare const BuildSubbuildStatusSchema: z.ZodObject<{
40
+ buildName: z.ZodString;
41
+ buildType: z.ZodString;
42
+ status: z.ZodString;
43
+ errorMessage: z.ZodOptional<z.ZodString>;
44
+ startedAt: z.ZodOptional<z.ZodString>;
45
+ finishedAt: z.ZodOptional<z.ZodString>;
46
+ rootPath: z.ZodOptional<z.ZodString>;
47
+ id: z.ZodOptional<z.ZodString>;
48
+ visible: z.ZodOptional<z.ZodBoolean>;
49
+ }, z.core.$strip>;
50
+ export declare const BuildSchema: z.ZodObject<{
51
+ buildId: z.ZodNumber;
52
+ status: z.ZodString;
53
+ isDeployed: z.ZodBoolean;
54
+ isAutoDeployEnabled: z.ZodOptional<z.ZodBoolean>;
55
+ deployableState: z.ZodOptional<z.ZodString>;
56
+ platformVersion: z.ZodOptional<z.ZodString>;
57
+ uploadMessage: z.ZodOptional<z.ZodString>;
58
+ enqueuedAt: z.ZodOptional<z.ZodString>;
59
+ startedAt: z.ZodOptional<z.ZodString>;
60
+ finishedAt: z.ZodOptional<z.ZodString>;
61
+ createdAt: z.ZodOptional<z.ZodString>;
62
+ subbuildStatuses: z.ZodArray<z.ZodObject<{
63
+ buildName: z.ZodString;
64
+ buildType: z.ZodString;
65
+ status: z.ZodString;
66
+ errorMessage: z.ZodOptional<z.ZodString>;
67
+ startedAt: z.ZodOptional<z.ZodString>;
68
+ finishedAt: z.ZodOptional<z.ZodString>;
69
+ rootPath: z.ZodOptional<z.ZodString>;
70
+ id: z.ZodOptional<z.ZodString>;
71
+ visible: z.ZodOptional<z.ZodBoolean>;
72
+ }, z.core.$strip>>;
73
+ }, z.core.$strip>;
74
+ export declare const ProjectBuildsListSchema: z.ZodObject<{
75
+ projectName: z.ZodString;
76
+ deployedBuildId: z.ZodOptional<z.ZodNumber>;
77
+ results: z.ZodArray<z.ZodObject<{
78
+ buildId: z.ZodNumber;
79
+ status: z.ZodString;
80
+ isDeployed: z.ZodBoolean;
81
+ isAutoDeployEnabled: z.ZodOptional<z.ZodBoolean>;
82
+ deployableState: z.ZodOptional<z.ZodString>;
83
+ platformVersion: z.ZodOptional<z.ZodString>;
84
+ uploadMessage: z.ZodOptional<z.ZodString>;
85
+ enqueuedAt: z.ZodOptional<z.ZodString>;
86
+ startedAt: z.ZodOptional<z.ZodString>;
87
+ finishedAt: z.ZodOptional<z.ZodString>;
88
+ createdAt: z.ZodOptional<z.ZodString>;
89
+ subbuildStatuses: z.ZodArray<z.ZodObject<{
90
+ buildName: z.ZodString;
91
+ buildType: z.ZodString;
92
+ status: z.ZodString;
93
+ errorMessage: z.ZodOptional<z.ZodString>;
94
+ startedAt: z.ZodOptional<z.ZodString>;
95
+ finishedAt: z.ZodOptional<z.ZodString>;
96
+ rootPath: z.ZodOptional<z.ZodString>;
97
+ id: z.ZodOptional<z.ZodString>;
98
+ visible: z.ZodOptional<z.ZodBoolean>;
99
+ }, z.core.$strip>>;
100
+ }, z.core.$strip>>;
101
+ paging: z.ZodOptional<z.ZodObject<{
102
+ next: z.ZodObject<{
103
+ after: z.ZodString;
104
+ }, z.core.$strip>;
105
+ }, z.core.$strip>>;
106
+ }, z.core.$strip>;
38
107
  export declare const ProjectInfoSchema: z.ZodObject<{
39
108
  projectName: z.ZodString;
40
109
  platformVersion: z.ZodString;
@@ -98,6 +167,9 @@ export declare const CreateTestAccountSchema: z.ZodObject<{
98
167
  export type ReleaseComponentJsonOutput = z.infer<typeof ReleaseComponentSchema>;
99
168
  export type ReleaseJsonOutput = z.infer<typeof ReleaseSchema>;
100
169
  export type ReleaseListJsonOutput = z.infer<typeof ReleaseListSchema>;
170
+ export type BuildSubbuildStatusJsonOutput = z.infer<typeof BuildSubbuildStatusSchema>;
171
+ export type BuildJsonOutput = z.infer<typeof BuildSchema>;
172
+ export type ProjectBuildsListJsonOutput = z.infer<typeof ProjectBuildsListSchema>;
101
173
  export type ProjectInfoJsonOutput = z.infer<typeof ProjectInfoSchema>;
102
174
  export type PreviewJsonOutput = z.infer<typeof PreviewSchema>;
103
175
  export type UploadJsonOutput = z.infer<typeof UploadSchema>;
@@ -105,5 +177,6 @@ export type DeployJsonOutput = z.infer<typeof DeploySchema>;
105
177
  export type InstallStatusJsonOutput = z.infer<typeof InstallStatusSchema>;
106
178
  export type InstallAppJsonOutput = z.infer<typeof InstallAppSchema>;
107
179
  export type CreateTestAccountJsonOutput = z.infer<typeof CreateTestAccountSchema>;
180
+ export declare function mapBuildToJsonOutput(build: Build, deployedBuildId?: number): BuildJsonOutput;
108
181
  export declare function mapReleaseToJsonOutput(release: Release): ReleaseJsonOutput;
109
182
  export {};
package/lib/jsonOutput.js CHANGED
@@ -21,6 +21,43 @@ export const ReleaseListSchema = z.object({
21
21
  })
22
22
  .optional(),
23
23
  });
24
+ const BuildSubbuildStatusSchema = z.object({
25
+ buildName: z.string(),
26
+ buildType: z.string(),
27
+ status: z.string(),
28
+ errorMessage: z.string().optional(),
29
+ startedAt: z.string().optional(),
30
+ finishedAt: z.string().optional(),
31
+ rootPath: z.string().optional(),
32
+ id: z.string().optional(),
33
+ visible: z.boolean().optional(),
34
+ });
35
+ export const BuildSchema = z.object({
36
+ buildId: z.number(),
37
+ status: z.string(),
38
+ isDeployed: z.boolean(),
39
+ isAutoDeployEnabled: z.boolean().optional(),
40
+ deployableState: z.string().optional(),
41
+ platformVersion: z.string().optional(),
42
+ uploadMessage: z.string().optional(),
43
+ enqueuedAt: z.string().optional(),
44
+ startedAt: z.string().optional(),
45
+ finishedAt: z.string().optional(),
46
+ createdAt: z.string().optional(),
47
+ subbuildStatuses: z.array(BuildSubbuildStatusSchema),
48
+ });
49
+ export const ProjectBuildsListSchema = z.object({
50
+ projectName: z.string(),
51
+ deployedBuildId: z.number().optional(),
52
+ results: z.array(BuildSchema),
53
+ paging: z
54
+ .object({
55
+ next: z.object({
56
+ after: z.string(),
57
+ }),
58
+ })
59
+ .optional(),
60
+ });
24
61
  export const ProjectInfoSchema = z.object({
25
62
  projectName: z.string(),
26
63
  platformVersion: z.string(),
@@ -78,6 +115,33 @@ export const CreateTestAccountSchema = z.object({
78
115
  accountId: z.number().optional(),
79
116
  personalAccessKey: z.string().optional(),
80
117
  });
118
+ export function mapBuildToJsonOutput(build, deployedBuildId) {
119
+ const { buildId, status, isAutoDeployEnabled, deployableState, platformVersion, uploadMessage, enqueuedAt, startedAt, finishedAt, createdAt, subbuildStatuses, } = build;
120
+ return {
121
+ buildId,
122
+ status,
123
+ isDeployed: buildId === deployedBuildId,
124
+ isAutoDeployEnabled,
125
+ deployableState,
126
+ platformVersion,
127
+ uploadMessage,
128
+ enqueuedAt,
129
+ startedAt,
130
+ finishedAt,
131
+ createdAt,
132
+ subbuildStatuses: subbuildStatuses.map(subbuild => ({
133
+ buildName: subbuild.buildName,
134
+ buildType: subbuild.buildType,
135
+ status: subbuild.status,
136
+ errorMessage: subbuild.errorMessage,
137
+ startedAt: subbuild.startedAt,
138
+ finishedAt: subbuild.finishedAt,
139
+ rootPath: subbuild.rootPath,
140
+ id: subbuild.id,
141
+ visible: subbuild.visible,
142
+ })),
143
+ };
144
+ }
81
145
  export function mapReleaseToJsonOutput(release) {
82
146
  const { releaseTag, buildId, createdAt, components } = release;
83
147
  return {
@@ -48,6 +48,7 @@ export async function createV2App(providedAuth, providedDistribution) {
48
48
  const componentTypeToGateMap = {
49
49
  [AppEventsKey]: FEATURES.APP_EVENTS,
50
50
  'workflow-action-tool': FEATURES.AGENT_TOOLS,
51
+ 'crm-bulk-action': FEATURES.APP_ACTIONS,
51
52
  };
52
53
  export async function calculateComponentTemplateChoices(components, authType, distribution, accountId, projectMetadata) {
53
54
  const enabledComponents = [];
@@ -22,6 +22,10 @@ declare class LocalDevLogger {
22
22
  cleanupError(): void;
23
23
  cleanupSuccess(): void;
24
24
  uploadInitiated(): void;
25
+ autoUploadScheduled(filePath: string): void;
26
+ autoUploadTriggered(): void;
27
+ autoUploadInProgress(): void;
28
+ autoUploadToggled(enabled: boolean): void;
25
29
  deployInitiated(): void;
26
30
  projectConfigMismatch(): void;
27
31
  uploadError(error: unknown): void;
@@ -99,6 +99,10 @@ class LocalDevLogger {
99
99
  uiLogger.log('');
100
100
  uiLogger.log(lib.LocalDevManager.localDevUIAutoMessage(this.state.targetTestingAccountId, showWelcomeScreen));
101
101
  }
102
+ if (this.state.autoUploadEnabled) {
103
+ uiLogger.log('');
104
+ uiLogger.log(lib.LocalDevManager.autoUploadEnabled);
105
+ }
102
106
  uiLogger.log('');
103
107
  uiLogger.log(lib.LocalDevManager.quitHelper);
104
108
  uiLine();
@@ -122,6 +126,20 @@ class LocalDevLogger {
122
126
  uploadInitiated() {
123
127
  uiLogger.log(lib.LocalDevProcess.uploadInitiated);
124
128
  }
129
+ autoUploadScheduled(filePath) {
130
+ uiLogger.debug(lib.LocalDevProcess.autoUploadScheduled(filePath));
131
+ }
132
+ autoUploadTriggered() {
133
+ uiLogger.debug(lib.LocalDevProcess.autoUploadTriggered);
134
+ }
135
+ autoUploadInProgress() {
136
+ uiLogger.debug(lib.LocalDevProcess.autoUploadInProgress);
137
+ }
138
+ autoUploadToggled(enabled) {
139
+ uiLogger.log('');
140
+ uiLogger.log(lib.LocalDevProcess.autoUploadToggled(enabled));
141
+ uiLogger.log('');
142
+ }
125
143
  deployInitiated() {
126
144
  uiLogger.log(lib.LocalDevProcess.deployInitiated);
127
145
  }
@@ -8,6 +8,9 @@ declare class LocalDevProcess {
8
8
  private _logger;
9
9
  private devServerManager;
10
10
  private devSessionManager;
11
+ private autoUploadTimeout;
12
+ private pendingAutoUpload;
13
+ private autoUploadStopped;
11
14
  constructor(options: LocalDevStateConstructorOptions);
12
15
  get projectDir(): string;
13
16
  get projectData(): Project;
@@ -17,6 +20,10 @@ declare class LocalDevProcess {
17
20
  [key: string]: IntermediateRepresentationNodeLocalDev;
18
21
  };
19
22
  get logger(): LocalDevLogger;
23
+ get autoUploadEnabled(): boolean;
24
+ setAutoUploadEnabled(enabled: boolean): void;
25
+ get autoUploadAvailable(): boolean;
26
+ private isAutoDeployEnabled;
20
27
  private setupDevServers;
21
28
  private startDevServers;
22
29
  private cleanupDevServers;
@@ -27,10 +34,13 @@ declare class LocalDevProcess {
27
34
  private openLocalDevUi;
28
35
  private updateProjectData;
29
36
  handleFileChange(filePath: string, event: string): Promise<void>;
30
- handleConfigFileChange(): Promise<void>;
37
+ handleConfigFileChange(configFilePath: string): Promise<void>;
38
+ private scheduleAutoUpload;
39
+ private runAutoUpload;
31
40
  start(): Promise<void>;
32
41
  stop(showProgress?: boolean): Promise<void>;
33
42
  uploadProject(): Promise<LocalDevProjectUploadResult>;
43
+ private performUpload;
34
44
  deployLatestBuild(force?: boolean): Promise<LocalDevProjectDeployResult>;
35
45
  addStateListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
36
46
  sendDevServerMessage(message: LocalDevServerMessage): void;
@@ -17,11 +17,15 @@ import { getLocalDevUiUrl } from '../urls.js';
17
17
  import { CONFIG_LOCAL_STATE_FLAGS, PROJECT_DEPLOY_STATES, } from '../../constants.js';
18
18
  import { lib } from '../../../lang/en.js';
19
19
  import { debugError } from '../../errorHandlers/index.js';
20
+ const AUTO_UPLOAD_DEBOUNCE_MS = 2000;
20
21
  class LocalDevProcess {
21
22
  state;
22
23
  _logger;
23
24
  devServerManager;
24
25
  devSessionManager;
26
+ autoUploadTimeout = null;
27
+ pendingAutoUpload = false;
28
+ autoUploadStopped = false;
25
29
  constructor(options) {
26
30
  this.state = new LocalDevState(options);
27
31
  this._logger = new LocalDevLogger(this.state);
@@ -53,6 +57,27 @@ class LocalDevProcess {
53
57
  get logger() {
54
58
  return this._logger;
55
59
  }
60
+ get autoUploadEnabled() {
61
+ return this.state.autoUploadEnabled;
62
+ }
63
+ setAutoUploadEnabled(enabled) {
64
+ this.state.autoUploadEnabled = enabled;
65
+ this.logger.autoUploadToggled(enabled);
66
+ if (!enabled) {
67
+ if (this.autoUploadTimeout) {
68
+ clearTimeout(this.autoUploadTimeout);
69
+ this.autoUploadTimeout = null;
70
+ }
71
+ this.pendingAutoUpload = false;
72
+ }
73
+ }
74
+ get autoUploadAvailable() {
75
+ return this.isAutoDeployEnabled();
76
+ }
77
+ isAutoDeployEnabled() {
78
+ const { deployedBuild, latestBuild } = this.state.projectData;
79
+ return (deployedBuild ?? latestBuild)?.isAutoDeployEnabled ?? true;
80
+ }
56
81
  async setupDevServers() {
57
82
  try {
58
83
  await this.devServerManager.setup();
@@ -141,10 +166,52 @@ class LocalDevProcess {
141
166
  catch (e) {
142
167
  this.logger.fileChangeError(e);
143
168
  }
169
+ this.scheduleAutoUpload(filePath);
144
170
  }
145
- async handleConfigFileChange() {
171
+ async handleConfigFileChange(configFilePath) {
146
172
  await this.updateProjectNodes();
147
- this.logger.uploadWarning();
173
+ if (this.state.autoUploadEnabled) {
174
+ this.scheduleAutoUpload(configFilePath);
175
+ }
176
+ else {
177
+ this.logger.uploadWarning();
178
+ }
179
+ }
180
+ scheduleAutoUpload(changedPath) {
181
+ if (!this.state.autoUploadEnabled || this.autoUploadStopped) {
182
+ return;
183
+ }
184
+ this.logger.autoUploadScheduled(changedPath ?? 'project files');
185
+ if (this.autoUploadTimeout) {
186
+ clearTimeout(this.autoUploadTimeout);
187
+ }
188
+ this.autoUploadTimeout = setTimeout(() => {
189
+ this.autoUploadTimeout = null;
190
+ void this.runAutoUpload();
191
+ }, AUTO_UPLOAD_DEBOUNCE_MS);
192
+ }
193
+ async runAutoUpload() {
194
+ if (this.autoUploadStopped) {
195
+ return;
196
+ }
197
+ if (this.state.uploadInProgress) {
198
+ this.logger.autoUploadInProgress();
199
+ this.pendingAutoUpload = true;
200
+ return;
201
+ }
202
+ this.logger.autoUploadTriggered();
203
+ try {
204
+ await this.uploadProject();
205
+ }
206
+ catch (e) {
207
+ this.logger.uploadError(e);
208
+ }
209
+ finally {
210
+ if (this.pendingAutoUpload) {
211
+ this.pendingAutoUpload = false;
212
+ this.scheduleAutoUpload();
213
+ }
214
+ }
148
215
  }
149
216
  async start() {
150
217
  this.logger.resetSpinnies();
@@ -153,18 +220,24 @@ class LocalDevProcess {
153
220
  return this.state.actions.exit(EXIT_CODES.ERROR);
154
221
  }
155
222
  this.logger.startupMessage();
156
- if (isConfigFlagEnabled(CONFIG_FLAGS.AUTO_OPEN_BROWSER, true)) {
157
- this.openLocalDevUi();
158
- }
159
223
  await this.startDevServers();
160
224
  const devSessionRegistered = await this.devSessionManager.registerSession();
161
225
  if (!devSessionRegistered) {
162
226
  return this.state.actions.exit(EXIT_CODES.ERROR);
163
227
  }
228
+ if (isConfigFlagEnabled(CONFIG_FLAGS.AUTO_OPEN_BROWSER, true)) {
229
+ this.openLocalDevUi();
230
+ }
164
231
  this.state.devServersStarted = true;
165
232
  this.logger.monitorConsoleOutput();
166
233
  }
167
234
  async stop(showProgress = true) {
235
+ this.autoUploadStopped = true;
236
+ this.pendingAutoUpload = false;
237
+ if (this.autoUploadTimeout) {
238
+ clearTimeout(this.autoUploadTimeout);
239
+ this.autoUploadTimeout = null;
240
+ }
168
241
  if (showProgress) {
169
242
  this.logger.cleanupStart();
170
243
  }
@@ -182,6 +255,15 @@ class LocalDevProcess {
182
255
  return this.state.actions.exit(EXIT_CODES.SUCCESS);
183
256
  }
184
257
  async uploadProject() {
258
+ this.state.uploadInProgress = true;
259
+ try {
260
+ return await this.performUpload();
261
+ }
262
+ finally {
263
+ this.state.uploadInProgress = false;
264
+ }
265
+ }
266
+ async performUpload() {
185
267
  this.logger.uploadInitiated();
186
268
  const isUploadable = await this.projectConfigValidForUpload();
187
269
  if (!isUploadable) {
@@ -21,8 +21,10 @@ declare class LocalDevState {
21
21
  private _devServerMessage;
22
22
  private _uploadWarnings;
23
23
  private _devServersStarted;
24
+ private _autoUploadEnabled;
25
+ private _uploadInProgress;
24
26
  actions: LocalDevActions;
25
- constructor({ targetProjectAccountId, targetTestingAccountId, projectConfig, projectDir, projectData, debug, initialProjectNodes, initialProjectProfileData, profile, env, actions, }: LocalDevStateConstructorOptions);
27
+ constructor({ targetProjectAccountId, targetTestingAccountId, projectConfig, projectDir, projectData, debug, initialProjectNodes, initialProjectProfileData, profile, env, actions, autoUploadEnabled, }: LocalDevStateConstructorOptions);
26
28
  private runListeners;
27
29
  get targetProjectAccountId(): number;
28
30
  get targetTestingAccountId(): number;
@@ -56,6 +58,10 @@ declare class LocalDevState {
56
58
  get uploadWarnings(): Set<string>;
57
59
  get devServersStarted(): boolean;
58
60
  set devServersStarted(started: boolean);
61
+ get autoUploadEnabled(): boolean;
62
+ set autoUploadEnabled(autoUploadEnabled: boolean);
63
+ get uploadInProgress(): boolean;
64
+ set uploadInProgress(inProgress: boolean);
59
65
  addUploadWarning(warning: string): void;
60
66
  clearUploadWarnings(): void;
61
67
  addListener<K extends keyof LocalDevState>(key: K, listener: LocalDevStateListener<K>): void;
@@ -16,8 +16,10 @@ class LocalDevState {
16
16
  _devServerMessage;
17
17
  _uploadWarnings;
18
18
  _devServersStarted;
19
+ _autoUploadEnabled;
20
+ _uploadInProgress;
19
21
  actions;
20
- constructor({ targetProjectAccountId, targetTestingAccountId, projectConfig, projectDir, projectData, debug, initialProjectNodes, initialProjectProfileData, profile, env, actions, }) {
22
+ constructor({ targetProjectAccountId, targetTestingAccountId, projectConfig, projectDir, projectData, debug, initialProjectNodes, initialProjectProfileData, profile, env, actions, autoUploadEnabled, }) {
21
23
  this._targetProjectAccountId = targetProjectAccountId;
22
24
  this._targetTestingAccountId = targetTestingAccountId;
23
25
  this._profile = profile;
@@ -33,6 +35,8 @@ class LocalDevState {
33
35
  this._devServerMessage = LOCAL_DEV_SERVER_MESSAGE_TYPES.INITIAL;
34
36
  this._uploadWarnings = new Set();
35
37
  this._devServersStarted = false;
38
+ this._autoUploadEnabled = autoUploadEnabled || false;
39
+ this._uploadInProgress = false;
36
40
  this.actions = actions;
37
41
  this._listeners = {};
38
42
  }
@@ -117,6 +121,20 @@ class LocalDevState {
117
121
  this._devServersStarted = started;
118
122
  this.runListeners('devServersStarted');
119
123
  }
124
+ get autoUploadEnabled() {
125
+ return this._autoUploadEnabled;
126
+ }
127
+ set autoUploadEnabled(autoUploadEnabled) {
128
+ this._autoUploadEnabled = autoUploadEnabled;
129
+ this.runListeners('autoUploadEnabled');
130
+ }
131
+ get uploadInProgress() {
132
+ return this._uploadInProgress;
133
+ }
134
+ set uploadInProgress(inProgress) {
135
+ this._uploadInProgress = inProgress;
136
+ this.runListeners('uploadInProgress');
137
+ }
120
138
  addUploadWarning(warning) {
121
139
  this.uploadWarnings.add(warning);
122
140
  this.runListeners('uploadWarnings');
@@ -16,7 +16,7 @@ class LocalDevWatcher {
16
16
  }
17
17
  handleWatchEvent(filePath, event, configPaths) {
18
18
  if (configPaths.includes(filePath)) {
19
- return this.localDevProcess.handleConfigFileChange();
19
+ return this.localDevProcess.handleConfigFileChange(filePath);
20
20
  }
21
21
  return this.localDevProcess.handleFileChange(filePath, event);
22
22
  }
@@ -14,6 +14,7 @@ declare class LocalDevWebsocketServer {
14
14
  private setupAppDataListener;
15
15
  private setupUploadWarningsListener;
16
16
  private setupDevServersStartedListener;
17
+ private setupUploadInProgressListener;
17
18
  private setupStateListeners;
18
19
  start(): Promise<void>;
19
20
  shutdown(): void;
@@ -1,7 +1,7 @@
1
1
  import { addLocalStateFlag } from '@hubspot/local-dev-lib/config';
2
2
  import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, CONFIG_LOCAL_STATE_FLAGS, LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID, } from '../../constants.js';
3
3
  import { removeAnsiCodes } from '../../ui/removeAnsiCodes.js';
4
- import { isDeployWebsocketMessage, isViewedWelcomeScreenWebsocketMessage, isUploadWebsocketMessage, isAppInstallFailureWebsocketMessage, isAppInstallSuccessWebsocketMessage, isAppInstallInitiatedWebsocketMessage, } from './localDevWebsocketServerUtils.js';
4
+ import { isDeployWebsocketMessage, isViewedWelcomeScreenWebsocketMessage, isUploadWebsocketMessage, isAppInstallFailureWebsocketMessage, isAppInstallSuccessWebsocketMessage, isAppInstallInitiatedWebsocketMessage, isSetAutoUploadWebsocketMessage, } from './localDevWebsocketServerUtils.js';
5
5
  import CLIWebSocketServer from '../../CLIWebSocketServer.js';
6
6
  const LOCAL_DEV_WEBSOCKET_SERVER_VERSION = 2;
7
7
  const LOG_PREFIX = '[LocalDevWebsocketServer]';
@@ -78,6 +78,12 @@ class LocalDevWebsocketServer {
78
78
  this.handleAppInstallInitiated();
79
79
  return true;
80
80
  }
81
+ else if (isSetAutoUploadWebsocketMessage(message)) {
82
+ if (this.localDevProcess.autoUploadAvailable) {
83
+ this.localDevProcess.setAutoUploadEnabled(message.data.enabled);
84
+ }
85
+ return true;
86
+ }
81
87
  return false;
82
88
  }
83
89
  sendProjectData(websocket) {
@@ -90,6 +96,9 @@ class LocalDevWebsocketServer {
90
96
  deployedBuild: this.localDevProcess.projectData.deployedBuild,
91
97
  targetProjectAccountId: this.localDevProcess.targetProjectAccountId,
92
98
  targetTestingAccountId: this.localDevProcess.targetTestingAccountId,
99
+ autoUploadEnabled: this.localDevProcess.autoUploadAvailable
100
+ ? this.localDevProcess.autoUploadEnabled
101
+ : undefined,
93
102
  },
94
103
  });
95
104
  }
@@ -143,11 +152,24 @@ class LocalDevWebsocketServer {
143
152
  this.localDevProcess.removeStateListener('devServersStarted', listener);
144
153
  });
145
154
  }
155
+ setupUploadInProgressListener(websocket) {
156
+ const listener = (uploadInProgress) => {
157
+ this.cliWebSocketServer.sendMessage(websocket, {
158
+ type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPLOAD_IN_PROGRESS,
159
+ data: { uploadInProgress },
160
+ });
161
+ };
162
+ this.localDevProcess.addStateListener('uploadInProgress', listener);
163
+ websocket.on('close', () => {
164
+ this.localDevProcess.removeStateListener('uploadInProgress', listener);
165
+ });
166
+ }
146
167
  setupStateListeners(websocket) {
147
168
  this.setupProjectNodesListener(websocket);
148
169
  this.setupAppDataListener(websocket);
149
170
  this.setupUploadWarningsListener(websocket);
150
171
  this.setupDevServersStartedListener(websocket);
172
+ this.setupUploadInProgressListener(websocket);
151
173
  }
152
174
  async start() {
153
175
  await this.cliWebSocketServer.start({
@@ -1,7 +1,8 @@
1
- import { LocalDevDeployWebsocketMessage } from '../../../types/LocalDev.js';
1
+ import { LocalDevDeployWebsocketMessage, LocalDevSetAutoUploadWebsocketMessage } from '../../../types/LocalDev.js';
2
2
  import { CLIWebSocketMessage } from '../../CLIWebSocketServer.js';
3
3
  export declare function isUploadWebsocketMessage(message: CLIWebSocketMessage): boolean;
4
4
  export declare function isDeployWebsocketMessage(message: CLIWebSocketMessage): message is LocalDevDeployWebsocketMessage;
5
+ export declare function isSetAutoUploadWebsocketMessage(message: CLIWebSocketMessage): message is LocalDevSetAutoUploadWebsocketMessage;
5
6
  export declare function isViewedWelcomeScreenWebsocketMessage(message: CLIWebSocketMessage): boolean;
6
7
  export declare function isAppInstallSuccessWebsocketMessage(message: CLIWebSocketMessage): boolean;
7
8
  export declare function isAppInstallInitiatedWebsocketMessage(message: CLIWebSocketMessage): boolean;
@@ -5,6 +5,9 @@ export function isUploadWebsocketMessage(message) {
5
5
  export function isDeployWebsocketMessage(message) {
6
6
  return message.type === LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.DEPLOY;
7
7
  }
8
+ export function isSetAutoUploadWebsocketMessage(message) {
9
+ return message.type === LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.SET_AUTO_UPLOAD;
10
+ }
8
11
  export function isViewedWelcomeScreenWebsocketMessage(message) {
9
12
  return (message.type === LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.VIEWED_WELCOME_SCREEN);
10
13
  }
@@ -60,7 +60,7 @@ export async function resolveBuildId(accountId, projectName, buildOption, force)
60
60
  export async function validateBuildForRelease(accountId, projectName, buildId) {
61
61
  try {
62
62
  const { data: build } = await getBuildStatus(accountId, projectName, buildId);
63
- return meetsMinimumPlatformVersion(build.platformVersion, PLATFORM_VERSIONS.v2026_09_BETA);
63
+ return meetsMinimumPlatformVersion(build.platformVersion, PLATFORM_VERSIONS.v2027_03_BETA);
64
64
  }
65
65
  catch (e) {
66
66
  if (isSpecifiedError(e, { statusCode: 404 })) {
@@ -20,6 +20,7 @@ declare const inputSchemaZodObject: z.ZodObject<{
20
20
  settings: "settings";
21
21
  "app-event": "app-event";
22
22
  "workflow-action-tool": "workflow-action-tool";
23
+ "crm-bulk-action": "crm-bulk-action";
23
24
  page: "page";
24
25
  webhooks: "webhooks";
25
26
  "app-function": "app-function";
@@ -24,6 +24,7 @@ declare const inputSchemaZodObject: z.ZodObject<{
24
24
  settings: "settings";
25
25
  "app-event": "app-event";
26
26
  "workflow-action-tool": "workflow-action-tool";
27
+ "crm-bulk-action": "crm-bulk-action";
27
28
  page: "page";
28
29
  webhooks: "webhooks";
29
30
  "app-function": "app-function";
@@ -6,6 +6,7 @@ export declare const features: z.ZodOptional<z.ZodArray<z.ZodEnum<{
6
6
  settings: "settings";
7
7
  "app-event": "app-event";
8
8
  "workflow-action-tool": "workflow-action-tool";
9
+ "crm-bulk-action": "crm-bulk-action";
9
10
  page: "page";
10
11
  webhooks: "webhooks";
11
12
  "app-function": "app-function";
@@ -18,8 +18,9 @@ export const features = z
18
18
  'app-event',
19
19
  'scim',
20
20
  'page',
21
+ 'crm-bulk-action',
21
22
  ]))
22
- .describe('The features to include in the project, multiple options can be selected. "app-function" is also known as a private serverless function. "app-function-endpoint" is a serverless functions that is publicly accessible via endpoint. "workflow-action" is also known as a custom workflow action. "workflow-action-tool" is also known as agent tools.')
23
+ .describe('The features to include in the project, multiple options can be selected. "app-function" is also known as a private serverless function. "app-function-endpoint" is a serverless functions that is publicly accessible via endpoint. "workflow-action" is also known as a custom workflow action. "workflow-action-tool" is also known as agent tools. "crm-bulk-action" is an app actions extension that lets users act on multiple CRM records at once.')
23
24
  .optional();
24
25
  export const docsSearchQuery = z
25
26
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.13.0",
3
+ "version": "8.14.0-beta.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",
@@ -10,10 +10,9 @@
10
10
  "!**/__tests__/**"
11
11
  ],
12
12
  "dependencies": {
13
- "@hubspot/local-dev-lib": "5.10.2",
14
- "@hubspot/project-parsing-lib": "0.21.0",
15
- "@hubspot/serverless-dev-runtime": "7.0.7",
16
- "@hubspot/ui-extensions-dev-server": "2.1.0",
13
+ "@hubspot/local-dev-lib": "5.10.3",
14
+ "@hubspot/project-parsing-lib": "0.23.0",
15
+ "@hubspot/ui-extensions-dev-server": "2.1.2",
17
16
  "@inquirer/prompts": "7.1.0",
18
17
  "@modelcontextprotocol/sdk": "1.29.0",
19
18
  "archiver": "7.0.1",
@@ -21,13 +20,13 @@
21
20
  "chokidar": "3.6.0",
22
21
  "cli-cursor": "3.1.0",
23
22
  "cli-progress": "3.12.0",
24
- "express": "4.22.1",
23
+ "express": "4.22.2",
25
24
  "findup-sync": "4.0.0",
26
25
  "fs-extra": "8.1.0",
27
26
  "ink": "6.6.0",
28
27
  "ink-spinner": "5.0.0",
29
28
  "ink-text-input": "6.0.0",
30
- "js-yaml": "4.1.1",
29
+ "js-yaml": "4.3.1",
31
30
  "minimatch": "10.2.5",
32
31
  "moment": "2.30.1",
33
32
  "open": "7.4.2",
@@ -36,9 +35,9 @@
36
35
  "semver": "7.6.3",
37
36
  "strip-ansi": "7.1.0",
38
37
  "table": "6.9.0",
39
- "tmp": "0.2.4",
38
+ "tmp": "0.2.7",
40
39
  "update-notifier": "7.3.1",
41
- "ws": "8.20.0",
40
+ "ws": "8.21.0",
42
41
  "yargs": "17.7.2",
43
42
  "yargs-parser": "21.1.1",
44
43
  "zod": "^4.4.3"
@@ -61,7 +60,7 @@
61
60
  "@typescript-eslint/eslint-plugin": "^8.30.1",
62
61
  "@typescript-eslint/parser": "^8.11.0",
63
62
  "@vitest/coverage-v8": "^2.1.9",
64
- "axios": "1.18.1",
63
+ "axios": "1.19.0",
65
64
  "eslint": "^8.56.0",
66
65
  "eslint-plugin-import": "^2.31.0",
67
66
  "husky": "^4.3.8",
@@ -125,6 +124,8 @@
125
124
  },
126
125
  "resolutions": {
127
126
  "eslint-visitor-keys": "4.2.0",
128
- "@eslint-community/eslint-utils": "4.9.0"
127
+ "@eslint-community/eslint-utils": "4.9.0",
128
+ "@opentelemetry/core": "2.10.0",
129
+ "axios": "1.19.0"
129
130
  }
130
131
  }
@@ -25,6 +25,7 @@ export type LocalDevStateConstructorOptions = {
25
25
  initialProjectProfileData: HSProfileVariables;
26
26
  env: Environment;
27
27
  actions: LocalDevActions;
28
+ autoUploadEnabled?: boolean;
28
29
  };
29
30
  export type LocalDevDeployWebsocketMessage = {
30
31
  type: typeof LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.DEPLOY;
@@ -32,6 +33,12 @@ export type LocalDevDeployWebsocketMessage = {
32
33
  force: boolean;
33
34
  };
34
35
  };
36
+ export type LocalDevSetAutoUploadWebsocketMessage = {
37
+ type: typeof LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.SET_AUTO_UPLOAD;
38
+ data: {
39
+ enabled: boolean;
40
+ };
41
+ };
35
42
  export type LocalDevStateListener<K extends keyof LocalDevState> = (value: LocalDevState[K]) => void;
36
43
  export type AppLocalDevData = {
37
44
  id: number;
package/types/Yargs.d.ts CHANGED
@@ -49,6 +49,7 @@ export type ProjectDevArgs = CommonArgs & ConfigArgs & EnvironmentArgs & {
49
49
  testingAccount?: string | number;
50
50
  projectAccount?: string | number;
51
51
  port?: number;
52
+ autoUpload?: boolean;
52
53
  };
53
54
  export type TestingArgs = {
54
55
  qa?: boolean;