@hs-x/cli 0.2.4 → 0.2.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 (49) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/kit.d.ts.map +1 -1
  5. package/dist/cli/kit.js +1 -1
  6. package/dist/cli/kit.js.map +1 -1
  7. package/dist/cloudflare-oauth.d.ts.map +1 -1
  8. package/dist/cloudflare-oauth.js +10 -9
  9. package/dist/cloudflare-oauth.js.map +1 -1
  10. package/dist/commands/connect.d.ts +2 -0
  11. package/dist/commands/connect.d.ts.map +1 -1
  12. package/dist/commands/connect.js +29 -1
  13. package/dist/commands/connect.js.map +1 -1
  14. package/dist/commands/control-plane-read.d.ts +60 -9
  15. package/dist/commands/control-plane-read.d.ts.map +1 -1
  16. package/dist/commands/control-plane-read.js +219 -26
  17. package/dist/commands/control-plane-read.js.map +1 -1
  18. package/dist/commands/deploy.d.ts +6 -0
  19. package/dist/commands/deploy.d.ts.map +1 -1
  20. package/dist/commands/deploy.js +311 -17
  21. package/dist/commands/deploy.js.map +1 -1
  22. package/dist/commands/dev.d.ts.map +1 -1
  23. package/dist/commands/dev.js +7 -4
  24. package/dist/commands/dev.js.map +1 -1
  25. package/dist/commands/help-command.d.ts.map +1 -1
  26. package/dist/commands/help-command.js +33 -11
  27. package/dist/commands/help-command.js.map +1 -1
  28. package/dist/commands/migrate.d.ts +1 -1
  29. package/dist/commands/migrate.d.ts.map +1 -1
  30. package/dist/commands/migrate.js +119 -11
  31. package/dist/commands/migrate.js.map +1 -1
  32. package/dist/commands/status.d.ts +7 -1
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +45 -2
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/constants.d.ts +1 -1
  37. package/dist/constants.js +1 -1
  38. package/dist/dev/invoke.d.ts +9 -0
  39. package/dist/dev/invoke.d.ts.map +1 -1
  40. package/dist/dev/invoke.js +67 -0
  41. package/dist/dev/invoke.js.map +1 -1
  42. package/dist/errors-registry.d.ts.map +1 -1
  43. package/dist/errors-registry.js +29 -0
  44. package/dist/errors-registry.js.map +1 -1
  45. package/dist/project-binding.d.ts +7 -1
  46. package/dist/project-binding.d.ts.map +1 -1
  47. package/dist/project-binding.js +10 -5
  48. package/dist/project-binding.js.map +1 -1
  49. package/package.json +7 -7
@@ -412,6 +412,11 @@ export async function deployCommand({ argv, root, json, }) {
412
412
  projectId = answer;
413
413
  }
414
414
  }
