@hs-x/cli 0.2.4 → 0.2.6

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 (68) 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-auth.d.ts.map +1 -1
  8. package/dist/cloudflare-auth.js +7 -0
  9. package/dist/cloudflare-auth.js.map +1 -1
  10. package/dist/cloudflare-oauth.d.ts.map +1 -1
  11. package/dist/cloudflare-oauth.js +11 -10
  12. package/dist/cloudflare-oauth.js.map +1 -1
  13. package/dist/commands/account.d.ts +6 -6
  14. package/dist/commands/api.d.ts +1 -1
  15. package/dist/commands/connect.d.ts +2 -0
  16. package/dist/commands/connect.d.ts.map +1 -1
  17. package/dist/commands/connect.js +103 -178
  18. package/dist/commands/connect.js.map +1 -1
  19. package/dist/commands/control-plane-read.d.ts +60 -9
  20. package/dist/commands/control-plane-read.d.ts.map +1 -1
  21. package/dist/commands/control-plane-read.js +222 -26
  22. package/dist/commands/control-plane-read.js.map +1 -1
  23. package/dist/commands/deploy-promote.d.ts +1 -1
  24. package/dist/commands/deploy.d.ts +6 -0
  25. package/dist/commands/deploy.d.ts.map +1 -1
  26. package/dist/commands/deploy.js +326 -18
  27. package/dist/commands/deploy.js.map +1 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +22 -6
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/doctor.d.ts +1 -1
  32. package/dist/commands/flags.d.ts +1 -1
  33. package/dist/commands/help-command.d.ts.map +1 -1
  34. package/dist/commands/help-command.js +37 -16
  35. package/dist/commands/help-command.js.map +1 -1
  36. package/dist/commands/init.d.ts +2 -2
  37. package/dist/commands/link.d.ts +1 -1
  38. package/dist/commands/link.d.ts.map +1 -1
  39. package/dist/commands/link.js +83 -0
  40. package/dist/commands/link.js.map +1 -1
  41. package/dist/commands/login.d.ts +2 -2
  42. package/dist/commands/migrate.d.ts +17 -17
  43. package/dist/commands/migrate.d.ts.map +1 -1
  44. package/dist/commands/migrate.js +119 -11
  45. package/dist/commands/migrate.js.map +1 -1
  46. package/dist/commands/rollback.d.ts +1 -1
  47. package/dist/commands/secrets.d.ts +2 -2
  48. package/dist/commands/status.d.ts +7 -1
  49. package/dist/commands/status.d.ts.map +1 -1
  50. package/dist/commands/status.js +45 -2
  51. package/dist/commands/status.js.map +1 -1
  52. package/dist/commands/unlink.d.ts +1 -1
  53. package/dist/commands/validate.d.ts +2 -2
  54. package/dist/constants.d.ts +1 -1
  55. package/dist/constants.js +1 -1
  56. package/dist/dev/invoke.d.ts +26 -0
  57. package/dist/dev/invoke.d.ts.map +1 -1
  58. package/dist/dev/invoke.js +155 -7
  59. package/dist/dev/invoke.js.map +1 -1
  60. package/dist/errors-registry.d.ts.map +1 -1
  61. package/dist/errors-registry.js +33 -4
  62. package/dist/errors-registry.js.map +1 -1
  63. package/dist/project-binding.d.ts +7 -1
  64. package/dist/project-binding.d.ts.map +1 -1
  65. package/dist/project-binding.js +10 -5
  66. package/dist/project-binding.js.map +1 -1
  67. package/dist/services/live.d.ts +1 -1
  68. package/package.json +9 -7
