@hs-x/cli 0.4.12 → 0.4.13

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 (109) hide show
  1. package/README.md +21 -7
  2. package/dist/commands/completion.d.ts.map +1 -1
  3. package/dist/commands/completion.js +3 -0
  4. package/dist/commands/completion.js.map +1 -1
  5. package/dist/commands/deploy.d.ts +10 -0
  6. package/dist/commands/deploy.d.ts.map +1 -1
  7. package/dist/commands/deploy.js +37 -13
  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 +368 -16
  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 +22 -6
  15. package/dist/commands/help-command.js.map +1 -1
  16. package/dist/commands/project-lifecycle.d.ts +10 -0
  17. package/dist/commands/project-lifecycle.d.ts.map +1 -1
  18. package/dist/commands/project-lifecycle.js +120 -33
  19. package/dist/commands/project-lifecycle.js.map +1 -1
  20. package/dist/commands/project.d.ts +3 -0
  21. package/dist/commands/project.d.ts.map +1 -1
  22. package/dist/commands/project.js +2 -0
  23. package/dist/commands/project.js.map +1 -1
  24. package/dist/constants.d.ts +1 -1
  25. package/dist/constants.js +1 -1
  26. package/dist/dev/account-selection.d.ts +55 -0
  27. package/dist/dev/account-selection.d.ts.map +1 -1
  28. package/dist/dev/account-selection.js +120 -0
  29. package/dist/dev/account-selection.js.map +1 -1
  30. package/dist/dev/browser-log-bridge.d.ts +13 -8
  31. package/dist/dev/browser-log-bridge.d.ts.map +1 -1
  32. package/dist/dev/browser-log-bridge.js +37 -17
  33. package/dist/dev/browser-log-bridge.js.map +1 -1
  34. package/dist/dev/clipboard.d.ts +9 -0
  35. package/dist/dev/clipboard.d.ts.map +1 -0
  36. package/dist/dev/clipboard.js +42 -0
  37. package/dist/dev/clipboard.js.map +1 -0
  38. package/dist/dev/compat-shim.d.ts +19 -4
  39. package/dist/dev/compat-shim.d.ts.map +1 -1
  40. package/dist/dev/compat-shim.js +31 -4
  41. package/dist/dev/compat-shim.js.map +1 -1
  42. package/dist/dev/component-selection.d.ts +11 -0
  43. package/dist/dev/component-selection.d.ts.map +1 -1
  44. package/dist/dev/component-selection.js +23 -0
  45. package/dist/dev/component-selection.js.map +1 -1
  46. package/dist/dev/frontend-log-relay.d.ts +12 -0
  47. package/dist/dev/frontend-log-relay.d.ts.map +1 -1
  48. package/dist/dev/frontend-log-relay.js +31 -20
  49. package/dist/dev/frontend-log-relay.js.map +1 -1
  50. package/dist/dev/hubspot-logs-poller.d.ts +7 -1
  51. package/dist/dev/hubspot-logs-poller.d.ts.map +1 -1
  52. package/dist/dev/hubspot-logs-poller.js +28 -1
  53. package/dist/dev/hubspot-logs-poller.js.map +1 -1
  54. package/dist/dev/keyboard.d.ts +21 -3
  55. package/dist/dev/keyboard.d.ts.map +1 -1
  56. package/dist/dev/keyboard.js +96 -4
  57. package/dist/dev/keyboard.js.map +1 -1
  58. package/dist/dev/local-proxy.d.ts +90 -0
  59. package/dist/dev/local-proxy.d.ts.map +1 -0
  60. package/dist/dev/local-proxy.js +186 -0
  61. package/dist/dev/local-proxy.js.map +1 -0
  62. package/dist/dev/log-server.d.ts +9 -0
  63. package/dist/dev/log-server.d.ts.map +1 -1
  64. package/dist/dev/log-server.js +46 -3
  65. package/dist/dev/log-server.js.map +1 -1
  66. package/dist/dev/production-app-check.d.ts +23 -0
  67. package/dist/dev/production-app-check.d.ts.map +1 -0
  68. package/dist/dev/production-app-check.js +28 -0
  69. package/dist/dev/production-app-check.js.map +1 -0
  70. package/dist/dev/session-manager.d.ts +49 -1
  71. package/dist/dev/session-manager.d.ts.map +1 -1
  72. package/dist/dev/session-manager.js +152 -12
  73. package/dist/dev/session-manager.js.map +1 -1
  74. package/dist/dev/stream-event-format.d.ts +10 -1
  75. package/dist/dev/stream-event-format.d.ts.map +1 -1
  76. package/dist/dev/stream-event-format.js +157 -5
  77. package/dist/dev/stream-event-format.js.map +1 -1
  78. package/dist/dev/stream-renderer.d.ts +34 -1
  79. package/dist/dev/stream-renderer.d.ts.map +1 -1
  80. package/dist/dev/stream-renderer.js +371 -60
  81. package/dist/dev/stream-renderer.js.map +1 -1
  82. package/dist/dev/trace-markdown.d.ts +25 -0
  83. package/dist/dev/trace-markdown.d.ts.map +1 -0
  84. package/dist/dev/trace-markdown.js +159 -0
  85. package/dist/dev/trace-markdown.js.map +1 -0
  86. package/dist/hubspot-app-logs.d.ts +9 -0
  87. package/dist/hubspot-app-logs.d.ts.map +1 -1
  88. package/dist/hubspot-app-logs.js +6 -4
  89. package/dist/hubspot-app-logs.js.map +1 -1
  90. package/dist/hubspot-developer-client.js +3 -0
  91. package/dist/hubspot-developer-client.js.map +1 -1
  92. package/dist/hubspot-project.d.ts +2 -0
  93. package/dist/hubspot-project.d.ts.map +1 -1
  94. package/dist/hubspot-project.js +13 -1
  95. package/dist/hubspot-project.js.map +1 -1
  96. package/dist/prompt.d.ts +6 -0
  97. package/dist/prompt.d.ts.map +1 -1
  98. package/dist/prompt.js +20 -0
  99. package/dist/prompt.js.map +1 -1
  100. package/dist/reporter/human.d.ts.map +1 -1
  101. package/dist/reporter/human.js +6 -1
  102. package/dist/reporter/human.js.map +1 -1
  103. package/dist/reporter/style.d.ts +10 -0
  104. package/dist/reporter/style.d.ts.map +1 -1
  105. package/dist/reporter/style.js +39 -0
  106. package/dist/reporter/style.js.map +1 -1
  107. package/dist/services/hubspot-dev-client.js +3 -0
  108. package/dist/services/hubspot-dev-client.js.map +1 -1
  109. package/package.json +8 -8
