@hs-x/cli 0.4.3 → 0.4.5

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 (51) hide show
  1. package/dist/commands/completion.d.ts.map +1 -1
  2. package/dist/commands/completion.js +1 -0
  3. package/dist/commands/completion.js.map +1 -1
  4. package/dist/commands/control-plane-read.js +1 -1
  5. package/dist/commands/control-plane-read.js.map +1 -1
  6. package/dist/commands/deploy.d.ts.map +1 -1
  7. package/dist/commands/deploy.js +182 -83
  8. package/dist/commands/deploy.js.map +1 -1
  9. package/dist/commands/dev.d.ts +1 -0
  10. package/dist/commands/dev.d.ts.map +1 -1
  11. package/dist/commands/dev.js +11 -6
  12. package/dist/commands/dev.js.map +1 -1
  13. package/dist/commands/help-command.d.ts.map +1 -1
  14. package/dist/commands/help-command.js +6 -1
  15. package/dist/commands/help-command.js.map +1 -1
  16. package/dist/commands/project-lifecycle.d.ts.map +1 -1
  17. package/dist/commands/project-lifecycle.js +143 -46
  18. package/dist/commands/project-lifecycle.js.map +1 -1
  19. package/dist/commands/project.d.ts +1 -0
  20. package/dist/commands/project.d.ts.map +1 -1
  21. package/dist/commands/project.js +24 -10
  22. package/dist/commands/project.js.map +1 -1
  23. package/dist/constants.d.ts +1 -1
  24. package/dist/constants.js +1 -1
  25. package/dist/dev/browser-log-bridge.d.ts +12 -0
  26. package/dist/dev/browser-log-bridge.d.ts.map +1 -1
  27. package/dist/dev/browser-log-bridge.js +86 -3
  28. package/dist/dev/browser-log-bridge.js.map +1 -1
  29. package/dist/dev/frontend-log-relay.d.ts +3 -1
  30. package/dist/dev/frontend-log-relay.d.ts.map +1 -1
  31. package/dist/dev/frontend-log-relay.js +48 -3
  32. package/dist/dev/frontend-log-relay.js.map +1 -1
  33. package/dist/dev/session-manager.d.ts +2 -0
  34. package/dist/dev/session-manager.d.ts.map +1 -1
  35. package/dist/dev/session-manager.js +21 -6
  36. package/dist/dev/session-manager.js.map +1 -1
  37. package/dist/dev/stream-renderer.d.ts.map +1 -1
  38. package/dist/dev/stream-renderer.js +5 -6
  39. package/dist/dev/stream-renderer.js.map +1 -1
  40. package/dist/hubspot-auth.d.ts.map +1 -1
  41. package/dist/hubspot-auth.js +86 -3
  42. package/dist/hubspot-auth.js.map +1 -1
  43. package/dist/hubspot-developer-client.d.ts +1 -0
  44. package/dist/hubspot-developer-client.d.ts.map +1 -1
  45. package/dist/hubspot-developer-client.js +16 -1
  46. package/dist/hubspot-developer-client.js.map +1 -1
  47. package/dist/hubspot-project-lifecycle.d.ts +12 -0
  48. package/dist/hubspot-project-lifecycle.d.ts.map +1 -1
  49. package/dist/hubspot-project-lifecycle.js +28 -0
  50. package/dist/hubspot-project-lifecycle.js.map +1 -1
  51. package/package.json +1 -1