@@ -351,7 +351,7 @@ export async function deployCommand({ argv, root, json, }) {
351
351
  const applySchema = argv.includes('--apply-schema');
352
352
  const heartbeatFlag = argv.includes('--heartbeat');
353
353
  const noHeartbeat = argv.includes('--no-heartbeat');
354
- const shouldRecordControlPlane = recordLocal || promoteWhenHealthy;
354
+ const noRecord = argv.includes('--no-record');
355
355
  const explicitControlPlaneUrl = resolveFlag(argv, '--control-plane-url');
356
356
  const controlPlaneUrl = explicitControlPlaneUrl ?? (planOnly ? undefined : process.env.HSX_CONTROL_PLANE_URL);
357
357
  const linked = await isLinked();
@@ -389,6 +389,20 @@ export async function deployCommand({ argv, root, json, }) {
389
389
  const cloudflareDeployRequested = !buildOnly &&
390
390
  (explicitCloudflareDeployRequested || (!planOnly && needsRuntime && !explicitHubspotUpload));
391
391
  const hubspotUploadRequested = !buildOnly && (explicitHubspotUpload || (!planOnly && !cloudflareDryRun && Boolean(projectId)));
392
+ // Hosted control-plane deploys record by default: shipping code without
393
+ // recording the revision guarantees `unknown_code` drift on the next
394
+ // attestation (the worker attests a deploy id the control plane has no
395
+ // record of) and `hs-x list` keeps saying "no recorded deploys".
396
+ // `--no-record` opts out; `--record-local` / `--promote-when-healthy`
397
+ // force recording regardless (incl. against `--local-control-plane`).
398
+ const shouldRecordControlPlane = recordLocal ||
399
+ promoteWhenHealthy ||
400
+ (!noRecord &&
401
+ !planOnly &&
402
+ !buildOnly &&
403
+ !cloudflareDryRun &&
404
+ Boolean(controlPlaneUrl) &&
405
+ (cloudflareDeployRequested || hubspotUploadRequested));
392
406
  const portalSchemaRead = await readPortalSchema(argv, workers);
393
407
  const portalSchemaPlan = portalSchemaRead
394
408
  ? planPortalSchemaManagement({ workers, observed: portalSchemaRead.observed })
@@ -412,6 +426,11 @@ export async function deployCommand({ argv, root, json, }) {
412
426
  projectId = answer;
413
427
  }
414
428
  }
429
+ if (projectId && accountId && !resolvedAcct.overrodeBinding) {
430
+ // Sticky binding, project half: project-scoped reads (status/logs/routes/
431
+ // drift) resolve their project id from here when no flag is passed.
432
+ await writeProjectBinding(root, accountId, projectId).catch(() => { });
433
+ }
415
434
  if (!json && willChangeAnything && !flagYes && isInteractive()) {
416
435
  const summary = buildDeployPlanSummary({
417
436
  accountId,
@@ -437,13 +456,31 @@ export async function deployCommand({ argv, root, json, }) {
437
456
  return cancelledResult(argv, 'deploy');
438
457
  }
439
458
  }
440
- const portalSchemaApply = applySchema
441
- ? await applyPortalSchemaPlan({
442
- argv,
443
- plan: portalSchemaPlan,
444
- source: portalSchemaRead?.source,
445
- })
446
- : undefined;
459
+ // The schema apply is the first phase that touches the portal. A failure here
460
+ // used to throw a raw HubSpot error out of the command — no HSX_E code, no
461
+ // phase/PLAN output (cold-stranger run #3, finding A). Render the curated
462
+ // failure WITH the plan that was being applied instead.
463
+ let portalSchemaApply;
464
+ if (applySchema) {
465
+ try {
466
+ portalSchemaApply = await applyPortalSchemaPlan({
467
+ argv,
468
+ plan: portalSchemaPlan,
469
+ source: portalSchemaRead?.source,
470
+ });
471
+ }
472
+ catch (error) {
473
+ return portalSchemaApplyFailure({
474
+ argv,
475
+ json,
476
+ root,
477
+ startedAt: commandStartedAt,
478
+ workers,
479
+ portalSchemaPlan,
480
+ error,
481
+ });
482
+ }
483
+ }
447
484
  const manifest = await generateProjectArtifacts({
448
485
  root,
449
486
  workers,
@@ -906,6 +943,11 @@ function renderDeployHuman(input) {
906
943
  .map((worker) => `${worker.workerName}${worker.dryRun ? ' (dry-run)' : ''}`)
907
944
  .join(', ');
908
945
  reporter.info(`Cloudflare deploy: ${deployed}`);
946
+ for (const worker of input.cloudflareDeployResult.workers) {
947
+ if (worker.unauthenticatedInvoke) {
948
+ 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).`);
949
+ }
950
+ }
909
951
  }
910
952
  if (!input.linked && input.cloudflareDeployResult) {
911
953
  reporter.info(`Deploy history for this machine: ${machineDashboardUrl(input.machineId)}`);
@@ -927,6 +969,22 @@ function renderDeployHuman(input) {
927
969
  if (upload.deployId !== undefined) {
928
970
  reporter.info(`HubSpot deploy id: ${upload.deployId}`);
929
971
  }
972
+ else if (hubspotBuildStatus === 'SUCCESS' &&
973
+ upload.deploySkipped &&
974
+ upload.deployStatus !== undefined) {
975
+ const autoStatus = readStatus(upload.deployStatus) ?? 'UNKNOWN';
976
+ if (autoStatus === 'SUCCESS') {
977
+ reporter.info(`HubSpot deploy: auto-deploy SUCCESS (build #${upload.buildId})`);
978
+ }
979
+ else {
980
+ 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}).`);
981
+ for (const subbuild of readSubbuildStatuses(upload.deployStatus)) {
982
+ if (subbuild.status !== 'SUCCESS') {
983
+ reporter.info(` [x] ${subbuild.name}: ${subbuild.errorMessage ?? subbuild.status}`);
984
+ }
985
+ }
986
+ }
987
+ }
930
988
  else if (hubspotBuildStatus === 'SUCCESS' && upload.deploySkipped) {
931
989
  reporter.info('HubSpot deploy: skipped (upload-only or auto-deploy handles it)');
932
990
  }
@@ -1077,13 +1135,24 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1077
1135
  });
1078
1136
  const applied = [];
1079
1137
  const skipped = [];
1138
+ // Each HubSpot write is wrapped so a failure carries WHICH action failed, the
1139
+ // target object type (scope errors are per-object), and what already applied
1140
+ // before it — the curated failure renderer needs all three.
1141
+ const apply = async (label, objectType, run) => {
1142
+ try {
1143
+ await run();
1144
+ }
1145
+ catch (cause) {
1146
+ throw new PortalSchemaApplyError({ action: label, objectType, applied: [...applied], cause });
1147
+ }
1148
+ applied.push(label);
1149
+ };
1080
1150
  for (const action of plan.actions) {
1081
1151
  if (action.kind === 'will-create-object') {
1082
- await client.schemaManagement.createSchema({
1152
+ await apply(`create-object:${action.objectType}`, action.objectType, () => client.schemaManagement.createSchema({
1083
1153
  name: action.objectType,
1084
1154
  labels: portalObjectLabels(action.objectType),
1085
- });
1086
- applied.push(`create-object:${action.objectType}`);
1155
+ }));
1087
1156
  continue;
1088
1157
  }
1089
1158
  if (!action.propertyName || !action.declaredType) {
@@ -1092,25 +1161,23 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1092
1161
  }
1093
1162
  if (action.kind === 'will-create-property') {
1094
1163
  const fieldType = defaultHubSpotFieldType(action.declaredType);
1095
- await client.schemaManagement.createProperty({
1164
+ await apply(`create-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.createProperty({
1096
1165
  objectType: action.objectType,
1097
1166
  name: action.propertyName,
1098
1167
  label: propertyLabel(action.propertyName),
1099
1168
  type: action.declaredType,
1100
1169
  ...(fieldType ? { fieldType } : {}),
1101
- });
1102
- applied.push(`create-property:${action.objectType}.${action.propertyName}`);
1170
+ }));
1103
1171
  continue;
1104
1172
  }
1105
1173
  if (action.kind === 'will-alter-property') {
1106
1174
  const fieldType = defaultHubSpotFieldType(action.declaredType);
1107
- await client.schemaManagement.updateProperty({
1175
+ await apply(`alter-property:${action.objectType}.${action.propertyName}`, action.objectType, () => client.schemaManagement.updateProperty({
1108
1176
  objectType: action.objectType,
1109
1177
  propertyName: action.propertyName,
1110
1178
  type: action.declaredType,
1111
1179
  ...(fieldType ? { fieldType } : {}),
1112
- });
1113
- applied.push(`alter-property:${action.objectType}.${action.propertyName}`);
1180
+ }));
1114
1181
  }
1115
1182
  }
1116
1183
  return {
@@ -1119,6 +1186,198 @@ async function applyPortalSchemaPlan({ argv, plan, source, }) {
1119
1186
  skipped,
1120
1187
  };
1121
1188
  }
1189
+ /** A portal-schema apply action failed against HubSpot. */
1190
+ class PortalSchemaApplyError extends Error {
1191
+ action;
1192
+ objectType;
1193
+ /** Actions that DID apply before the failure (partial-apply visibility). */
1194
+ applied;
1195
+ cause;
1196
+ constructor(input) {
1197
+ super(input.cause instanceof Error ? input.cause.message : String(input.cause));
1198
+ this.name = 'PortalSchemaApplyError';
1199
+ this.action = input.action;
1200
+ this.objectType = input.objectType;
1201
+ this.applied = input.applied;
1202
+ this.cause = input.cause;
1203
+ }
1204
+ }
1205
+ /**
1206
+ * The raw HubSpot response body off a failed schema call. local-dev-lib wraps
1207
+ * axios failures in `HubSpotHttpError` and lifts `response.data` to `.data`;
1208
+ * a raw axios error keeps it at `.cause.response.data`.
1209
+ */
1210
+ function hubSpotErrorBody(error) {
1211
+ if (!isRecord(error))
1212
+ return undefined;
1213
+ if (error.data !== undefined)
1214
+ return error.data;
1215
+ const cause = error.cause;
1216
+ if (isRecord(cause) && isRecord(cause.response))
1217
+ return cause.response.data;
1218
+ return undefined;
1219
+ }
1220
+ /**
1221
+ * Extract HubSpot's MISSING_SCOPES payload. Captured live (cold-stranger run #3
1222
+ * repro — POST crm/v3/properties/contacts with a PAK lacking the write scope):
1223
+ *
1224
+ * { "status": "error",
1225
+ * "message": "This app hasn't been granted all required scopes to make this call. …",
1226
+ * "category": "MISSING_SCOPES",
1227
+ * "correlationId": "…",
1228
+ * "errors": [{ "message": "One or more of the following scopes are required.",
1229
+ * "context": { "requiredGranularScopes": ["crm.schemas.contacts.write", …] } }] }
1230
+ *
1231
+ * The list is ANY-OF (any one scope grants the call). Older error shapes use
1232
+ * `requiredScopes` (the only key local-dev-lib knows — which is why its own
1233
+ * message renders an EMPTY scope list for granular-scope errors).
1234
+ * Returns undefined when the error is not a scope error.
1235
+ */
1236
+ function hubSpotMissingScopes(error) {
1237
+ const body = hubSpotErrorBody(error);
1238
+ if (!isRecord(body))
1239
+ return undefined;
1240
+ const scopes = [];
1241
+ if (Array.isArray(body.errors)) {
1242
+ for (const entry of body.errors) {
1243
+ if (!isRecord(entry) || !isRecord(entry.context))
1244
+ continue;
1245
+ for (const key of ['requiredGranularScopes', 'requiredScopes']) {
1246
+ const list = entry.context[key];
1247
+ if (!Array.isArray(list))
1248
+ continue;
1249
+ for (const scope of list) {
1250
+ if (typeof scope === 'string')
1251
+ scopes.push(scope);
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+ if (scopes.length === 0 && body.category !== 'MISSING_SCOPES')
1257
+ return undefined;
1258
+ return {
1259
+ scopes: [...new Set(scopes)],
1260
+ ...(typeof body.correlationId === 'string' ? { correlationId: body.correlationId } : {}),
1261
+ };
1262
+ }
1263
+ // Object types with a dedicated property-settings write scope
1264
+ // (crm.schemas.<type>.write — "create, delete, or make changes to property
1265
+ // settings"; crm.objects.<type>.write is RECORD writes and does not grant
1266
+ // property creation). Everything else falls under crm.schemas.custom.write.
1267
+ const HUBSPOT_SCHEMA_SCOPE_OBJECTS = new Set([
1268
+ 'appointments',
1269
+ 'carts',
1270
+ 'commercepayments',
1271
+ 'companies',
1272
+ 'contacts',
1273
+ 'courses',
1274
+ 'deals',
1275
+ 'invoices',
1276
+ 'line_items',
1277
+ 'listings',
1278
+ 'orders',
1279
+ 'quotes',
1280
+ 'services',
1281
+ 'subscriptions',
1282
+ 'tickets',
1283
+ 'users',
1284
+ ]);
1285
+ /** The property-definition write scope `--apply-schema` needs for an object type. */
1286
+ function propertyWriteScopeFor(objectType) {
1287
+ return HUBSPOT_SCHEMA_SCOPE_OBJECTS.has(objectType)
1288
+ ? `crm.schemas.${objectType}.write`
1289
+ : 'crm.schemas.custom.write';
1290
+ }
1291
+ /**
1292
+ * Narrow HubSpot's any-of scope list to what actually unblocks THIS apply:
1293
+ * the schemas-write scope for the target object when present, else every scope
1294
+ * that names the object, else the full response list (else the doc-grounded
1295
+ * default for the object type when the response carried none).
1296
+ */
1297
+ function relevantMissingScopes(all, objectType) {
1298
+ const expected = propertyWriteScopeFor(objectType);
1299
+ if (all.includes(expected))
1300
+ return [expected];
1301
+ const mentioning = all.filter((scope) => scope.includes(`.${objectType}.`));
1302
+ if (mentioning.length > 0)
1303
+ return mentioning;
1304
+ return all.length > 0 ? all : [expected];
1305
+ }
1306
+ const HUBSPOT_PAK_URL = 'https://app.hubspot.com/l/personal-access-key';
1307
+ /**
1308
+ * Curated rendering for a failed `--apply-schema` leg (cold-stranger run #3,
1309
+ * finding A): the phase + PLAN output still prints, partial progress is shown,
1310
+ * and a HubSpot MISSING_SCOPES response becomes HSX_E_DEPLOY_SCHEMA_SCOPES
1311
+ * naming the exact scopes to grant — never a raw truncated HubSpot error.
1312
+ */
1313
+ function portalSchemaApplyFailure(input) {
1314
+ const applyError = input.error instanceof PortalSchemaApplyError ? input.error : undefined;
1315
+ const cause = applyError?.cause ?? input.error;
1316
+ const scopeError = hubSpotMissingScopes(cause);
1317
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
1318
+ const hubSpotDetail = `${causeMessage.split('\n')[0]}${scopeError?.correlationId ? ` (correlationId ${scopeError.correlationId})` : ''}`;
1319
+ let code;
1320
+ let message;
1321
+ let hint;
1322
+ let missingScopes = [];
1323
+ if (scopeError && applyError) {
1324
+ code = 'HSX_E_DEPLOY_SCHEMA_SCOPES';
1325
+ missingScopes = relevantMissingScopes(scopeError.scopes, applyError.objectType);
1326
+ const scopesText = missingScopes.length === 1
1327
+ ? `the ${missingScopes[0]} scope`
1328
+ : `one of these scopes: ${missingScopes.join(', ')}`;
1329
+ message = `HubSpot rejected ${applyError.action}: your personal access key cannot write ${applyError.objectType} property definitions. Grant ${scopesText}.`;
1330
+ 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.`;
1331
+ }
1332
+ else {
1333
+ code = 'HSX_E_DEPLOY_SCHEMA_APPLY';
1334
+ message = applyError
1335
+ ? `Portal schema apply failed at ${applyError.action}: ${causeMessage.split('\n')[0]}`
1336
+ : causeMessage;
1337
+ hint = 'Preview the schema plan without applying: hs-x deploy --plan --portal-schema-live.';
1338
+ }
1339
+ const exitCode = code === 'HSX_E_DEPLOY_SCHEMA_SCOPES' ? 10 : 1;
1340
+ if (input.json) {
1341
+ write(`${JSON.stringify({
1342
+ schema_version: 1,
1343
+ command: 'deploy',
1344
+ ok: false,
1345
+ ...(input.portalSchemaPlan ? { portalSchemaPlan: input.portalSchemaPlan } : {}),
1346
+ ...(applyError && applyError.applied.length > 0
1347
+ ? { portalSchemaApplied: applyError.applied }
1348
+ : {}),
1349
+ error: {
1350
+ code,
1351
+ message,
1352
+ hint,
1353
+ ...(missingScopes.length > 0 ? { missing_scopes: missingScopes } : {}),
1354
+ detail: hubSpotDetail,
1355
+ },
1356
+ }, null, 2)}\n`);
1357
+ return { exitCode };
1358
+ }
1359
+ const reporter = createReporter({ command: 'deploy', argv: input.argv, startedAt: input.startedAt });
1360
+ reporter.header(basename(input.root));
1361
+ const capCount = input.workers.reduce((count, worker) => count + worker.capabilities.length, 0);
1362
+ reporter
1363
+ .step('Validating project')
1364
+ .ok(`${input.workers.length} workers, ${capCount} capabilities`);
1365
+ if (input.portalSchemaPlan) {
1366
+ reporter.info(renderPortalSchemaPlan(input.portalSchemaPlan).trimEnd());
1367
+ }
1368
+ if (applyError && applyError.applied.length > 0) {
1369
+ for (const done of applyError.applied)
1370
+ reporter.info(`APPLIED ${done}`);
1371
+ }
1372
+ reporter.step('Applying portal schema').fail(applyError?.action);
1373
+ reporter.error(code, message, {
1374
+ hint,
1375
+ cause: hubSpotDetail,
1376
+ docs_url: `https://hs-x.dev/errors/${code}`,
1377
+ });
1378
+ reporter.done(undefined, exitCode);
1379
+ return { exitCode };
1380
+ }
1122
1381
  function renderPortalSchemaPlan(plan) {
1123
1382
  if (plan.items.length === 0) {
1124
1383
  return 'Portal schema: no changes or warnings.\n';
@@ -1520,6 +1779,10 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
1520
1779
  process.stderr.write(`warning: could not register the tenant Worker URL with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
1521
1780
  }
1522
1781
  }
1782
+ // Without the client secret on the Worker, the runtime accepts UNSIGNED
1783
+ // workflow-action invokes (HubSpot must still be able to call them pre-OAuth)
1784
+ // — surface that as a one-line warning instead of leaving it silent.
1785
+ const hasWorkflowActions = worker.capabilities.some((capability) => capability.kind === 'tool');
1523
1786
  return {
1524
1787
  workerName,
1525
1788
  workerSourcePath: relative(root, workerSourcePath),
@@ -1530,6 +1793,9 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
1530
1793
  dryRun,
1531
1794
  stdout: output.stdout,
1532
1795
  stderr: output.stderr,
1796
+ ...(hasWorkflowActions && !resolvedHubSpotClientSecret && !dryRun
1797
+ ? { unauthenticatedInvoke: true }
1798
+ : {}),
1533
1799
  };
1534
1800
  }
1535
1801
  async function discoverWorkerSourcePaths(root) {
@@ -2351,6 +2617,14 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
2351
2617
  : 0;
2352
2618
  if (appId)
2353
2619
  await persistHubSpotAppBinding(root, { appId, projectName, developerAccountId });
2620
+ // Build SUCCESS does NOT mean the app shipped: HubSpot's auto-deploy can
2621
+ // still fail (found live: "Apps are not allowed to have 0 redirect URLs"
2622
+ // failed deploy #4 while the CLI reported green). Verify the deploy too.
2623
+ // Auto-deploy fires on uploads too — verify it regardless of uploadOnly
2624
+ // (the f1-data 0-redirect failure rode exactly this path).
2625
+ const autoDeployStatus = autoDeployEnabled && readStatus(buildStatus) === 'SUCCESS'
2626
+ ? await waitForHubSpotAutoDeploy({ client, projectName, buildStatus })
2627
+ : undefined;
2354
2628
  return {
2355
2629
  projectName,
2356
2630
  archivePath,
@@ -2360,7 +2634,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
2360
2634
  buildId: upload.buildId,
2361
2635
  deployId: undefined,
2362
2636
  buildStatus,
2363
- deployStatus: undefined,
2637
+ deployStatus: autoDeployStatus,
2364
2638
  deploySkipped: Boolean(uploadOnly || autoDeployEnabled),
2365
2639
  local,
2366
2640
  };
@@ -2401,6 +2675,40 @@ async function optionalHubSpotAppId(client, projectName, appUid) {
2401
2675
  return 0;
2402
2676
  }
2403
2677
  }
2678
+ /**
2679
+ * Poll the auto-deploy triggered by a successful build until it reaches a
2680
+ * terminal state. The build status carries the deploy task locator
2681
+ * (deployStatusTaskLocator.id); absent/transient lookups resolve undefined —
2682
+ * the caller treats that as "could not verify", never as success.
2683
+ */
2684
+ async function waitForHubSpotAutoDeploy({ client, projectName, buildStatus, timeoutMs = 60_000, intervalMs = 2_000, }) {
2685
+ const locator = buildStatus && typeof buildStatus === 'object'
2686
+ ? buildStatus.deployStatusTaskLocator
2687
+ : undefined;
2688
+ const deployId = locator && typeof locator.id === 'number'
2689
+ ? locator.id
2690
+ : locator && typeof locator.id === 'string'
2691
+ ? Number.parseInt(locator.id, 10)
2692
+ : undefined;
2693
+ if (deployId === undefined || Number.isNaN(deployId))
2694
+ return undefined;
2695
+ const deadline = Date.now() + timeoutMs;
2696
+ let last;
2697
+ while (Date.now() < deadline) {
2698
+ try {
2699
+ last = await client.projects.getDeployStatus({ projectName, deployId });
2700
+ }
2701
+ catch {
2702
+ last = undefined;
2703
+ }
2704
+ const status = readStatus(last);
2705
+ if (status && status !== 'PENDING' && status !== 'BUILDING' && status !== 'DEPLOYING') {
2706
+ return last;
2707
+ }
2708
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
2709
+ }
2710
+ return last;
2711
+ }
2404
2712
  async function waitForHubSpotBuildStatus({ client, projectName, buildId, timeoutMs, intervalMs, }) {
2405
2713
  const startedAt = Date.now();
2406
2714
  let latest;