@@ -28,6 +28,7 @@ export declare const devCmd: Command.Command<"dev", Cwd, CliError, {
28
28
  readonly testingAccount: Option.Option<string>;
29
29
  readonly profile: Option.Option<string>;
30
30
  readonly preserveState: boolean;
31
+ readonly localProxy: "auto" | "always" | "never";
31
32
  readonly targetOrigin: Option.Option<string>;
32
33
  readonly telemetryOrigin: Option.Option<string>;
33
34
  readonly accountId: Option.Option<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAI/C,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,QAAQ,EAAY,MAAM,iBAAiB,CAAC;AA2C1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AA6zBzC,wBAAsB,UAAU,CAAC,EAC/B,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC9B,GAAG,OAAO,CAAC,SAAS,CAAC,CAuErB;AA+mFD,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmJlB,CAAC"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAI/C,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,QAAQ,EAAY,MAAM,iBAAiB,CAAC;AAwD1D,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAm9BzC,wBAAsB,UAAU,CAAC,EAC/B,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC9B,GAAG,OAAO,CAAC,SAAS,CAAC,CA0ErB;AA+yFD,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqJlB,CAAC"}
@@ -15,13 +15,14 @@ import { cwdOption, jsonOption, runHandler, runQuarantined } from '../cli/kit.js
15
15
  import { formatConfigUrl, loadCliConfig } from '../config.js';
16
16
  import { CLI_VERSION } from '../constants.js';
17
17
  import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
18
- import { selectHubSpotDirectDevAccounts } from '../dev/account-selection.js';
19
- import { listHubSpotDevComponentCandidates, resolveHubSpotDevComponentSelectors, resolveHubSpotDevSessionApp, } from '../dev/component-selection.js';
18
+ import { describeHubSpotDevTestingChoice, describeHubSpotDevTestingPlanReason, planHubSpotDevTestingAccount, selectHubSpotDirectDevAccounts, } from '../dev/account-selection.js';
19
+ import { findHubSpotDevApplication, isHubSpotMarketplaceApp, listHubSpotDevComponentCandidates, resolveHubSpotDevComponentSelectors, resolveHubSpotDevSessionApp, } from '../dev/component-selection.js';
20
+ import { resolveHubSpotLocalProxyMode } from '../dev/local-proxy.js';
20
21
  import { createManagedDevEventWriter, createManagedDevSession, markManagedDevSessionReady, markManagedDevSessionState, prepareManagedDevSessionRestart, publicManagedDevSession, readManagedDevEvents, readManagedDevSession, reconcileManagedDevSession, setManagedDevSessionPid, stopManagedDevSessionProcess, waitForManagedDevSessionOwner, } from '../dev/managed-session.js';
21
22
  import { findListeningProcess, isHubSpotDevPortConflict, stopProcessCommand, terminateProcessGracefully, } from '../dev/port-process.js';
22
23
  import { executeCliHttp } from '../effect-http.js';
23
24
  import { discoverHubSpotAccounts, exchangePak } from '../hubspot-auth.js';
24
- import { detectLocalProjectMode, validateHubSpotProject } from '../hubspot-project.js';
25
+ import { detectLocalProjectMode, findContainingProjectRoot, validateHubSpotProject, } from '../hubspot-project.js';
25
26
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
26
27
  import { requestMutationConsent } from '../mutation-consent.js';
27
28
  import { isMachineOutput } from '../output-context.js';
@@ -281,6 +282,7 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
281
282
  const reporter = createReporter({ command: 'dev', argv, entry: !json });
282
283
  const profile = resolveFlag(argv, '--profile');
283
284
  const preserveState = argv.includes('--preserve-state');
285
+ const yes = argv.some((argument) => argument === '--yes' || argument === '-y');
284
286
  const validation = await validateHubSpotProject({ root, ...(profile ? { profile } : {}) });
285
287
  if (!validation.ok) {
286
288
  const result = {
@@ -363,15 +365,62 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
363
365
  }
364
366
  return { exitCode: 1 };
365
367
  }