415
+ if (projectId && accountId && !resolvedAcct.overrodeBinding) {
416
+ // Sticky binding, project half: project-scoped reads (status/logs/routes/
417
+ // drift) resolve their project id from here when no flag is passed.
418
+ await writeProjectBinding(root, accountId, projectId).catch(() => { });
419
+ }
415
420
  if (!json && willChangeAnything && !flagYes && isInteractive()) {
416
421
  const summary = buildDeployPlanSummary({
417
422
  accountId,
@@ -437,13 +442,31 @@ export async function deployCommand({ argv, root, json, }) {
437
442
  return cancelledResult(argv, 'deploy');
438
443
  }
439
444
  }
440
- const portalSchemaApply = applySchema
441
- ? await applyPortalSchemaPlan({
442
- argv,
443
- plan: portalSchemaPlan,
444
- source: portalSchemaRead?.source,
445
- })
446
- : undefined;
445
+ // The schema apply is the first phase that touches the portal. A failure here
446
+ // used to throw a raw HubSpot error out of the command — no HSX_E code, no
447
+ // phase/PLAN output (cold-stranger run #3, finding A). Render the curated
448
+ // failure WITH the plan that was being applied instead.
449
+ let portalSchemaApply;
450
+ if (applySchema) {
451
+ try {
452
+ portalSchemaApply = await applyPortalSchemaPlan({
453
+ argv,
454
+ plan: portalSchemaPlan,
455
+ source: portalSchemaRead?.source,
456
+ });
457
+ }
458
+ catch (error) {
459
+ return portalSchemaApplyFailure({
460
+ argv,
461
+ json,
462
+ root,
463
+ startedAt: commandStartedAt,
464
+ workers,
465
+ portalSchemaPlan,
466
+ error,
467
+ });
468
+ }
469
+ }
447
470
  const manifest = await generateProjectArtifacts({
448
471
  root,
449
472
  workers,
@@ -906,6 +929,11 @@ function renderDeployHuman(input) {
906
929
  .map((worker) => `${worker.workerName}${worker.dryRun ? ' (dry-run)' : ''}`)
907
930
  .join(', ');
908
931
  reporter.info(`Cloudflare deploy: ${deployed}`);
932
+ for (const worker of input.cloudflareDeployResult.workers) {
933
+ if (worker.unauthenticatedInvoke) {
934
+ reporter.warn('HSX_W_DEPLOY_UNAUTHENTICATED_INVOKE', `${worker.workerName}: workflow-action invoke endpoints accept unsigned requests until the app's OAuth client secret is configured (redeploy with HSX_HUBSPOT_CLIENT_SECRET / --hubspot-client-secret after the OAuth install).`);
935
+ }
936
+ }
909
937
  }
910
938
  if (!input.linked && input.cloudflareDeployResult) {
911
939
  reporter.info(`Deploy history for this machine: ${machineDashboardUrl(input.machineId)}`);
@@ -927,6 +955,22 @@ function renderDeployHuman(input) {
927
955
  if (upload.deployId !== undefined) {
928
956
  reporter.info(`HubSpot deploy id: ${upload.deployId}`);
929
957
  }
958
+ else if (hubspotBuildStatus === 'SUCCESS' &&
959
+ upload.deploySkipped &&
960
+ upload.deployStatus !== undefined) {
961
+ const autoStatus = readStatus(upload.deployStatus) ?? 'UNKNOWN';
962
+ if (autoStatus === 'SUCCESS') {
963
+ reporter.info(`HubSpot deploy: auto-deploy SUCCESS (build #${upload.buildId})`);
964
+ }
965
+ else {
966
+ reporter.warn('HSX_W_DEPLOY_HUBSPOT_AUTODEPLOY_FAILED', `HubSpot auto-deploy finished ${autoStatus} — the build is green but the app did NOT ship. Check the deploy log in the HubSpot UI (project ${upload.projectName}).`);
967
+ for (const subbuild of readSubbuildStatuses(upload.deployStatus)) {
968
+ if (subbuild.status !== 'SUCCESS') {
969
+ reporter.info(` [x] ${subbuild.name}: ${subbuild.errorMessage ?? subbuild.status}`);
970
+ }
971
+ }
972
+ }
973
+ }
930
974
  else if (hubspotBuildStatus === 'SUCCESS' && upload.deploySkipped) {
931
975
  reporter.info('HubSpot deploy: skipped (upload-only or auto-deploy handles it)');
932
976
  }
@@ -1077,13 +1121,24 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1077
1121
  });
1078
1122
  const applied = [];
1079
1123
  const skipped = [];
1124
+ // Each HubSpot write is wrapped so a failure carries WHICH action failed, the
1125
+ // target object type (scope errors are per-object), and what already applied
1126
+ // before it — the curated failure renderer needs all three.
1127
+ const apply = async (label, objectType, run) => {
1128
+ try {
1129
+ await run();
1130
+ }
1131
+ catch (cause) {
1132
+ throw new PortalSchemaApplyError({ action: label, objectType, applied: [...applied], cause });
1133
+ }
1134
+ applied.push(label);
1135
+ };
1080
1136
  for (const action of plan.actions) {
1081
1137
  if (action.kind === 'will-create-object') {
1082
- await client.schemaManagement.createSchema({
1138
+ await apply(`create-object:${action.objectType}`, action.objectType, () => client.schemaManagement.createSchema({
1083
1139
  name: action.objectType,
1084
1140
  labels: portalObjectLabels(action.objectType),
1085
- });
1086
- applied.push(`create-object:${action.objectType}`);
1141
+ }));
1087
1142
  continue;
1088
1143
  }
1089
1144
  if (!action.propertyName || !action.declaredType) {
@@ -1092,25 +1147,23 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1092
1147
  }
1093
1148
  if (action.kind === 'will-create-property') {
1094
1149
  const fieldType = defaultHubSpotFieldType(action.declaredType);
1095
- await client.schemaManagement.createProperty({
1150
+ await apply(`create-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.createProperty({
1096
1151
  objectType: action.objectType,
1097
1152
  name: action.propertyName,
1098
1153
  label: propertyLabel(action.propertyName),
1099
1154
  type: action.declaredType,
1100
1155
  ...(fieldType ? { fieldType } : {}),
1101
- });
1102
- applied.push(`create-property:${action.objectType}.${action.propertyName}`);
1156
+ }));
1103
1157
  continue;
1104
1158
  }
1105
1159
  if (action.kind === 'will-alter-property') {
1106
1160
  const fieldType = defaultHubSpotFieldType(action.declaredType);
1107
- await client.schemaManagement.updateProperty({
1161
+ await apply(`alter-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.updateProperty({
1108
1162
  objectType: action.objectType,
1109
1163
  propertyName: action.propertyName,
1110
1164
  type: action.declaredType,
1111
1165
  ...(fieldType ? { fieldType } : {}),
1112
- });
1113
- applied.push(`alter-property:${action.objectType}.${action.propertyName}`);
1166
+ }));
1114
1167
  }
1115
1168
  }
1116
1169
  return {
@@ -1119,6 +1172,198 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1119
1172
  skipped,
1120
1173
  };
1121
1174
  }
1175
+ /** A portal-schema apply action failed against HubSpot. */
1176
+ class PortalSchemaApplyError extends Error {
1177
+ action;
1178
+ objectType;
1179
+ /** Actions that DID apply before the failure (partial-apply visibility). */
1180
+ applied;
1181
+ cause;
1182
+ constructor(input) {
1183
+ super(input.cause instanceof Error ? input.cause.message : String(input.cause));
1184
+ this.name = 'PortalSchemaApplyError';
1185
+ this.action = input.action;
1186
+ this.objectType = input.objectType;
1187
+ this.applied = input.applied;
1188
+ this.cause = input.cause;
1189
+ }
1190
+ }
1191
+ /**
1192
+ * The raw HubSpot response body off a failed schema call. local-dev-lib wraps
1193
+ * axios failures in `HubSpotHttpError` and lifts `response.data` to `.data`;
1194
+ * a raw axios error keeps it at `.cause.response.data`.
1195
+ */
1196
+ function hubSpotErrorBody(error) {
1197
+ if (!isRecord(error))
1198
+ return undefined;
1199
+ if (error.data !== undefined)
1200
+ return error.data;
1201
+ const cause = error.cause;
1202
+ if (isRecord(cause) && isRecord(cause.response))
1203
+ return cause.response.data;
1204
+ return undefined;
1205
+ }
1206
+ /**
1207
+ * Extract HubSpot's MISSING_SCOPES payload. Captured live (cold-stranger run #3
1208
+ * repro — POST crm/v3/properties/contacts with a PAK lacking the write scope):
1209
+ *
1210
+ * { "status": "error",
1211
+ * "message": "This app hasn't been granted all required scopes to make this call. …",
1212
+ * "category": "MISSING_SCOPES",
1213
+ * "correlationId": "…",
1214
+ * "errors": [{ "message": "One or more of the following scopes are required.",
1215
+ * "context": { "requiredGranularScopes": ["crm.schemas.contacts.write", …] } }] }
1216
+ *
1217
+ * The list is ANY-OF (any one scope grants the call). Older error shapes use
1218
+ * `requiredScopes` (the only key local-dev-lib knows — which is why its own
1219
+ * message renders an EMPTY scope list for granular-scope errors).
1220
+ * Returns undefined when the error is not a scope error.
1221
+ */
1222
+ function hubSpotMissingScopes(error) {
1223
+ const body = hubSpotErrorBody(error);
1224
+ if (!isRecord(body))
1225
+ return undefined;
1226
+ const scopes = [];
1227
+ if (Array.isArray(body.errors)) {
1228
+ for (const entry of body.errors) {
1229
+ if (!isRecord(entry) || !isRecord(entry.context))
1230
+ continue;
1231
+ for (const key of ['requiredGranularScopes', 'requiredScopes']) {
1232
+ const list = entry.context[key];
1233
+ if (!Array.isArray(list))
1234
+ continue;
1235
+ for (const scope of list) {
1236
+ if (typeof scope === 'string')
1237
+ scopes.push(scope);
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ if (scopes.length === 0 && body.category !== 'MISSING_SCOPES')
1243
+ return undefined;
1244
+ return {
1245
+ scopes: [...new Set(scopes)],
1246
+ ...(typeof body.correlationId === 'string' ? { correlationId: body.correlationId } : {}),
1247
+ };
1248
+ }
1249
+ // Object types with a dedicated property-settings write scope
1250
+ // (crm.schemas.<type>.write — "create, delete, or make changes to property
1251
+ // settings"; crm.objects.<type>.write is RECORD writes and does not grant
1252
+ // property creation). Everything else falls under crm.schemas.custom.write.
1253
+ const HUBSPOT_SCHEMA_SCOPE_OBJECTS = new Set([
1254
+ 'appointments',
1255
+ 'carts',
1256
+ 'commercepayments',
1257
+ 'companies',
1258
+ 'contacts',
1259
+ 'courses',
1260
+ 'deals',
1261
+ 'invoices',
1262
+ 'line_items',
1263
+ 'listings',
1264
+ 'orders',
1265
+ 'quotes',
1266
+ 'services',
1267
+ 'subscriptions',
1268
+ 'tickets',
1269
+ 'users',
1270
+ ]);
1271
+ /** The property-definition write scope `--apply-schema` needs for an object type. */
1272
+ function propertyWriteScopeFor(objectType) {
1273
+ return HUBSPOT_SCHEMA_SCOPE_OBJECTS.has(objectType)
1274
+ ? `crm.schemas.${objectType}.write`
1275
+ : 'crm.schemas.custom.write';
1276
+ }
1277
+ /**
1278
+ * Narrow HubSpot's any-of scope list to what actually unblocks THIS apply:
1279
+ * the schemas-write scope for the target object when present, else every scope
1280
+ * that names the object, else the full response list (else the doc-grounded
1281
+ * default for the object type when the response carried none).
1282
+ */
1283
+ function relevantMissingScopes(all, objectType) {
1284
+ const expected = propertyWriteScopeFor(objectType);
1285
+ if (all.includes(expected))
1286
+ return [expected];
1287
+ const mentioning = all.filter((scope) => scope.includes(`.${objectType}.`));
1288
+ if (mentioning.length > 0)
1289
+ return mentioning;
1290
+ return all.length > 0 ? all : [expected];
1291
+ }
1292
+ const HUBSPOT_PAK_URL = 'https://app.hubspot.com/l/personal-access-key';
1293
+ /**
1294
+ * Curated rendering for a failed `--apply-schema` leg (cold-stranger run #3,
1295
+ * finding A): the phase + PLAN output still prints, partial progress is shown,
1296
+ * and a HubSpot MISSING_SCOPES response becomes HSX_E_DEPLOY_SCHEMA_SCOPES
1297
+ * naming the exact scopes to grant — never a raw truncated HubSpot error.
1298
+ */
1299
+ function portalSchemaApplyFailure(input) {
1300
+ const applyError = input.error instanceof PortalSchemaApplyError ? input.error : undefined;
1301
+ const cause = applyError?.cause ?? input.error;
1302
+ const scopeError = hubSpotMissingScopes(cause);
1303
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
1304
+ const hubSpotDetail = `${causeMessage.split('\n')[0]}${scopeError?.correlationId ? ` (correlationId ${scopeError.correlationId})` : ''}`;
1305
+ let code;
1306
+ let message;
1307
+ let hint;
1308
+ let missingScopes = [];
1309
+ if (scopeError && applyError) {
1310
+ code = 'HSX_E_DEPLOY_SCHEMA_SCOPES';
1311
+ missingScopes = relevantMissingScopes(scopeError.scopes, applyError.objectType);
1312
+ const scopesText = missingScopes.length === 1
1313
+ ? `the ${missingScopes[0]} scope`
1314
+ : `one of these scopes: ${missingScopes.join(', ')}`;
1315
+ message = `HubSpot rejected ${applyError.action}: your personal access key cannot write ${applyError.objectType} property definitions. Grant ${scopesText}.`;
1316
+ hint = `Edit your personal access key at ${HUBSPOT_PAK_URL} and add ${missingScopes.length === 1 ? missingScopes[0] : 'one of the scopes above'}, then re-run. To preview without applying: hs-x deploy --plan --portal-schema-live.`;
1317
+ }
1318
+ else {
1319
+ code = 'HSX_E_DEPLOY_SCHEMA_APPLY';
1320
+ message = applyError
1321
+ ? `Portal schema apply failed at ${applyError.action}: ${causeMessage.split('\n')[0]}`
1322
+ : causeMessage;
1323
+ hint = 'Preview the schema plan without applying: hs-x deploy --plan --portal-schema-live.';
1324
+ }
1325
+ const exitCode = code === 'HSX_E_DEPLOY_SCHEMA_SCOPES' ? 10 : 1;
1326
+ if (input.json) {
1327
+ write(`${JSON.stringify({
1328
+ schema_version: 1,
1329
+ command: 'deploy',
1330
+ ok: false,
1331
+ ...(input.portalSchemaPlan ? { portalSchemaPlan: input.portalSchemaPlan } : {}),
1332
+ ...(applyError && applyError.applied.length > 0
1333
+ ? { portalSchemaApplied: applyError.applied }
1334
+ : {}),
1335
+ error: {
1336
+ code,
1337
+ message,
1338
+ hint,
1339
+ ...(missingScopes.length > 0 ? { missing_scopes: missingScopes } : {}),
1340
+ detail: hubSpotDetail,
1341
+ },
1342
+ }, null, 2)}\n`);
1343
+ return { exitCode };
1344
+ }
1345
+ const reporter = createReporter({ command: 'deploy', argv: input.argv, startedAt: input.startedAt });
1346
+ reporter.header(basename(input.root));
1347
+ const capCount = input.workers.reduce((count, worker) => count + worker.capabilities.length, 0);
1348
+ reporter
1349
+ .step('Validating project')
1350
+ .ok(`${input.workers.length} workers, ${capCount} capabilities`);
1351
+ if (input.portalSchemaPlan) {
1352
+ reporter.info(renderPortalSchemaPlan(input.portalSchemaPlan).trimEnd());
1353
+ }
1354
+ if (applyError && applyError.applied.length > 0) {
1355
+ for (const done of applyError.applied)
1356
+ reporter.info(`APPLIED ${done}`);
1357
+ }
1358
+ reporter.step('Applying portal schema').fail(applyError?.action);
1359
+ reporter.error(code, message, {
1360
+ hint,
1361
+ cause: hubSpotDetail,
1362
+ docs_url: `https://hs-x.dev/errors/${code}`,
1363
+ });
1364
+ reporter.done(undefined, exitCode);
1365
+ return { exitCode };
1366
+ }
1122
1367
  function renderPortalSchemaPlan(plan) {
1123
1368
  if (plan.items.length === 0) {
1124
1369
  return 'Portal schema: no changes or warnings.\n';
@@ -1520,6 +1765,10 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
1520
1765
  process.stderr.write(`warning: could not register the tenant Worker URL with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
1521
1766
  }
1522
1767
  }
1768
+ // Without the client secret on the Worker, the runtime accepts UNSIGNED
1769
+ // workflow-action invokes (HubSpot must still be able to call them pre-OAuth)
1770
+ // — surface that as a one-line warning instead of leaving it silent.
1771
+ const hasWorkflowActions = worker.capabilities.some((capability) => capability.kind === 'tool');
1523
1772
  return {
1524
1773
  workerName,
1525
1774
  workerSourcePath: relative(root, workerSourcePath),
@@ -1530,6 +1779,9 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
1530
1779
  dryRun,
1531
1780
  stdout: output.stdout,
1532
1781
  stderr: output.stderr,
1782
+ ...(hasWorkflowActions && !resolvedHubSpotClientSecret && !dryRun
1783
+ ? { unauthenticatedInvoke: true }
1784
+ : {}),
1533
1785
  };
1534
1786
  }
1535
1787
  async function discoverWorkerSourcePaths(root) {
@@ -2351,6 +2603,14 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
2351
2603
  : 0;
2352
2604
  if (appId)
2353
2605
  await persistHubSpotAppBinding(root, { appId, projectName, developerAccountId });
2606
+ // Build SUCCESS does NOT mean the app shipped: HubSpot's auto-deploy can
2607
+ // still fail (found live: "Apps are not allowed to have 0 redirect URLs"
2608
+ // failed deploy #4 while the CLI reported green). Verify the deploy too.
2609
+ // Auto-deploy fires on uploads too — verify it regardless of uploadOnly
2610
+ // (the f1-data 0-redirect failure rode exactly this path).
2611
+ const autoDeployStatus = autoDeployEnabled && readStatus(buildStatus) === 'SUCCESS'
2612
+ ? await waitForHubSpotAutoDeploy({ client, projectName, buildStatus })
2613
+ : undefined;
2354
2614
  return {
2355
2615
  projectName,
2356
2616
  archivePath,
@@ -2360,7 +2620,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
2360
2620
  buildId: upload.buildId,
2361
2621
  deployId: undefined,
2362
2622
  buildStatus,
2363
- deployStatus: undefined,
2623
+ deployStatus: autoDeployStatus,
2364
2624
  deploySkipped: Boolean(uploadOnly || autoDeployEnabled),
2365
2625
  local,
2366
2626
  };
@@ -2401,6 +2661,40 @@ async function optionalHubSpotAppId(client, projectName, appUid) {
2401
2661
  return 0;
2402
2662
  }
2403
2663
  }
2664
+ /**
2665
+ * Poll the auto-deploy triggered by a successful build until it reaches a
2666
+ * terminal state. The build status carries the deploy task locator
2667
+ * (deployStatusTaskLocator.id); absent/transient lookups resolve undefined —
2668
+ * the caller treats that as "could not verify", never as success.
2669
+ */
2670
+ async function waitForHubSpotAutoDeploy({ client, projectName, buildStatus, timeoutMs = 60_000, intervalMs = 2_000, }) {
2671
+ const locator = buildStatus && typeof buildStatus === 'object'
2672
+ ? buildStatus.deployStatusTaskLocator
2673
+ : undefined;
2674
+ const deployId = locator && typeof locator.id === 'number'
2675
+ ? locator.id
2676
+ : locator && typeof locator.id === 'string'
2677
+ ? Number.parseInt(locator.id, 10)
2678
+ : undefined;
2679
+ if (deployId === undefined || Number.isNaN(deployId))
2680
+ return undefined;
2681
+ const deadline = Date.now() + timeoutMs;
2682
+ let last;
2683
+ while (Date.now() < deadline) {
2684
+ try {
2685
+ last = await client.projects.getDeployStatus({ projectName, deployId });
2686
+ }
2687
+ catch {
2688
+ last = undefined;
2689
+ }
2690
+ const status = readStatus(last);
2691
+ if (status && status !== 'PENDING' && status !== 'BUILDING' && status !== 'DEPLOYING') {
2692
+ return last;
2693
+ }
2694
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
2695
+ }
2696
+ return last;
2697
+ }
2404
2698
  async function waitForHubSpotBuildStatus({ client, projectName, buildId, timeoutMs, intervalMs, }) {
2405
2699
  const startedAt = Date.now();
2406
2700
  let latest;