@@ -10,6 +10,7 @@ import { cloudflareResourceName, generateHubSpotRuntimeProject, generateProjectA
10
10
  import { HttpClientRequest, Schema, schemas, signDeploymentAttestationGrant } from '@hs-x/types';
11
11
  import { validateProject } from '@hs-x/validator';
12
12
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
+ import { translate as translateHubSpotProject } from '@hubspot/project-parsing-lib/translate';
13
14
  import { Effect, Option } from 'effect';
14
15
  import { isLinked } from '../account-store.js';
15
16
  import { exitWith } from '../cli-error.js';
@@ -27,7 +28,7 @@ import { resolveDeployTargets } from '../deploy-targets.js';
27
28
  import { executeCliHttp } from '../effect-http.js';
28
29
  import { DirectHubSpotAccountResolutionError, createDeployHubSpotDeveloperClient, resolveDirectHubSpotContext, } from '../hubspot-developer-client.js';
29
30
  import { HubSpotProjectArchiveSourceError, fingerprintHubSpotArchiveSource, writeLocalHubSpotProjectArchive, } from '../hubspot-project-archive.js';
30
- import { deployHubSpotProjectBuild, waitForHubSpotAutoDeploy, waitForHubSpotBuildStatus, } from '../hubspot-project-lifecycle.js';
31
+ import { deployHubSpotProjectBuild, readHubSpotSubbuildStatuses, waitForHubSpotAutoDeploy, waitForHubSpotBuildStatus, } from '../hubspot-project-lifecycle.js';
31
32
  import { detectLocalProjectMode, readHubSpotProjectDescriptor, validateHubSpotProject, } from '../hubspot-project.js';
32
33
  import { hydrateAncestorEnv } from '../load-env.js';
33
34
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
@@ -772,6 +773,15 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
772
773
  force,
773
774
  sourceFingerprint,
774
775
  createClient: async () => context.createClient(),
776
+ ...(!isLegacyProject(platformVersion)
777
+ ? {
778
+ translateProject: () => context.runWithEnvironment((selectedAccount) => translateHubSpotProject({
779
+ projectSourceDir: join(root, descriptor.srcDir),
780
+ platformVersion,
781
+ accountId: Number(selectedAccount.id),
782
+ })),
783
+ }
784
+ : {}),
775
785
  };
776
786
  }
777
787
  }
@@ -929,7 +939,6 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
929
939
  });
930
940
  }