366
- const testingAccount = selectedTestingAccount ?? projectAccount;
368
+ // Mirror `hs project dev`: recommend a developer test account or sandbox,
369
+ // and never land on the production project account without saying so.
370
+ const testingPlan = planHubSpotDevTestingAccount({
371
+ accounts,
372
+ projectAccount,
373
+ // selectHubSpotDirectDevAccounts defaults the testing account to the
374
+ // project account; only an explicit flag counts as an answer here.
375
+ selectedTestingAccount: requestedTestingAccount ? selectedTestingAccount : undefined,
376
+ interactive: !json && !yes && isInteractive(),
377
+ });
378
+ let testingAccount;
379
+ let testingChoice;
380
+ if (testingPlan.kind === 'prompt') {
381
+ const picked = await promptSelect({
382
+ message: 'Choose the account to test on',
383
+ options: testingPlan.choices.map((choice) => ({
384
+ value: String(choice.account.portalId),
385
+ ...describeHubSpotDevTestingChoice(choice),
386
+ })),
387
+ default: String(testingPlan.choices[0]?.account.portalId),
388
+ });
389
+ if (picked === undefined)
390
+ return cancelledResult(argv, 'dev');
391
+ const choice = testingPlan.choices.find((item) => String(item.account.portalId) === picked);
392
+ if (!choice)
393
+ return cancelledResult(argv, 'dev');
394
+ testingAccount = choice.account;
395
+ testingChoice = choice.kind;
396
+ reporter.info(`Testing on ${testingAccount.name} (${testingAccount.portalId}) — ${describeTestingChoiceForRow(testingChoice)}`);
397
+ }
398
+ else {
399
+ testingAccount = testingPlan.account;
400
+ testingChoice = testingPlan.choice;
401
+ if (!json) {
402
+ const explanation = describeHubSpotDevTestingPlanReason(testingPlan);
403
+ if (explanation.level === 'warn') {
404
+ reporter.warn('HSX_W_DEV_TESTING_ON_PRODUCTION_ACCOUNT', explanation.message);
405
+ }
406
+ else {
407
+ reporter.info(explanation.message);
408
+ }
409
+ }
410
+ }
367
411
  if (testingAccount.personalAccessKey) {
412
+ const credentialStep = json
413
+ ? undefined
414
+ : reporter.step(`Checking ${testingAccount.name} (${testingAccount.portalId}) credentials`);
368
415
  try {
369
416
  const exchanged = await exchangePak(testingAccount.personalAccessKey);
370
417
  if (exchanged.portalId !== testingAccount.portalId) {
371
418
  throw new Error(`the configured PAK belongs to account ${exchanged.portalId}, not ${testingAccount.portalId}`);
372
419
  }
420
+ credentialStep?.ok();
373
421
  }
374
422
  catch (cause) {
423
+ credentialStep?.fail();
375
424
  const message = `HubSpot testing account ${testingAccount.name} (${testingAccount.portalId}) needs fresh authentication: ${cause instanceof Error ? cause.message : String(cause)} Run \`hs account auth\` for that account, then retry.`;
376
425
  if (json) {
377
426
  write(`${JSON.stringify({
@@ -394,12 +443,19 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
394
443
  const existingDevServer = await recoverHubSpotDevPort({ argv, json, reporter });
395
444
  if (existingDevServer.kind === 'exit')
396
445
  return existingDevServer.result;
446
+ const probeStep = json ? undefined : reporter.step('Reading HubSpot project components');
397
447
  const uiExtensionNodes = await probeHubSpotUiNodes({
398
448
  root,
399
449
  portalIdFlag: String(projectAccount.portalId),
400
450
  accountId: undefined,
401
451
  ...(profile ? { profile } : {}),
402
452
  });
453
+ if (uiExtensionNodes) {
454
+ probeStep?.ok(`${uiExtensionNodes.candidates.length} component${uiExtensionNodes.candidates.length === 1 ? '' : 's'}${uiExtensionNodes.application?.name ? ` · ${uiExtensionNodes.application.name}` : ''}`);
455
+ }
456
+ else {
457
+ probeStep?.warn('no UI-extension components found');
458
+ }
403
459
  const requestedComponents = resolveFlags(argv, '--component');
404
460
  const resolvedComponents = resolveHubSpotDevComponentSelectors(uiExtensionNodes?.candidates ?? [], requestedComponents);
405
461
  const requestedAppCheck = resolvedComponents.ok && requestedComponents.length > 0
@@ -456,6 +512,48 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
456
512
  return cancelledResult(argv, 'dev');
457
513
  selectedNodeUids = picked;
458
514
  }
515
+ // `hs project dev` warns before developing a marketplace app that real
516
+ // portals have installed. Same check, same numbers, same default of "no".
517
+ let productionInstallCount;
518
+ const application = uiExtensionNodes?.application;
519
+ if (isHubSpotMarketplaceApp(application) && application && projectAccount.personalAccessKey) {
520
+ const appLabel = application.name ?? application.uid;
521
+ const installStep = json
522
+ ? undefined
523
+ : reporter.step(`Checking ${appLabel} production installs`);
524
+ const check = await checkProductionInstallsForDev({
525
+ root,
526
+ projectAccount,
527
+ application,
528
+ });
529
+ if (!check.ok) {
530
+ installStep?.warn('could not check');
531
+ if (!json)
532
+ reporter.warn('HSX_W_DEV_PRODUCTION_APP_CHECK_FAILED', check.reason);
533
+ }
534
+ else if (check.installCount === 0) {
535
+ installStep?.ok('none');
536
+ }
537
+ else {
538
+ installStep?.warn(`${check.installCount} ${check.installCount === 1 ? 'account' : 'accounts'}`);
539
+ productionInstallCount = check.installCount;
540
+ const { describeHubSpotProductionInstallWarning, HUBSPOT_PRODUCTION_APP_CONFIRMATION } = await import('../dev/production-app-check.js');
541
+ const warning = describeHubSpotProductionInstallWarning(application.name ?? application.uid, check.installCount);
542
+ if (!json)
543
+ reporter.warn('HSX_W_DEV_PRODUCTION_APP_INSTALLS', warning);
544
+ if (!json && !yes && isInteractive()) {
545
+ const proceed = await promptConfirm({
546
+ message: HUBSPOT_PRODUCTION_APP_CONFIRMATION,
547
+ default: false,
548
+ // The checks above take seconds; an Enter mashed meanwhile must not
549
+ // answer "no" on the user's behalf.
550
+ discardTypeAhead: true,
551
+ });
552
+ if (!proceed)
553
+ return cancelledResult(argv, 'dev');
554
+ }
555
+ }
556
+ }
459
557
  let directDevLogStream;
460
558
  if (!json) {
461
559
  const hsxLogPortValue = resolveFlag(argv, '--hsx-log-port') ?? '9099';
@@ -486,6 +584,7 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
486
584
  root,
487
585
  reporter,
488
586
  filter: onlyParsed.filter,
587
+ hubspotAccount: projectAccount,
489
588
  ...(managedSessionId ? { managedSessionId } : {}),
490
589
  });
491
590
  }
@@ -497,6 +596,16 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
497
596
  ...(selectedNodeUids ? { selectedNodeUids } : {}),
498
597
  ...(profile ? { profile } : {}),
499
598
  preserveState,
599
+ localProxyMode: resolveLocalProxyMode(argv),
600
+ ...(directDevLogStream
601
+ ? {
602
+ relayUrl: directDevLogStream.url,
603
+ onRelayReady: (handle) => {
604
+ if (directDevLogStream)
605
+ directDevLogStream.relay.current = handle;
606
+ },
607
+ }
608
+ : {}),
500
609
  onHeartbeatError: (failure) => {
501
610
  const warning = describeHeartbeatFailure(failure, testingAccount.portalId);
502
611
  if (!warning)
@@ -609,11 +718,15 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
609
718
  portalId: testingAccount.portalId,
610
719
  projectAccountId: projectAccount.portalId,
611
720
  testingAccountId: testingAccount.portalId,
721
+ testingAccountKind: testingChoice,
722
+ ...(productionInstallCount !== undefined ? { productionInstallCount } : {}),
612
723
  sessionId: session.sessionId,
613
724
  components: session.nodeUids,
614
725
  controlPlaneUsed: false,
615
726
  cloudflareUsed: false,
616
- preserveState,
727
+ preserveState: preserveState && session.frontendLogs.active,
728
+ frontendLogs: session.frontendLogs,
729
+ localProxy: session.localProxy,
617
730
  };
618
731
  if (json)
619
732
  write(`${JSON.stringify(started, null, 2)}\n`);
@@ -622,22 +735,57 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
622
735
  reporter.rows([
623
736
  { key: 'mode', value: 'HubSpot direct' },
624
737
  { key: 'project-account', value: `${projectAccount.name} (${projectAccount.portalId})` },
625
- { key: 'testing-account', value: `${testingAccount.name} (${testingAccount.portalId})` },
738
+ {
739
+ key: 'testing-account',
740
+ value: `${testingAccount.name} (${testingAccount.portalId})`,
741
+ detail: describeTestingChoiceForRow(testingChoice),
742
+ ...(testingChoice === 'production'
743
+ ? { status: 'warn' }
744
+ : { status: 'ok' }),
745
+ },
746
+ ...(productionInstallCount !== undefined
747
+ ? [
748
+ {
749
+ key: 'production installs',
750
+ value: `${productionInstallCount} ${productionInstallCount === 1 ? 'account' : 'accounts'}`,
751
+ detail: 'local changes can affect these installs',
752
+ status: 'warn',
753
+ },
754
+ ]
755
+ : []),
626
756
  { key: 'ui-extensions', value: `session ${session.sessionId}`, status: 'ok' },
627
757
  { key: 'components', value: String(session.nodeUids.length) },
628
- ...(preserveState
758
+ ...(preserveState && session.frontendLogs.active
629
759
  ? [{ key: 'reload state', value: 'JSON-safe React useState values', status: 'ok' }]
630
760
  : []),
631
761
  ...(directDevLogStream
632
762
  ? [
633
763
  {
634
764
  key: 'logs',
635
- value: 'frontend + backend + HubSpot events',
636
- detail: directDevLogStream.url,
765
+ value: [
766
+ ...(session.frontendLogs.active ? ['frontend'] : []),
767
+ 'backend',
768
+ ...(directDevLogStream.hubspotLane.active ? ['HubSpot events'] : []),
769
+ ].join(' + '),
770
+ detail: session.frontendLogs.transport === 'fetch'
771
+ ? `${directDevLogStream.url} · card logs via HubSpot local proxy`
772
+ : directDevLogStream.url,
637
773
  status: 'ok',
638
774
  },
775
+ hubspotLogsLaneRow(directDevLogStream.hubspotLane),
776
+ ]
777
+ : []),
778
+ ...(session.frontendLogs.unavailableReason
779
+ ? [
780
+ {
781
+ key: 'frontend logs',
782
+ value: 'off',
783
+ detail: session.frontendLogs.unavailableReason,
784
+ status: 'warn',
785
+ },
639
786
  ]
640
787
  : []),
788
+ ...localProxyRows(session.localProxy),
641
789
  { key: 'Cloudflare', value: 'not used' },
642
790
  { key: 'HS-X control plane', value: 'not used' },
643
791
  ]);
@@ -707,6 +855,9 @@ const KNOWN_DEV_SUBCOMMANDS = new Set([...MANAGED_DEV_SUBCOMMANDS, 'invoke', 'cl
707
855
  const MANAGED_DEV_LANES = new Set(['frontend', 'backend', 'request', 'hubspot']);
708
856
  const MANAGED_DEV_LEVELS = new Set(['debug', 'info', 'warn', 'error']);
709
857
  export async function devCommand({ argv, root, json, reporter, }) {
858
+ // Resolve once before direct/enhanced mode detection and managed-session
859
+ // creation so starting in src/app uses the same project, bindings, and cwd.
860
+ root = (await findContainingProjectRoot(root)) ?? root;
710
861
  const managedSessionId = resolveFlag(argv, '--managed-session');
711
862
  if (managedSessionId) {
712
863
  await waitForManagedDevSessionOwner(managedSessionId);
@@ -1023,6 +1174,9 @@ async function devCommandCore({ argv, root, json, }) {
1023
1174
  const requestStarted = Date.now();
1024
1175
  const method = request.method ?? 'GET';
1025
1176
  const path = request.url ?? '/';
1177
+ const capabilityId = capabilityIdFromDevPath(path);
1178
+ const traceHeader = request.headers['x-hsx-trace-id'];
1179
+ const traceId = (Array.isArray(traceHeader) ? traceHeader[0] : traceHeader)?.trim();
1026
1180
  response.on('finish', () => {
1027
1181
  renderRequestLog({
1028
1182
  method,
@@ -1030,6 +1184,8 @@ async function devCommandCore({ argv, root, json, }) {
1030
1184
  status: response.statusCode,
1031
1185
  durationMs: Date.now() - requestStarted,
1032
1186
  source: 'local',
1187
+ ...(capabilityId ? { capabilityId } : {}),
1188
+ ...(traceId ? { traceId: traceId.slice(0, 128) } : {}),
1033
1189
  });
1034
1190
  });
1035
1191
  if (path === '/_hsx/health') {
@@ -1244,6 +1400,11 @@ async function devCommandCore({ argv, root, json, }) {
1244
1400
  runtimeOrigin: migratedCardTunnel.url,
1245
1401
  })
1246
1402
  : undefined;
1403
+ // The sidecar (below) is what the card's log relay posts to. Its URL is
1404
+ // fixed by --hsx-log-port, so the HubSpot session can be told about it
1405
+ // before it starts; the relay handler lands in this shared slot.
1406
+ const hsxLogPort = Number(resolveFlag(argv, '--hsx-log-port') ?? '9099');
1407
+ const hsxRelay = {};
1247
1408
  // Optional: start a HubSpot UI-extension dev session if this directory
1248
1409
  // contains an hsproject.json AND the user selected at least one UI node.
1249
1410
  const hubSpotDevSession = selectedUiNodeUids && selectedUiNodeUids.length > 0
@@ -1254,12 +1415,15 @@ async function devCommandCore({ argv, root, json, }) {
1254
1415
  accountId,
1255
1416
  reporter,
1256
1417
  selectedNodeUids: selectedUiNodeUids,
1418
+ relayUrl: `http://127.0.0.1:${hsxLogPort}`,
1419
+ onRelayReady: (handle) => {
1420
+ hsxRelay.current = handle;
1421
+ },
1257
1422
  })
1258
1423
  : undefined;
1259
1424
  // HS-X dev sidecar — receives frontend logs (and later: backend/request
1260
1425
  // events) from inside the card iframe and surfaces them in the terminal.
1261
1426
  // Stable default port 9099 so card code can hard-default the dev-log URL.
1262
- const hsxLogPort = Number(resolveFlag(argv, '--hsx-log-port') ?? '9099');
1263
1427
  // `--only` selectors pre-filter the stream (same grammar as the interactive
1264
1428
  // keyboard, see dev/stream-filter.ts). Invalid selectors are a usage error.
1265
1429
  const { parseOnlySelectors } = await import('../dev/stream-filter.js');
@@ -1279,16 +1443,29 @@ async function devCommandCore({ argv, root, json, }) {
1279
1443
  root,
1280
1444
  reporter,
1281
1445
  filter: onlyParsed.filter,
1446
+ relay: hsxRelay,
1282
1447
  ...(managedLogSessionId ? { managedSessionId: managedLogSessionId } : {}),
1283
1448
  });
1284
1449
  if (devLogStream) {
1285
1450
  const paint = makeDevLogPaint(process.env, process.stderr);
1286
1451
  requestSink = (entry) => {
1452
+ const effectiveAccountId = entry.accountId ?? accountId;
1287
1453
  devLogStream.bus.emit({
1288
1454
  lane: 'request',
1289
1455
  level: entry.status >= 500 ? 'error' : entry.status >= 400 ? 'warn' : 'info',
1290
1456
  ts: Date.now(),
1291
1457
  msg: formatDevLogEntry(entry, paint, accountId),
1458
+ fields: {
1459
+ method: entry.method,
1460
+ path: entry.path,
1461
+ status: entry.status,
1462
+ durationMs: entry.durationMs,
1463
+ origin: entry.source ?? 'local',
1464
+ ...(effectiveAccountId ? { accountId: effectiveAccountId } : {}),
1465
+ ...(entry.capabilityId ? { capabilityId: entry.capabilityId } : {}),
1466
+ ...(entry.traceId ? { traceId: entry.traceId } : {}),
1467
+ ...(entry.detail ? { detail: entry.detail } : {}),
1468
+ },
1292
1469
  });
1293
1470
  };
1294
1471
  }
@@ -1376,7 +1553,10 @@ async function devCommandCore({ argv, root, json, }) {
1376
1553
  ]
1377
1554
  : []),
1378
1555
  ...(devLogStream
1379
- ? [{ status: 'ok', key: 'logs', value: `streaming on ${devLogStream.url}` }]
1556
+ ? [
1557
+ { status: 'ok', key: 'logs', value: `streaming on ${devLogStream.url}` },
1558
+ hubspotLogsLaneRow(devLogStream.hubspotLane),
1559
+ ]
1380
1560
  : []),
1381
1561
  ...(devTunnel ? [{ status: 'ok', key: 'tunnel', value: devTunnel.url }] : []),
1382
1562
  ...(migratedCardDevArtifacts
@@ -1501,6 +1681,17 @@ async function devCommandCore({ argv, root, json, }) {
1501
1681
  });
1502
1682
  return { exitCode: 0 };
1503
1683
  }
1684
+ function capabilityIdFromDevPath(path) {
1685
+ const match = /^\/_hsx\/(?:cards|invoke)\/([^/?]+)/.exec(path);
1686
+ if (!match?.[1])
1687
+ return undefined;
1688
+ try {
1689
+ return decodeURIComponent(match[1]);
1690
+ }
1691
+ catch {
1692
+ return match[1];
1693
+ }
1694
+ }
1504
1695
  async function managedDevStartCommand(input) {
1505
1696
  const reporter = input.reporter ??
1506
1697
  createReporter({ command: 'dev start', argv: input.argv, entry: !input.json });
@@ -2091,6 +2282,9 @@ function managedDevFollowReadFilter(filter) {
2091
2282
  function delayMs(ms) {
2092
2283
  return new Promise((resolve) => setTimeout(resolve, ms));
2093
2284
  }
2285
+ const HUBSPOT_LOGS_POLL_INTERVAL_MS = 30_000;
2286
+ /** First poll reaches this far back so the lane shows recent activity at once. */
2287
+ const HUBSPOT_LOGS_CATCH_UP_MS = 2 * 60_000;
2094
2288
  async function startHsxDevLogStream(input) {
2095
2289
  try {
2096
2290
  const [{ createDevEventBus }, { startDevLogServer }, { startDevStreamRenderer }] = await Promise.all([
@@ -2100,7 +2294,8 @@ async function startHsxDevLogStream(input) {
2100
2294
  ]);
2101
2295
  const bus = createDevEventBus();
2102
2296
  const authToken = randomBytes(32).toString('base64url');
2103
- const server = await startDevLogServer({ bus, port: input.port, authToken });
2297
+ const relay = input.relay ?? {};
2298
+ const server = await startDevLogServer({ bus, port: input.port, authToken, relay });
2104
2299
  const renderer = startDevStreamRenderer({
2105
2300
  bus,
2106
2301
  deferStatus: true,
@@ -2117,24 +2312,61 @@ async function startHsxDevLogStream(input) {
2117
2312
  // Missing binding/credentials just means the lane stays off; `hs-x dev`
2118
2313
  // must work without HubSpot credentials.
2119
2314
  let hubspotPoller;
2315
+ let hubspotLane = {
2316
+ active: false,
2317
+ reason: 'HubSpot app logs could not be set up.',
2318
+ };
2120
2319
  try {
2121
2320
  const [{ resolveHubSpotLogsSource }, { startHubSpotLogsPoller }] = await Promise.all([
2122
2321
  import('../hubspot-app-logs.js'),
2123
2322
  import('../dev/hubspot-logs-poller.js'),
2124
2323
  ]);
2125
- const resolution = await resolveHubSpotLogsSource({ cwd: input.root });
2324
+ const account = input.hubspotAccount;
2325
+ const resolution = await resolveHubSpotLogsSource({
2326
+ cwd: input.root,
2327
+ ...(account?.personalAccessKey
2328
+ ? {
2329
+ credentials: {
2330
+ developerAccountId: account.portalId,
2331
+ personalAccessKey: account.personalAccessKey,
2332
+ },
2333
+ }
2334
+ : {}),
2335
+ });
2126
2336
  if (resolution.ok) {
2127
- hubspotPoller = startHubSpotLogsPoller({ bus, source: resolution.source });
2337
+ hubspotPoller = startHubSpotLogsPoller({
2338
+ bus,
2339
+ source: resolution.source,
2340
+ intervalMs: HUBSPOT_LOGS_POLL_INTERVAL_MS,
2341
+ catchUpMs: HUBSPOT_LOGS_CATCH_UP_MS,
2342
+ });
2343
+ // Poll once now so the lane proves itself (or its first error) within
2344
+ // seconds of startup instead of one interval later.
2345
+ void hubspotPoller.poll();
2346
+ hubspotLane = {
2347
+ active: true,
2348
+ appId: resolution.source.appId,
2349
+ developerAccountId: resolution.source.developerAccountId,
2350
+ };
2351
+ }
2352
+ else {
2353
+ hubspotLane = { active: false, reason: resolution.reason };
2128
2354
  }
2129
2355
  }
2130
- catch {
2356
+ catch (cause) {
2131
2357
  // The HubSpot lane is additive; never fail the dev stream over it.
2358
+ hubspotLane = {
2359
+ active: false,
2360
+ reason: cause instanceof Error ? cause.message : String(cause),
2361
+ };
2132
2362
  }
2133
2363
  return {
2134
2364
  url: server.url,
2135
2365
  authToken,
2136
2366
  renderer,
2137
2367
  bus,
2368
+ hubspotLane,
2369
+ relay,
2138
2370
  async cleanup() {
2139
2371
  hubspotPoller?.stop();
2140
2372
  unsubscribeManagedEvents?.();
@@ -2191,7 +2423,13 @@ async function probeHubSpotUiNodes(input) {
2191
2423
  accountId: portalId,
2192
2424
  }, input.profile ? { profile: input.profile } : {});
2193
2425
  const candidates = listHubSpotDevComponentCandidates(ir.intermediateNodesIndexedByUid);
2194
- return { uids: candidates.map((candidate) => candidate.uid), candidates, portalId };
2426
+ const application = findHubSpotDevApplication(ir.intermediateNodesIndexedByUid);
2427
+ return {
2428
+ uids: candidates.map((candidate) => candidate.uid),
2429
+ candidates,
2430
+ portalId,
2431
+ ...(application ? { application } : {}),
2432
+ };
2195
2433
  }
2196
2434
  catch {
2197
2435
  // IR translate failed — caller falls back to capability-only behavior.
@@ -2216,6 +2454,9 @@ async function maybeStartHubSpotDevSession(input) {
2216
2454
  selectedNodeUids: input.selectedNodeUids,
2217
2455
  ...(profile ? { profile } : {}),
2218
2456
  preserveState: input.argv.includes('--preserve-state'),
2457
+ localProxyMode: resolveLocalProxyMode(input.argv),
2458
+ ...(input.relayUrl ? { relayUrl: input.relayUrl } : {}),
2459
+ ...(input.onRelayReady ? { onRelayReady: input.onRelayReady } : {}),
2219
2460
  onHeartbeatError: (failure) => {
2220
2461
  const warning = describeHeartbeatFailure(failure, portalId);
2221
2462
  if (warning && !input.json)
@@ -2225,6 +2466,16 @@ async function maybeStartHubSpotDevSession(input) {
2225
2466
  input.reporter
2226
2467
  .step('HubSpot UI-extension session')
2227
2468
  .ok(`id ${session.sessionId} · ${session.nodeUids.length} component${session.nodeUids.length === 1 ? '' : 's'}`);
2469
+ if (!input.json) {
2470
+ if (session.frontendLogs.unavailableReason) {
2471
+ input.reporter.warn('HSX_W_DEV_FRONTEND_LOGS_UNAVAILABLE', session.frontendLogs.unavailableReason);
2472
+ }
2473
+ for (const probe of session.localProxy.mappings) {
2474
+ if (probe.applied && probe.reachable)
2475
+ continue;
2476
+ input.reporter.warn('HSX_W_DEV_LOCAL_PROXY_UNREACHABLE', `local.json proxies ${probe.from} → ${probe.to}: ${describeLocalProxyIssue(session.localProxy, probe)}`);
2477
+ }
2478
+ }
2228
2479
  return session;
2229
2480
  }
2230
2481
  catch (error) {
@@ -2953,6 +3204,59 @@ function formatDevExpiry(now, expiresAt) {
2953
3204
  function isFlagValue(value) {
2954
3205
  return value !== undefined && value.length > 0 && !value.startsWith('-');
2955
3206
  }
3207
+ /**
3208
+ * The HubSpot-side lane is the only place renders and proxy executions show
3209
+ * up, so its state must be on the checklist: a silent lane looks identical to
3210
+ * a broken one.
3211
+ */
3212
+ function hubspotLogsLaneRow(lane) {
3213
+ if (lane.active) {
3214
+ return {
3215
+ key: 'HubSpot app logs',
3216
+ value: `app ${lane.appId} · renders + proxy executions`,
3217
+ detail: `read from account ${lane.developerAccountId} every ${HUBSPOT_LOGS_POLL_INTERVAL_MS / 1000}s; entries appear shortly after the card acts`,
3218
+ status: 'ok',
3219
+ };
3220
+ }
3221
+ return { key: 'HubSpot app logs', value: 'off', detail: lane.reason, status: 'warn' };
3222
+ }
3223
+ function describeTestingChoiceForRow(choice) {
3224
+ switch (choice) {
3225
+ case 'developer-test':
3226
+ return 'developer test account';
3227
+ case 'sandbox':
3228
+ return 'sandbox';
3229
+ default:
3230
+ return 'production';
3231
+ }
3232
+ }
3233
+ /**
3234
+ * Resolve the deployed app and count its non-test installs with the project
3235
+ * account's own credentials — the same lookup `hs project dev` performs.
3236
+ */
3237
+ async function checkProductionInstallsForDev(input) {
3238
+ try {
3239
+ const projectConfig = JSON.parse(await readFile(join(input.root, 'hsproject.json'), 'utf8'));
3240
+ if (!projectConfig.name) {
3241
+ return { ok: false, reason: 'hsproject.json has no project name to look the app up by.' };
3242
+ }
3243
+ const [{ createHubSpotDeveloperClient }, { checkHubSpotProductionInstalls }] = await Promise.all([import('@hs-x/hubspot-cli'), import('../dev/production-app-check.js')]);
3244
+ const client = createHubSpotDeveloperClient({
3245
+ developerAccountId: input.projectAccount.portalId,
3246
+ ...(input.projectAccount.personalAccessKey
3247
+ ? { personalAccessKey: input.projectAccount.personalAccessKey }
3248
+ : {}),
3249
+ });
3250
+ return await checkHubSpotProductionInstalls({
3251
+ client,
3252
+ projectName: projectConfig.name,
3253
+ appUid: input.application.uid,
3254
+ });
3255
+ }
3256
+ catch (cause) {
3257
+ return { ok: false, reason: cause instanceof Error ? cause.message : String(cause) };
3258
+ }
3259
+ }
2956
3260
  function resolveFlag(argv, flag) {
2957
3261
  const index = argv.indexOf(flag);
2958
3262
  if (index !== -1) {
@@ -3021,6 +3325,7 @@ const projectAccountOption = Options.text('project-account').pipe(Options.option
3021
3325
  const testingAccountOption = Options.text('testing-account').pipe(Options.optional);
3022
3326
  const profileOption = Options.text('profile').pipe(Options.withAlias('p'), Options.optional);
3023
3327
  const preserveStateOption = Options.boolean('preserve-state').pipe(Options.withDefault(false));
3328
+ const localProxyOption = Options.choice('local-proxy', ['auto', 'always', 'never']).pipe(Options.withDefault('auto'));
3024
3329
  const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
3025
3330
  const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
3026
3331
  const accountIdOption = Options.text('account-id').pipe(Options.optional);
@@ -3069,6 +3374,7 @@ export const devCmd = Command.make('dev', {
3069
3374
  testingAccount: testingAccountOption,
3070
3375
  profile: profileOption,
3071
3376
  preserveState: preserveStateOption,
3377
+ localProxy: localProxyOption,
3072
3378
  targetOrigin: targetOriginOption,
3073
3379
  telemetryOrigin: telemetryOriginOption,
3074
3380
  accountId: accountIdOption,
@@ -3148,6 +3454,8 @@ export const devCmd = Command.make('dev', {
3148
3454
  argv.push('--profile', opts.profile.value);
3149
3455
  if (opts.preserveState)
3150
3456
  argv.push('--preserve-state');
3457
+ if (opts.localProxy !== 'auto')
3458
+ argv.push('--local-proxy', opts.localProxy);
3151
3459
  if (Option.isSome(opts.targetOrigin)) {
3152
3460
  argv.push('--target-origin', opts.targetOrigin.value);
3153
3461
  }
@@ -3320,4 +3628,48 @@ async function devInvokeRemote(args) {
3320
3628
  reporter.done(ok ? 'Invoked' : 'Failed', ok ? 0 : 1);
3321
3629
  return { exitCode: ok ? 0 : 1 };
3322
3630
  }
3631
+ /**
3632
+ * `<app root>/local.json` proxy mappings switch HubSpot's dev server into local
3633
+ * proxy mode for `hubspot.fetch`; a mapping whose target is down fails every
3634
+ * fetch to that origin with an empty error, so show each one up front.
3635
+ */
3636
+ function resolveLocalProxyMode(argv) {
3637
+ return resolveHubSpotLocalProxyMode(resolveFlag(argv, '--local-proxy'));
3638
+ }
3639
+ /**
3640
+ * One short row per local.json mapping, plus one when proxy mode is on with no
3641
+ * mappings (`--local-proxy always`). Details stay one clause long; the mode
3642
+ * semantics live in `hs-x dev --help` and the guide.
3643
+ */
3644
+ function localProxyRows(localProxy) {
3645
+ const rows = localProxy.mappings.map((probe) => {
3646
+ const healthy = probe.applied && probe.reachable;
3647
+ return {
3648
+ key: 'local proxy',
3649
+ value: probe.applied ? `${probe.from} → ${probe.to}` : `off · ${probe.from}`,
3650
+ status: healthy ? 'ok' : 'warn',
3651
+ ...(healthy ? {} : { detail: describeLocalProxyIssue(localProxy, probe) }),
3652
+ };
3653
+ });
3654
+ if (localProxy.active && localProxy.mappings.length === 0) {
3655
+ rows.push({
3656
+ key: 'local proxy',
3657
+ value: 'on · no local.json mappings',
3658
+ detail: localProxy.signed
3659
+ ? 'hubspot.fetch leaves this machine signed with the app secret'
3660
+ : 'hubspot.fetch leaves this machine unsigned; set HUBSPOT_CLIENT_SECRET to sign',
3661
+ status: localProxy.signed ? 'ok' : 'warn',
3662
+ });
3663
+ }
3664
+ return rows;
3665
+ }
3666
+ function describeLocalProxyIssue(localProxy, probe) {
3667
+ if (localProxy.mode === 'never') {
3668
+ return 'ignored (--local-proxy never); hubspot.fetch stays on HubSpot proxy';
3669
+ }
3670
+ if (!probe.applied) {
3671
+ return `${probe.to} not listening; hubspot.fetch stays on HubSpot proxy — start it and restart, or --local-proxy always`;
3672
+ }
3673
+ return `${probe.to} not listening — every hubspot.fetch to this origin fails until it starts (--local-proxy always)`;
3674
+ }
3323
3675
  //# sourceMappingURL=dev.js.map