931
941
  try {
932
- const step = machineOutput ? undefined : reporter.step('Uploading to HubSpot');
933
942
  const uploaded = await executeHubSpotOnlyUpload({
934
943
  argv,
935
944
  root,
@@ -937,6 +946,7 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
937
946
  persistBinding: false,
938
947
  uploadOnly,
939
948
  prepared: preparedUpload,
949
+ ...(machineOutput ? {} : { progress: reporter }),
940
950
  });
941
951
  const buildState = readStatus(uploaded.buildStatus) ?? 'UNKNOWN';
942
952
  const deployState = uploaded.deployStatus
@@ -944,7 +954,6 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
944
954
  : undefined;
945
955
  const deployUnverified = uploaded.autoDeployExpected && deployState === undefined;
946
956
  const ok = buildState === 'SUCCESS' && !deployUnverified && (!deployState || deployState === 'SUCCESS');
947
- step?.[ok ? 'ok' : 'fail'](`build #${uploaded.buildId} ${buildState}${deployState ? ` · deploy ${deployState}` : deployUnverified ? ' · deploy unverified' : ''}`);
948
957
  const result = {
949
958
  schema_version: 1,
950
959
  ok,
@@ -970,7 +979,7 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
970
979
  reporter.done(ok ? directDeployDoneMessage(uploaded) : 'HubSpot deploy failed', ok ? 0 : 1);
971
980
  }
972
981
  else {
973
- streamHubSpotUploadOutcome(reporter, uploaded);
982
+ streamHubSpotUploadOutcome(reporter, uploaded, { componentMode: 'failures' });
974
983
  if (deployUnverified) {
975
984
  reporter.error('HSX_E_DEPLOY_HUBSPOT', 'HubSpot auto-deploy could not be verified before the timeout.', { hint: 'Check the project deploy status in HubSpot, then retry if it did not ship.' });
976
985
  }
@@ -1455,31 +1464,22 @@ export async function deployCommand({ argv, root, json, }) {
1455
1464
  // vs deploy-then-upload). A FAILED build resolves the step as a failure so
1456
1465
  // the failing phase is visible in the transcript, not just the exit code.
1457
1466
  const runHubSpotUploadPhase = async () => {
1458
- const uploadStartedAt = Date.now();
1459
- const uploadStep = progress?.step('Uploading to HubSpot');
1460
- let result;
1461
- try {
1462
- result = await executeHubSpotOnlyUpload({
1463
- argv,
1464
- root,
1465
- local: localHubspotUpload,
1466
- uploadOnly: hubspotUploadOnly,
1467
- });
1468
- }
1469
- catch (error) {
1470
- uploadStep?.fail(firstErrorLine(error));
1471
- throw error;
1472
- }
1473
- const buildState = readStatus(result.buildStatus) ?? 'UNKNOWN';
1474
- const detail = `build #${result.buildId} ${buildState} (${elapsedSince(uploadStartedAt)})`;
1475
- if (buildState === 'SUCCESS') {
1476
- uploadStep?.ok(detail);
1477
- }
1478
- else {
1479
- uploadStep?.fail(detail);
1480
- }
1467
+ const result = await executeHubSpotOnlyUpload({
1468
+ argv,
1469
+ root,
1470
+ local: localHubspotUpload,
1471
+ uploadOnly: hubspotUploadOnly,
1472
+ ...(progress
1473
+ ? {
1474
+ progress: {
1475
+ step: (label) => progress.step(label),
1476
+ rows: (rows) => progress.reporter.rows(rows),
1477
+ },
1478
+ }
1479
+ : {}),
1480
+ });
1481
1481
  if (echo)
1482
- streamHubSpotUploadOutcome(echo, result);
1482
+ streamHubSpotUploadOutcome(echo, result, { componentMode: 'failures' });
1483
1483
  return result;
1484
1484
  };
1485
1485
  if (uploadFirstOrigin && controlPlanePlan) {
@@ -2081,12 +2081,13 @@ function buildDeploySummaryRows(input) {
2081
2081
  * diagnosable from the terminal, not the HubSpot UI (run-008 build #2 failed
2082
2082
  * silently here).
2083
2083
  */
2084
- function streamHubSpotUploadOutcome(reporter, upload) {
2084
+ function streamHubSpotUploadOutcome(reporter, upload, options = {}) {
2085
+ const componentMode = options.componentMode ?? 'all';
2085
2086
  for (const subbuild of readSubbuildStatuses(upload.buildStatus)) {
2086
- if (subbuild.status === 'SUCCESS') {
2087
+ if (subbuild.status === 'SUCCESS' && componentMode === 'all') {
2087
2088
  reporter.info(` [ok] ${subbuild.name}${subbuild.type ? ` (${subbuild.type})` : ''}`);
2088
2089
  }
2089
- else {
2090
+ else if (subbuild.status !== 'SUCCESS' && componentMode !== 'none') {
2090
2091
  reporter.error('HSX_E_DEPLOY_HUBSPOT_BUILD_COMPONENT', `${subbuild.name}${subbuild.type ? ` (${subbuild.type})` : ''}: ${subbuild.errorMessage ?? subbuild.status}`);
2091
2092
  }
2092
2093
  }
@@ -4288,15 +4289,35 @@ function runCloudflareCommand(command, options) {
4288
4289
  },
4289
4290
  })));
4290
4291
  }
4291
- async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = true, uploadOnly, prepared, }) {
4292
+ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = true, uploadOnly, prepared, progress, }) {
4292
4293
  // The HubSpot project name must match the name baked into the generated
4293
4294
  // bundle (hsproject.json + app-hsmeta uid), not the on-disk directory name.
4294
4295
  // Using basename(root) breaks when the project lives in a generic dir (e.g.
4295
4296
  // `app/`): the upload targets a project named "app" while the bundle declares
4296
4297
  // the real project id, and HubSpot rejects the mismatch with an opaque 400.
4297
4298
  const projectName = prepared?.projectName ?? (await resolveHubSpotProjectName(argv, root));
4298
- const intermediateRepresentation = prepared?.intermediateRepresentation ??
4299
+ let intermediateRepresentation = prepared?.intermediateRepresentation ??
4299
4300
  (await readHubSpotProjectIntermediateRepresentation(root));
4301
+ if (prepared?.translateProject) {
4302
+ const translateStartedAt = Date.now();
4303
+ const translateStep = progress?.step('Translating HubSpot components');
4304
+ try {
4305
+ const translated = await prepared.translateProject();
4306
+ if (translated.skippedHsMetaFiles.length > 0 && !prepared.force) {
4307
+ throw new Error(`HubSpot skipped ${translated.skippedHsMetaFiles.length} component metadata file(s): ${translated.skippedHsMetaFiles.join(', ')}. Review them or re-run with --force.`);
4308
+ }
4309
+ intermediateRepresentation = translated.intermediateRepresentation;
4310
+ const nodes = isRecord(intermediateRepresentation)
4311
+ ? intermediateRepresentation.intermediateNodesIndexedByUid
4312
+ : undefined;
4313
+ const componentCount = isRecord(nodes) ? Object.keys(nodes).length : 0;
4314
+ translateStep?.ok(`${componentCount} components · ${elapsedSince(translateStartedAt)}`);
4315
+ }
4316
+ catch (error) {
4317
+ translateStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(translateStartedAt)}`);
4318
+ throw error;
4319
+ }
4320
+ }
4300
4321
  const appUid = prepared?.appUid ??
4301
4322
  readAppUidFromIntermediateRepresentation(intermediateRepresentation) ??
4302
4323
  (await readAppUidFromProjectSource(root));
@@ -4306,10 +4327,20 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4306
4327
  const developerAccountId = prepared?.developerAccountId ??
4307
4328
  resolveFlag(argv, '--developer-account-id') ??
4308
4329
  process.env.HSX_HUBSPOT_DEVELOPER_ACCOUNT_ID;
4309
- const archivePath = await writeLocalHubSpotProjectArchive(root, projectName, {
4310
- sourceOnly: intermediateRepresentation !== undefined,
4311
- ...(prepared ? { expectedSourceFingerprint: prepared.sourceFingerprint } : {}),
4312
- });
4330
+ const packageStartedAt = Date.now();
4331
+ const packageStep = progress?.step('Packaging HubSpot project');
4332
+ let archivePath;
4333
+ try {
4334
+ archivePath = await writeLocalHubSpotProjectArchive(root, projectName, {
4335
+ sourceOnly: intermediateRepresentation !== undefined,
4336
+ ...(prepared ? { expectedSourceFingerprint: prepared.sourceFingerprint } : {}),
4337
+ });
4338
+ packageStep?.ok(elapsedSince(packageStartedAt));
4339
+ }
4340
+ catch (error) {
4341
+ packageStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(packageStartedAt)}`);
4342
+ throw error;
4343
+ }
4313
4344
  const client = prepared
4314
4345
  ? await prepared.createClient()
4315
4346
  : await createDeployHubSpotDeveloperClient({
@@ -4318,29 +4349,52 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4318
4349
  allowedOperations: ['hubspot-project-upload'],
4319
4350
  cwd: root,
4320
4351
  });
4321
- await client.projects.ensureProject({ projectName });
4322
4352
  const hsproject = await readHubSpotProjectConfig(root);
4323
4353
  const configuredPlatformVersion = typeof hsproject.platformVersion === 'string' ? hsproject.platformVersion : undefined;
4324
4354
  const platformVersion = prepared?.platformVersion ??
4325
4355
  resolveFlag(argv, '--platform-version') ??
4326
4356
  configuredPlatformVersion ??
4327
4357
  '2026.03';
4328
- const upload = await client.projects.upload({
4329
- projectName,
4330
- archivePath,
4331
- message: prepared?.buildMessage ?? resolveFlag(argv, '--message') ?? 'HS-X HubSpot-only deploy',
4332
- platformVersion,
4333
- intermediateRepresentation,
4334
- skipAutoDeploy: uploadOnly,
4335
- });
4336
4358
  const timeoutMs = prepared?.buildTimeoutMs ?? Number(resolveFlag(argv, '--hubspot-build-timeout-ms') ?? '60000');
4337
- const buildStatus = await waitForHubSpotBuildStatus({
4338
- client,
4339
- projectName,
4340
- buildId: upload.buildId,
4341
- timeoutMs,
4342
- intervalMs: 1000,
4343
- });
4359
+ const uploadStartedAt = Date.now();
4360
+ const uploadStep = progress?.step('Uploading project');
4361
+ let upload;
4362
+ try {
4363
+ await client.projects.ensureProject({ projectName });
4364
+ upload = await client.projects.upload({
4365
+ projectName,
4366
+ archivePath,
4367
+ message: prepared?.buildMessage ?? resolveFlag(argv, '--message') ?? 'HS-X HubSpot-only deploy',
4368
+ platformVersion,
4369
+ intermediateRepresentation,
4370
+ skipAutoDeploy: uploadOnly,
4371
+ });
4372
+ uploadStep?.ok(`build #${upload.buildId} · ${elapsedSince(uploadStartedAt)}`);
4373
+ }
4374
+ catch (error) {
4375
+ uploadStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(uploadStartedAt)}`);
4376
+ throw error;
4377
+ }
4378
+ const buildStartedAt = Date.now();
4379
+ const buildStep = progress?.step(`Building project (build #${upload.buildId})`);
4380
+ const reportedComponents = new Map();
4381
+ let buildStatus;
4382
+ try {
4383
+ buildStatus = await waitForHubSpotBuildStatus({
4384
+ client,
4385
+ projectName,
4386
+ buildId: upload.buildId,
4387
+ timeoutMs,
4388
+ intervalMs: 1000,
4389
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedComponents, 'build'),
4390
+ });
4391
+ }
4392
+ catch (error) {
4393
+ buildStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(buildStartedAt)}`);
4394
+ throw error;
4395
+ }
4396
+ const buildState = readStatus(buildStatus) ?? 'UNKNOWN';
4397
+ buildStep?.[buildState === 'SUCCESS' ? 'ok' : 'fail'](`${buildState} · ${elapsedSince(buildStartedAt)}`);
4344
4398
  const autoDeployEnabled = readBooleanProperty(buildStatus, 'isAutoDeployEnabled');
4345
4399
  if (uploadOnly || readStatus(buildStatus) !== 'SUCCESS' || autoDeployEnabled) {
4346
4400
  const appId = readStatus(buildStatus) === 'SUCCESS'
@@ -4358,15 +4412,32 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4358
4412
  // Normal uploads can auto-deploy; uploadOnly sends skipAutoDeploy and is
4359
4413
  // therefore a strict build-only result.
4360
4414
  const autoDeployExpected = !uploadOnly && autoDeployEnabled === true && readStatus(buildStatus) === 'SUCCESS';
4361
- const autoDeployStatus = autoDeployExpected
4362
- ? (await waitForHubSpotAutoDeploy({
4363
- client,
4364
- projectName,
4365
- buildId: upload.buildId,
4366
- buildStatus,
4367
- timeoutMs,
4368
- })).status
4369
- : undefined;
4415
+ let autoDeployStatus;
4416
+ if (autoDeployExpected) {
4417
+ const deployStartedAt = Date.now();
4418
+ const deployStep = progress?.step(`Auto-deploying project (build #${upload.buildId})`);
4419
+ const reportedDeployComponents = new Map();
4420
+ try {
4421
+ const autoDeploy = await waitForHubSpotAutoDeploy({
4422
+ client,
4423
+ projectName,
4424
+ buildId: upload.buildId,
4425
+ buildStatus,
4426
+ timeoutMs,
4427
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedDeployComponents, 'deploy'),
4428
+ });
4429
+ autoDeployStatus = autoDeploy.status;
4430
+ const state = readStatus(autoDeployStatus) ?? 'UNKNOWN';
4431
+ if (state === 'SUCCESS') {
4432
+ streamHubSpotComponentProgress(progress, buildStatus, reportedDeployComponents, 'deploy');
4433
+ }
4434
+ deployStep?.[state === 'SUCCESS' ? 'ok' : 'fail'](`deploy #${autoDeploy.deployId} ${state} · ${elapsedSince(deployStartedAt)}`);
4435
+ }
4436
+ catch (error) {
4437
+ deployStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(deployStartedAt)}`);
4438
+ throw error;
4439
+ }
4440
+ }
4370
4441
  return {
4371
4442
  projectName,
4372
4443
  archivePath,
@@ -4382,15 +4453,31 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4382
4453
  local,
4383
4454
  };
4384
4455
  }
4385
- const deployed = await deployHubSpotProjectBuild({
4386
- client,
4387
- projectName,
4388
- buildId: upload.buildId,
4389
- force: prepared?.force ?? argv.includes('--force'),
4390
- legacy: isLegacyProject(platformVersion),
4391
- timeoutMs,
4392
- executeDeploy: (request) => withTransientDeployRetry(() => client.projects.deployBuild(request)),
4393
- });
4456
+ const deployStartedAt = Date.now();
4457
+ const deployStep = progress?.step(`Deploying project (build #${upload.buildId})`);
4458
+ const reportedDeployComponents = new Map();
4459
+ let deployed;
4460
+ try {
4461
+ deployed = await deployHubSpotProjectBuild({
4462
+ client,
4463
+ projectName,
4464
+ buildId: upload.buildId,
4465
+ force: prepared?.force ?? argv.includes('--force'),
4466
+ legacy: isLegacyProject(platformVersion),
4467
+ timeoutMs,
4468
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedDeployComponents, 'deploy'),
4469
+ executeDeploy: (request) => withTransientDeployRetry(() => client.projects.deployBuild(request)),
4470
+ });
4471
+ const state = readStatus(deployed.status) ?? 'UNKNOWN';
4472
+ if (state === 'SUCCESS') {
4473
+ streamHubSpotComponentProgress(progress, buildStatus, reportedDeployComponents, 'deploy');
4474
+ }
4475
+ deployStep?.[state === 'SUCCESS' ? 'ok' : 'fail'](`deploy #${deployed.deployId} ${state} · ${elapsedSince(deployStartedAt)}`);
4476
+ }
4477
+ catch (error) {
4478
+ deployStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(deployStartedAt)}`);
4479
+ throw error;
4480
+ }
4394
4481
  const deployId = deployed.deployId;
4395
4482
  const deployStatus = deployed.status;
4396
4483
  const appId = await optionalHubSpotAppId(client, projectName, appUid);
@@ -4604,18 +4691,30 @@ async function resolveHubSpotAppAuthUrl(argv, root) {
4604
4691
  function readStatus(value) {
4605
4692
  return isRecord(value) && typeof value.status === 'string' ? value.status : undefined;
4606
4693
  }
4607
- /** Tolerant parse of local-dev-lib's build subbuildStatuses for reporting. */
4608
- function readSubbuildStatuses(value) {
4609
- if (!isRecord(value) || !Array.isArray(value.subbuildStatuses))
4610
- return [];
4611
- return value.subbuildStatuses.filter(isRecord).map((subbuild) => ({
4612
- name: String(subbuild.buildName ?? subbuild.componentName ?? 'component'),
4613
- ...(typeof subbuild.buildType === 'string' ? { type: subbuild.buildType } : {}),
4614
- status: String(subbuild.status ?? 'UNKNOWN'),
4615
- ...(typeof subbuild.errorMessage === 'string' && subbuild.errorMessage.length > 0
4616
- ? { errorMessage: subbuild.errorMessage }
4617
- : {}),
4618
- }));
4694
+ const readSubbuildStatuses = readHubSpotSubbuildStatuses;
4695
+ const HUBSPOT_COMPONENT_TERMINAL_STATES = new Set(['SUCCESS', 'FAILURE', 'ERROR', 'CANCELLED']);
4696
+ /** Emit each HubSpot component once, when it reaches a terminal build state. */
4697
+ function streamHubSpotComponentProgress(progress, buildStatus, reported, phase) {
4698
+ if (!progress)
4699
+ return;
4700
+ const rows = [];
4701
+ for (const component of readSubbuildStatuses(buildStatus)) {
4702
+ const state = component.status.toUpperCase();
4703
+ if (!HUBSPOT_COMPONENT_TERMINAL_STATES.has(state))
4704
+ continue;
4705
+ const key = `${component.name}\0${component.type ?? ''}`;
4706
+ if (reported.get(key) === state)
4707
+ continue;
4708
+ reported.set(key, state);
4709
+ rows.push({
4710
+ key: component.name,
4711
+ value: component.type ?? 'COMPONENT',
4712
+ status: state === 'SUCCESS' ? 'ok' : 'fail',
4713
+ detail: `${phase} ${state}`,
4714
+ ...(component.errorMessage ? { hint: component.errorMessage } : {}),
4715
+ });
4716
+ }
4717
+ progress.rows(rows);
4619
4718
  }
4620
4719
  function hasFlag(argv, flag) {
4621
4720
  return argv.includes(flag);