@hs-x/cli 0.4.12 → 0.4.14

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 (117) 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 +346 -32
  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 +12 -0
  43. package/dist/dev/component-selection.d.ts.map +1 -1
  44. package/dist/dev/component-selection.js +29 -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-log-lane.d.ts +27 -0
  51. package/dist/dev/hubspot-log-lane.d.ts.map +1 -0
  52. package/dist/dev/hubspot-log-lane.js +73 -0
  53. package/dist/dev/hubspot-log-lane.js.map +1 -0
  54. package/dist/dev/hubspot-logs-poller.d.ts +7 -1
  55. package/dist/dev/hubspot-logs-poller.d.ts.map +1 -1
  56. package/dist/dev/hubspot-logs-poller.js +28 -1
  57. package/dist/dev/hubspot-logs-poller.js.map +1 -1
  58. package/dist/dev/keyboard.d.ts +21 -3
  59. package/dist/dev/keyboard.d.ts.map +1 -1
  60. package/dist/dev/keyboard.js +106 -6
  61. package/dist/dev/keyboard.js.map +1 -1
  62. package/dist/dev/local-proxy.d.ts +90 -0
  63. package/dist/dev/local-proxy.d.ts.map +1 -0
  64. package/dist/dev/local-proxy.js +186 -0
  65. package/dist/dev/local-proxy.js.map +1 -0
  66. package/dist/dev/log-server.d.ts +9 -0
  67. package/dist/dev/log-server.d.ts.map +1 -1
  68. package/dist/dev/log-server.js +46 -3
  69. package/dist/dev/log-server.js.map +1 -1
  70. package/dist/dev/production-app-check.d.ts +23 -0
  71. package/dist/dev/production-app-check.d.ts.map +1 -0
  72. package/dist/dev/production-app-check.js +28 -0
  73. package/dist/dev/production-app-check.js.map +1 -0
  74. package/dist/dev/session-manager.d.ts +51 -1
  75. package/dist/dev/session-manager.d.ts.map +1 -1
  76. package/dist/dev/session-manager.js +155 -12
  77. package/dist/dev/session-manager.js.map +1 -1
  78. package/dist/dev/startup-progress.d.ts +7 -0
  79. package/dist/dev/startup-progress.d.ts.map +1 -0
  80. package/dist/dev/startup-progress.js +27 -0
  81. package/dist/dev/startup-progress.js.map +1 -0
  82. package/dist/dev/stream-event-format.d.ts +10 -1
  83. package/dist/dev/stream-event-format.d.ts.map +1 -1
  84. package/dist/dev/stream-event-format.js +157 -5
  85. package/dist/dev/stream-event-format.js.map +1 -1
  86. package/dist/dev/stream-renderer.d.ts +34 -1
  87. package/dist/dev/stream-renderer.d.ts.map +1 -1
  88. package/dist/dev/stream-renderer.js +371 -60
  89. package/dist/dev/stream-renderer.js.map +1 -1
  90. package/dist/dev/trace-markdown.d.ts +25 -0
  91. package/dist/dev/trace-markdown.d.ts.map +1 -0
  92. package/dist/dev/trace-markdown.js +159 -0
  93. package/dist/dev/trace-markdown.js.map +1 -0
  94. package/dist/hubspot-app-logs.d.ts +9 -0
  95. package/dist/hubspot-app-logs.d.ts.map +1 -1
  96. package/dist/hubspot-app-logs.js +6 -4
  97. package/dist/hubspot-app-logs.js.map +1 -1
  98. package/dist/hubspot-developer-client.js +3 -0
  99. package/dist/hubspot-developer-client.js.map +1 -1
  100. package/dist/hubspot-project.d.ts +2 -0
  101. package/dist/hubspot-project.d.ts.map +1 -1
  102. package/dist/hubspot-project.js +13 -1
  103. package/dist/hubspot-project.js.map +1 -1
  104. package/dist/prompt.d.ts +15 -0
  105. package/dist/prompt.d.ts.map +1 -1
  106. package/dist/prompt.js +66 -15
  107. package/dist/prompt.js.map +1 -1
  108. package/dist/reporter/human.d.ts.map +1 -1
  109. package/dist/reporter/human.js +6 -1
  110. package/dist/reporter/human.js.map +1 -1
  111. package/dist/reporter/style.d.ts +10 -0
  112. package/dist/reporter/style.d.ts.map +1 -1
  113. package/dist/reporter/style.js +39 -0
  114. package/dist/reporter/style.js.map +1 -1
  115. package/dist/services/hubspot-dev-client.js +3 -0
  116. package/dist/services/hubspot-dev-client.js.map +1 -1
  117. package/package.json +9 -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;AA8D1D,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;AA09BzC,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;AA2vFD,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqJlB,CAAC"}
@@ -15,13 +15,16 @@ 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 { HUBSPOT_LOGS_POLL_INTERVAL_MS, startHubSpotLogLane, } from '../dev/hubspot-log-lane.js';
21
+ import { resolveHubSpotLocalProxyMode } from '../dev/local-proxy.js';
20
22
  import { createManagedDevEventWriter, createManagedDevSession, markManagedDevSessionReady, markManagedDevSessionState, prepareManagedDevSessionRestart, publicManagedDevSession, readManagedDevEvents, readManagedDevSession, reconcileManagedDevSession, setManagedDevSessionPid, stopManagedDevSessionProcess, waitForManagedDevSessionOwner, } from '../dev/managed-session.js';
21
23
  import { findListeningProcess, isHubSpotDevPortConflict, stopProcessCommand, terminateProcessGracefully, } from '../dev/port-process.js';
24
+ import { createDevStartupProgress } from '../dev/startup-progress.js';
22
25
  import { executeCliHttp } from '../effect-http.js';
23
26
  import { discoverHubSpotAccounts, exchangePak } from '../hubspot-auth.js';
24
- import { detectLocalProjectMode, validateHubSpotProject } from '../hubspot-project.js';
27
+ import { detectLocalProjectMode, findContainingProjectRoot, validateHubSpotProject, } from '../hubspot-project.js';
25
28
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
26
29
  import { requestMutationConsent } from '../mutation-consent.js';
27
30
  import { isMachineOutput } from '../output-context.js';
@@ -281,6 +284,7 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
281
284
  const reporter = createReporter({ command: 'dev', argv, entry: !json });
282
285
  const profile = resolveFlag(argv, '--profile');
283
286
  const preserveState = argv.includes('--preserve-state');
287
+ const yes = argv.some((argument) => argument === '--yes' || argument === '-y');
284
288
  const validation = await validateHubSpotProject({ root, ...(profile ? { profile } : {}) });
285
289
  if (!validation.ok) {
286
290
  const result = {
@@ -363,15 +367,62 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
363
367
  }
364
368
  return { exitCode: 1 };
365
369
  }
366
- const testingAccount = selectedTestingAccount ?? projectAccount;
370
+ // Mirror `hs project dev`: recommend a developer test account or sandbox,
371
+ // and never land on the production project account without saying so.
372
+ const testingPlan = planHubSpotDevTestingAccount({
373
+ accounts,
374
+ projectAccount,
375
+ // selectHubSpotDirectDevAccounts defaults the testing account to the
376
+ // project account; only an explicit flag counts as an answer here.
377
+ selectedTestingAccount: requestedTestingAccount ? selectedTestingAccount : undefined,
378
+ interactive: !json && !yes && isInteractive(),
379
+ });
380
+ let testingAccount;
381
+ let testingChoice;
382
+ if (testingPlan.kind === 'prompt') {
383
+ const picked = await promptSelect({
384
+ message: 'Choose the account to test on',
385
+ options: testingPlan.choices.map((choice) => ({
386
+ value: String(choice.account.portalId),
387
+ ...describeHubSpotDevTestingChoice(choice),
388
+ })),
389
+ default: String(testingPlan.choices[0]?.account.portalId),
390
+ });
391
+ if (picked === undefined)
392
+ return cancelledResult(argv, 'dev');
393
+ const choice = testingPlan.choices.find((item) => String(item.account.portalId) === picked);
394
+ if (!choice)
395
+ return cancelledResult(argv, 'dev');
396
+ testingAccount = choice.account;
397
+ testingChoice = choice.kind;
398
+ reporter.info(`Testing on ${testingAccount.name} (${testingAccount.portalId}) — ${describeTestingChoiceForRow(testingChoice)}`);
399
+ }
400
+ else {
401
+ testingAccount = testingPlan.account;
402
+ testingChoice = testingPlan.choice;
403
+ if (!json) {
404
+ const explanation = describeHubSpotDevTestingPlanReason(testingPlan);
405
+ if (explanation.level === 'warn') {
406
+ reporter.warn('HSX_W_DEV_TESTING_ON_PRODUCTION_ACCOUNT', explanation.message);
407
+ }
408
+ else {
409
+ reporter.info(explanation.message);
410
+ }
411
+ }
412
+ }
367
413
  if (testingAccount.personalAccessKey) {
414
+ const credentialStep = json
415
+ ? undefined
416
+ : reporter.step(`Checking ${testingAccount.name} (${testingAccount.portalId}) credentials`);
368
417
  try {
369
418
  const exchanged = await exchangePak(testingAccount.personalAccessKey);
370
419
  if (exchanged.portalId !== testingAccount.portalId) {
371
420
  throw new Error(`the configured PAK belongs to account ${exchanged.portalId}, not ${testingAccount.portalId}`);
372
421
  }
422
+ credentialStep?.ok();
373
423
  }
374
424
  catch (cause) {
425
+ credentialStep?.fail();
375
426
  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
427
  if (json) {
377
428
  write(`${JSON.stringify({
@@ -394,12 +445,19 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
394
445
  const existingDevServer = await recoverHubSpotDevPort({ argv, json, reporter });
395
446
  if (existingDevServer.kind === 'exit')
396
447
  return existingDevServer.result;
448
+ const probeStep = json ? undefined : reporter.step('Reading HubSpot project components');
397
449
  const uiExtensionNodes = await probeHubSpotUiNodes({
398
450
  root,
399
451
  portalIdFlag: String(projectAccount.portalId),
400
452
  accountId: undefined,
401
453
  ...(profile ? { profile } : {}),
402
454
  });
455
+ if (uiExtensionNodes) {
456
+ probeStep?.ok(`${uiExtensionNodes.candidates.length} component${uiExtensionNodes.candidates.length === 1 ? '' : 's'}${uiExtensionNodes.application?.name ? ` · ${uiExtensionNodes.application.name}` : ''}`);
457
+ }
458
+ else {
459
+ probeStep?.warn('no UI-extension components found');
460
+ }
403
461
  const requestedComponents = resolveFlags(argv, '--component');
404
462
  const resolvedComponents = resolveHubSpotDevComponentSelectors(uiExtensionNodes?.candidates ?? [], requestedComponents);
405
463
  const requestedAppCheck = resolvedComponents.ok && requestedComponents.length > 0
@@ -446,16 +504,61 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
446
504
  const check = resolveHubSpotDevSessionApp(pickerCandidates, selection);
447
505
  return check.ok ? undefined : check.message;
448
506
  },
507
+ columns: ['Name', 'Type', 'Objects', 'Location', 'UID'],
449
508
  options: pickerCandidates.map((candidate) => ({
450
509
  value: candidate.uid,
451
510
  label: candidate.label,
452
511
  description: candidate.description,
512
+ ...(candidate.cells ? { cells: candidate.cells } : {}),
453
513
  })),
454
514
  });
455
515
  if (picked === undefined)
456
516
  return cancelledResult(argv, 'dev');
457
517
  selectedNodeUids = picked;
458
518
  }
519
+ // `hs project dev` warns before developing a marketplace app that real
520
+ // portals have installed. Same check, same numbers, same default of "no".
521
+ let productionInstallCount;
522
+ const application = uiExtensionNodes?.application;
523
+ if (isHubSpotMarketplaceApp(application) && application && projectAccount.personalAccessKey) {
524
+ const appLabel = application.name ?? application.uid;
525
+ const installStep = json
526
+ ? undefined
527
+ : reporter.step(`Checking ${appLabel} production installs`);
528
+ const check = await checkProductionInstallsForDev({
529
+ root,
530
+ projectAccount,
531
+ application,
532
+ });
533
+ if (!check.ok) {
534
+ installStep?.warn('could not check');
535
+ if (!json)
536
+ reporter.warn('HSX_W_DEV_PRODUCTION_APP_CHECK_FAILED', check.reason);
537
+ }
538
+ else if (check.installCount === 0) {
539
+ installStep?.ok('none');
540
+ }
541
+ else {
542
+ installStep?.warn(`${check.installCount} ${check.installCount === 1 ? 'account' : 'accounts'}`);
543
+ productionInstallCount = check.installCount;
544
+ const { describeHubSpotProductionInstallWarning, HUBSPOT_PRODUCTION_APP_CONFIRMATION } = await import('../dev/production-app-check.js');
545
+ const warning = describeHubSpotProductionInstallWarning(application.name ?? application.uid, check.installCount);
546
+ if (!json)
547
+ reporter.warn('HSX_W_DEV_PRODUCTION_APP_INSTALLS', warning);
548
+ if (!json && !yes && isInteractive()) {
549
+ const proceed = await promptConfirm({
550
+ message: HUBSPOT_PRODUCTION_APP_CONFIRMATION,
551
+ default: false,
552
+ // The checks above take seconds; an Enter mashed meanwhile must not
553
+ // answer "no" on the user's behalf.
554
+ discardTypeAhead: true,
555
+ });
556
+ if (!proceed)
557
+ return cancelledResult(argv, 'dev');
558
+ }
559
+ }
560
+ }
561
+ const startupProgress = json ? undefined : createDevStartupProgress(reporter);
459
562
  let directDevLogStream;
460
563
  if (!json) {
461
564
  const hsxLogPortValue = resolveFlag(argv, '--hsx-log-port') ?? '9099';
@@ -481,11 +584,13 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
481
584
  return { exitCode: 2 };
482
585
  }
483
586
  const managedSessionId = resolveFlag(argv, '--managed-session');
587
+ startupProgress?.phase('Starting local log relay');
484
588
  directDevLogStream = await startHsxDevLogStream({
485
589
  port: hsxLogPort,
486
590
  root,
487
591
  reporter,
488
592
  filter: onlyParsed.filter,
593
+ hubspotAccount: projectAccount,
489
594
  ...(managedSessionId ? { managedSessionId } : {}),
490
595
  });
491
596
  }
@@ -493,10 +598,21 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
493
598
  const startDirectSession = () => startDevSession({
494
599
  projectAccountId: projectAccount.portalId,
495
600
  testingAccountId: testingAccount.portalId,
601
+ ...(startupProgress ? { onStartupProgress: startupProgress.phase } : {}),
496
602
  projectDir: root,
497
603
  ...(selectedNodeUids ? { selectedNodeUids } : {}),
498
604
  ...(profile ? { profile } : {}),
499
605
  preserveState,
606
+ localProxyMode: resolveLocalProxyMode(argv),
607
+ ...(directDevLogStream
608
+ ? {
609
+ relayUrl: directDevLogStream.url,
610
+ onRelayReady: (handle) => {
611
+ if (directDevLogStream)
612
+ directDevLogStream.relay.current = handle;
613
+ },
614
+ }
615
+ : {}),
500
616
  onHeartbeatError: (failure) => {
501
617
  const warning = describeHeartbeatFailure(failure, testingAccount.portalId);
502
618
  if (!warning)
@@ -548,6 +664,7 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
548
664
  if (isHubSpotDevPortConflict(initialCause)) {
549
665
  const recovery = await recoverHubSpotDevPort({ argv, json, reporter });
550
666
  if (recovery.kind === 'exit') {
667
+ startupProgress?.finish(true);
551
668
  try {
552
669
  await directDevLogStream?.cleanup();
553
670
  }
@@ -567,6 +684,7 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
567
684
  }
568
685
  }
569
686
  }
687
+ startupProgress?.finish(!session);
570
688
  if (!session) {
571
689
  if (directDevLogStream) {
572
690
  try {
@@ -609,11 +727,15 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
609
727
  portalId: testingAccount.portalId,
610
728
  projectAccountId: projectAccount.portalId,
611
729
  testingAccountId: testingAccount.portalId,
730
+ testingAccountKind: testingChoice,
731
+ ...(productionInstallCount !== undefined ? { productionInstallCount } : {}),
612
732
  sessionId: session.sessionId,
613
733
  components: session.nodeUids,
614
734
  controlPlaneUsed: false,
615
735
  cloudflareUsed: false,
616
- preserveState,
736
+ preserveState: preserveState && session.frontendLogs.active,
737
+ frontendLogs: session.frontendLogs,
738
+ localProxy: session.localProxy,
617
739
  };
618
740
  if (json)
619
741
  write(`${JSON.stringify(started, null, 2)}\n`);
@@ -622,22 +744,57 @@ async function hubSpotDirectDevCommand({ argv, root, json, }) {
622
744
  reporter.rows([
623
745
  { key: 'mode', value: 'HubSpot direct' },
624
746
  { key: 'project-account', value: `${projectAccount.name} (${projectAccount.portalId})` },
625
- { key: 'testing-account', value: `${testingAccount.name} (${testingAccount.portalId})` },
747
+ {
748
+ key: 'testing-account',
749
+ value: `${testingAccount.name} (${testingAccount.portalId})`,
750
+ detail: describeTestingChoiceForRow(testingChoice),
751
+ ...(testingChoice === 'production'
752
+ ? { status: 'warn' }
753
+ : { status: 'ok' }),
754
+ },
755
+ ...(productionInstallCount !== undefined
756
+ ? [
757
+ {
758
+ key: 'production installs',
759
+ value: `${productionInstallCount} ${productionInstallCount === 1 ? 'account' : 'accounts'}`,
760
+ detail: 'local changes can affect these installs',
761
+ status: 'warn',
762
+ },
763
+ ]
764
+ : []),
626
765
  { key: 'ui-extensions', value: `session ${session.sessionId}`, status: 'ok' },
627
766
  { key: 'components', value: String(session.nodeUids.length) },
628
- ...(preserveState
767
+ ...(preserveState && session.frontendLogs.active
629
768
  ? [{ key: 'reload state', value: 'JSON-safe React useState values', status: 'ok' }]
630
769
  : []),
631
770
  ...(directDevLogStream
632
771
  ? [
633
772
  {
634
773
  key: 'logs',
635
- value: 'frontend + backend + HubSpot events',
636
- detail: directDevLogStream.url,
774
+ value: [
775
+ ...(session.frontendLogs.active ? ['frontend'] : []),
776
+ 'backend',
777
+ ...(directDevLogStream.hubspotLane.active ? ['HubSpot events'] : []),
778
+ ].join(' + '),
779
+ detail: session.frontendLogs.transport === 'fetch'
780
+ ? `${directDevLogStream.url} · card logs via HubSpot local proxy`
781
+ : directDevLogStream.url,
637
782
  status: 'ok',
638
783
  },
784
+ hubspotLogsLaneRow(directDevLogStream.hubspotLane),
639
785
  ]
640
786
  : []),
787
+ ...(session.frontendLogs.unavailableReason
788
+ ? [
789
+ {
790
+ key: 'frontend logs',
791
+ value: 'off',
792
+ detail: session.frontendLogs.unavailableReason,
793
+ status: 'warn',
794
+ },
795
+ ]
796
+ : []),
797
+ ...localProxyRows(session.localProxy),
641
798
  { key: 'Cloudflare', value: 'not used' },
642
799
  { key: 'HS-X control plane', value: 'not used' },
643
800
  ]);
@@ -707,6 +864,9 @@ const KNOWN_DEV_SUBCOMMANDS = new Set([...MANAGED_DEV_SUBCOMMANDS, 'invoke', 'cl
707
864
  const MANAGED_DEV_LANES = new Set(['frontend', 'backend', 'request', 'hubspot']);
708
865
  const MANAGED_DEV_LEVELS = new Set(['debug', 'info', 'warn', 'error']);
709
866
  export async function devCommand({ argv, root, json, reporter, }) {
867
+ // Resolve once before direct/enhanced mode detection and managed-session
868
+ // creation so starting in src/app uses the same project, bindings, and cwd.
869
+ root = (await findContainingProjectRoot(root)) ?? root;
710
870
  const managedSessionId = resolveFlag(argv, '--managed-session');
711
871
  if (managedSessionId) {
712
872
  await waitForManagedDevSessionOwner(managedSessionId);
@@ -1023,6 +1183,9 @@ async function devCommandCore({ argv, root, json, }) {
1023
1183
  const requestStarted = Date.now();
1024
1184
  const method = request.method ?? 'GET';
1025
1185
  const path = request.url ?? '/';
1186
+ const capabilityId = capabilityIdFromDevPath(path);
1187
+ const traceHeader = request.headers['x-hsx-trace-id'];
1188
+ const traceId = (Array.isArray(traceHeader) ? traceHeader[0] : traceHeader)?.trim();
1026
1189
  response.on('finish', () => {
1027
1190
  renderRequestLog({
1028
1191
  method,
@@ -1030,6 +1193,8 @@ async function devCommandCore({ argv, root, json, }) {
1030
1193
  status: response.statusCode,
1031
1194
  durationMs: Date.now() - requestStarted,
1032
1195
  source: 'local',
1196
+ ...(capabilityId ? { capabilityId } : {}),
1197
+ ...(traceId ? { traceId: traceId.slice(0, 128) } : {}),
1033
1198
  });
1034
1199
  });
1035
1200
  if (path === '/_hsx/health') {
@@ -1244,6 +1409,11 @@ async function devCommandCore({ argv, root, json, }) {
1244
1409
  runtimeOrigin: migratedCardTunnel.url,
1245
1410
  })
1246
1411
  : undefined;
1412
+ // The sidecar (below) is what the card's log relay posts to. Its URL is
1413
+ // fixed by --hsx-log-port, so the HubSpot session can be told about it
1414
+ // before it starts; the relay handler lands in this shared slot.
1415
+ const hsxLogPort = Number(resolveFlag(argv, '--hsx-log-port') ?? '9099');
1416
+ const hsxRelay = {};
1247
1417
  // Optional: start a HubSpot UI-extension dev session if this directory
1248
1418
  // contains an hsproject.json AND the user selected at least one UI node.
1249
1419
  const hubSpotDevSession = selectedUiNodeUids && selectedUiNodeUids.length > 0
@@ -1254,12 +1424,15 @@ async function devCommandCore({ argv, root, json, }) {
1254
1424
  accountId,
1255
1425
  reporter,
1256
1426
  selectedNodeUids: selectedUiNodeUids,
1427
+ relayUrl: `http://127.0.0.1:${hsxLogPort}`,
1428
+ onRelayReady: (handle) => {
1429
+ hsxRelay.current = handle;
1430
+ },
1257
1431
  })
1258
1432
  : undefined;
1259
1433
  // HS-X dev sidecar — receives frontend logs (and later: backend/request
1260
1434
  // events) from inside the card iframe and surfaces them in the terminal.
1261
1435
  // 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
1436
  // `--only` selectors pre-filter the stream (same grammar as the interactive
1264
1437
  // keyboard, see dev/stream-filter.ts). Invalid selectors are a usage error.
1265
1438
  const { parseOnlySelectors } = await import('../dev/stream-filter.js');
@@ -1279,16 +1452,29 @@ async function devCommandCore({ argv, root, json, }) {
1279
1452
  root,
1280
1453
  reporter,
1281
1454
  filter: onlyParsed.filter,
1455
+ relay: hsxRelay,
1282
1456
  ...(managedLogSessionId ? { managedSessionId: managedLogSessionId } : {}),
1283
1457
  });
1284
1458
  if (devLogStream) {
1285
1459
  const paint = makeDevLogPaint(process.env, process.stderr);
1286
1460
  requestSink = (entry) => {
1461
+ const effectiveAccountId = entry.accountId ?? accountId;
1287
1462
  devLogStream.bus.emit({
1288
1463
  lane: 'request',
1289
1464
  level: entry.status >= 500 ? 'error' : entry.status >= 400 ? 'warn' : 'info',
1290
1465
  ts: Date.now(),
1291
1466
  msg: formatDevLogEntry(entry, paint, accountId),
1467
+ fields: {
1468
+ method: entry.method,
1469
+ path: entry.path,
1470
+ status: entry.status,
1471
+ durationMs: entry.durationMs,
1472
+ origin: entry.source ?? 'local',
1473
+ ...(effectiveAccountId ? { accountId: effectiveAccountId } : {}),
1474
+ ...(entry.capabilityId ? { capabilityId: entry.capabilityId } : {}),
1475
+ ...(entry.traceId ? { traceId: entry.traceId } : {}),
1476
+ ...(entry.detail ? { detail: entry.detail } : {}),
1477
+ },
1292
1478
  });
1293
1479
  };
1294
1480
  }
@@ -1376,7 +1562,10 @@ async function devCommandCore({ argv, root, json, }) {
1376
1562
  ]
1377
1563
  : []),
1378
1564
  ...(devLogStream
1379
- ? [{ status: 'ok', key: 'logs', value: `streaming on ${devLogStream.url}` }]
1565
+ ? [
1566
+ { status: 'ok', key: 'logs', value: `streaming on ${devLogStream.url}` },
1567
+ hubspotLogsLaneRow(devLogStream.hubspotLane),
1568
+ ]
1380
1569
  : []),
1381
1570
  ...(devTunnel ? [{ status: 'ok', key: 'tunnel', value: devTunnel.url }] : []),
1382
1571
  ...(migratedCardDevArtifacts
@@ -1501,6 +1690,17 @@ async function devCommandCore({ argv, root, json, }) {
1501
1690
  });
1502
1691
  return { exitCode: 0 };
1503
1692
  }
1693
+ function capabilityIdFromDevPath(path) {
1694
+ const match = /^\/_hsx\/(?:cards|invoke)\/([^/?]+)/.exec(path);
1695
+ if (!match?.[1])
1696
+ return undefined;
1697
+ try {
1698
+ return decodeURIComponent(match[1]);
1699
+ }
1700
+ catch {
1701
+ return match[1];
1702
+ }
1703
+ }
1504
1704
  async function managedDevStartCommand(input) {
1505
1705
  const reporter = input.reporter ??
1506
1706
  createReporter({ command: 'dev start', argv: input.argv, entry: !input.json });
@@ -2100,7 +2300,8 @@ async function startHsxDevLogStream(input) {
2100
2300
  ]);
2101
2301
  const bus = createDevEventBus();
2102
2302
  const authToken = randomBytes(32).toString('base64url');
2103
- const server = await startDevLogServer({ bus, port: input.port, authToken });
2303
+ const relay = input.relay ?? {};
2304
+ const server = await startDevLogServer({ bus, port: input.port, authToken, relay });
2104
2305
  const renderer = startDevStreamRenderer({
2105
2306
  bus,
2106
2307
  deferStatus: true,
@@ -2112,31 +2313,22 @@ async function startHsxDevLogStream(input) {
2112
2313
  const unsubscribeManagedEvents = managedEventWriter
2113
2314
  ? bus.subscribe((event) => managedEventWriter.log(event))
2114
2315
  : undefined;
2115
- // HubSpot-side lane: renders/proxy failures that never reach our Worker
2116
- // only exist in HubSpot's app logs — poll them into the same stream.
2117
- // Missing binding/credentials just means the lane stays off; `hs-x dev`
2118
- // must work without HubSpot credentials.
2119
- let hubspotPoller;
2120
- try {
2121
- const [{ resolveHubSpotLogsSource }, { startHubSpotLogsPoller }] = await Promise.all([
2122
- import('../hubspot-app-logs.js'),
2123
- import('../dev/hubspot-logs-poller.js'),
2124
- ]);
2125
- const resolution = await resolveHubSpotLogsSource({ cwd: input.root });
2126
- if (resolution.ok) {
2127
- hubspotPoller = startHubSpotLogsPoller({ bus, source: resolution.source });
2128
- }
2129
- }
2130
- catch {
2131
- // The HubSpot lane is additive; never fail the dev stream over it.
2132
- }
2316
+ const hubspotLane = startHubSpotLogLane({
2317
+ root: input.root,
2318
+ bus,
2319
+ account: input.hubspotAccount,
2320
+ });
2133
2321
  return {
2134
2322
  url: server.url,
2135
2323
  authToken,
2136
2324
  renderer,
2137
2325
  bus,
2326
+ get hubspotLane() {
2327
+ return hubspotLane.state;
2328
+ },
2329
+ relay,
2138
2330
  async cleanup() {
2139
- hubspotPoller?.stop();
2331
+ hubspotLane.stop();
2140
2332
  unsubscribeManagedEvents?.();
2141
2333
  renderer.stop();
2142
2334
  await Promise.all([server.close(), managedEventWriter?.close()]);
@@ -2191,7 +2383,13 @@ async function probeHubSpotUiNodes(input) {
2191
2383
  accountId: portalId,
2192
2384
  }, input.profile ? { profile: input.profile } : {});
2193
2385
  const candidates = listHubSpotDevComponentCandidates(ir.intermediateNodesIndexedByUid);
2194
- return { uids: candidates.map((candidate) => candidate.uid), candidates, portalId };
2386
+ const application = findHubSpotDevApplication(ir.intermediateNodesIndexedByUid);
2387
+ return {
2388
+ uids: candidates.map((candidate) => candidate.uid),
2389
+ candidates,
2390
+ portalId,
2391
+ ...(application ? { application } : {}),
2392
+ };
2195
2393
  }
2196
2394
  catch {
2197
2395
  // IR translate failed — caller falls back to capability-only behavior.
@@ -2216,6 +2414,9 @@ async function maybeStartHubSpotDevSession(input) {
2216
2414
  selectedNodeUids: input.selectedNodeUids,
2217
2415
  ...(profile ? { profile } : {}),
2218
2416
  preserveState: input.argv.includes('--preserve-state'),
2417
+ localProxyMode: resolveLocalProxyMode(input.argv),
2418
+ ...(input.relayUrl ? { relayUrl: input.relayUrl } : {}),
2419
+ ...(input.onRelayReady ? { onRelayReady: input.onRelayReady } : {}),
2219
2420
  onHeartbeatError: (failure) => {
2220
2421
  const warning = describeHeartbeatFailure(failure, portalId);
2221
2422
  if (warning && !input.json)
@@ -2225,6 +2426,16 @@ async function maybeStartHubSpotDevSession(input) {
2225
2426
  input.reporter
2226
2427
  .step('HubSpot UI-extension session')
2227
2428
  .ok(`id ${session.sessionId} · ${session.nodeUids.length} component${session.nodeUids.length === 1 ? '' : 's'}`);
2429
+ if (!input.json) {
2430
+ if (session.frontendLogs.unavailableReason) {
2431
+ input.reporter.warn('HSX_W_DEV_FRONTEND_LOGS_UNAVAILABLE', session.frontendLogs.unavailableReason);
2432
+ }
2433
+ for (const probe of session.localProxy.mappings) {
2434
+ if (probe.applied && probe.reachable)
2435
+ continue;
2436
+ input.reporter.warn('HSX_W_DEV_LOCAL_PROXY_UNREACHABLE', `local.json proxies ${probe.from} → ${probe.to}: ${describeLocalProxyIssue(session.localProxy, probe)}`);
2437
+ }
2438
+ }
2228
2439
  return session;
2229
2440
  }
2230
2441
  catch (error) {
@@ -2953,6 +3164,61 @@ function formatDevExpiry(now, expiresAt) {
2953
3164
  function isFlagValue(value) {
2954
3165
  return value !== undefined && value.length > 0 && !value.startsWith('-');
2955
3166
  }
3167
+ /**
3168
+ * The HubSpot-side lane is the only place renders and proxy executions show
3169
+ * up, so its state must be on the checklist: a silent lane looks identical to
3170
+ * a broken one.
3171
+ */
3172
+ function hubspotLogsLaneRow(lane) {
3173
+ if (lane.active) {
3174
+ return {
3175
+ key: 'HubSpot app logs',
3176
+ value: `app ${lane.appId} · renders + proxy executions`,
3177
+ detail: `read from account ${lane.developerAccountId} every ${HUBSPOT_LOGS_POLL_INTERVAL_MS / 1000}s; entries appear shortly after the card acts`,
3178
+ status: 'ok',
3179
+ };
3180
+ }
3181
+ if (lane.pending)
3182
+ return { key: 'HubSpot app logs', value: 'connecting', detail: lane.reason };
3183
+ return { key: 'HubSpot app logs', value: 'off', detail: lane.reason, status: 'warn' };
3184
+ }
3185
+ function describeTestingChoiceForRow(choice) {
3186
+ switch (choice) {
3187
+ case 'developer-test':
3188
+ return 'developer test account';
3189
+ case 'sandbox':
3190
+ return 'sandbox';
3191
+ default:
3192
+ return 'production';
3193
+ }
3194
+ }
3195
+ /**
3196
+ * Resolve the deployed app and count its non-test installs with the project
3197
+ * account's own credentials — the same lookup `hs project dev` performs.
3198
+ */
3199
+ async function checkProductionInstallsForDev(input) {
3200
+ try {
3201
+ const projectConfig = JSON.parse(await readFile(join(input.root, 'hsproject.json'), 'utf8'));
3202
+ if (!projectConfig.name) {
3203
+ return { ok: false, reason: 'hsproject.json has no project name to look the app up by.' };
3204
+ }
3205
+ const [{ createHubSpotDeveloperClient }, { checkHubSpotProductionInstalls }] = await Promise.all([import('@hs-x/hubspot-cli'), import('../dev/production-app-check.js')]);
3206
+ const client = createHubSpotDeveloperClient({
3207
+ developerAccountId: input.projectAccount.portalId,
3208
+ ...(input.projectAccount.personalAccessKey
3209
+ ? { personalAccessKey: input.projectAccount.personalAccessKey }
3210
+ : {}),
3211
+ });
3212
+ return await checkHubSpotProductionInstalls({
3213
+ client,
3214
+ projectName: projectConfig.name,
3215
+ appUid: input.application.uid,
3216
+ });
3217
+ }
3218
+ catch (cause) {
3219
+ return { ok: false, reason: cause instanceof Error ? cause.message : String(cause) };
3220
+ }
3221
+ }
2956
3222
  function resolveFlag(argv, flag) {
2957
3223
  const index = argv.indexOf(flag);
2958
3224
  if (index !== -1) {
@@ -3021,6 +3287,7 @@ const projectAccountOption = Options.text('project-account').pipe(Options.option
3021
3287
  const testingAccountOption = Options.text('testing-account').pipe(Options.optional);
3022
3288
  const profileOption = Options.text('profile').pipe(Options.withAlias('p'), Options.optional);
3023
3289
  const preserveStateOption = Options.boolean('preserve-state').pipe(Options.withDefault(false));
3290
+ const localProxyOption = Options.choice('local-proxy', ['auto', 'always', 'never']).pipe(Options.withDefault('auto'));
3024
3291
  const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
3025
3292
  const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
3026
3293
  const accountIdOption = Options.text('account-id').pipe(Options.optional);
@@ -3069,6 +3336,7 @@ export const devCmd = Command.make('dev', {
3069
3336
  testingAccount: testingAccountOption,
3070
3337
  profile: profileOption,
3071
3338
  preserveState: preserveStateOption,
3339
+ localProxy: localProxyOption,
3072
3340
  targetOrigin: targetOriginOption,
3073
3341
  telemetryOrigin: telemetryOriginOption,
3074
3342
  accountId: accountIdOption,
@@ -3148,6 +3416,8 @@ export const devCmd = Command.make('dev', {
3148
3416
  argv.push('--profile', opts.profile.value);
3149
3417
  if (opts.preserveState)
3150
3418
  argv.push('--preserve-state');
3419
+ if (opts.localProxy !== 'auto')
3420
+ argv.push('--local-proxy', opts.localProxy);
3151
3421
  if (Option.isSome(opts.targetOrigin)) {
3152
3422
  argv.push('--target-origin', opts.targetOrigin.value);
3153
3423
  }
@@ -3320,4 +3590,48 @@ async function devInvokeRemote(args) {
3320
3590
  reporter.done(ok ? 'Invoked' : 'Failed', ok ? 0 : 1);
3321
3591
  return { exitCode: ok ? 0 : 1 };
3322
3592
  }
3593
+ /**
3594
+ * `<app root>/local.json` proxy mappings switch HubSpot's dev server into local
3595
+ * proxy mode for `hubspot.fetch`; a mapping whose target is down fails every
3596
+ * fetch to that origin with an empty error, so show each one up front.
3597
+ */
3598
+ function resolveLocalProxyMode(argv) {
3599
+ return resolveHubSpotLocalProxyMode(resolveFlag(argv, '--local-proxy'));
3600
+ }
3601
+ /**
3602
+ * One short row per local.json mapping, plus one when proxy mode is on with no
3603
+ * mappings (`--local-proxy always`). Details stay one clause long; the mode
3604
+ * semantics live in `hs-x dev --help` and the guide.
3605
+ */
3606
+ function localProxyRows(localProxy) {
3607
+ const rows = localProxy.mappings.map((probe) => {
3608
+ const healthy = probe.applied && probe.reachable;
3609
+ return {
3610
+ key: 'local proxy',
3611
+ value: probe.applied ? `${probe.from} → ${probe.to}` : `off · ${probe.from}`,
3612
+ status: healthy ? 'ok' : 'warn',
3613
+ ...(healthy ? {} : { detail: describeLocalProxyIssue(localProxy, probe) }),
3614
+ };
3615
+ });
3616
+ if (localProxy.active && localProxy.mappings.length === 0) {
3617
+ rows.push({
3618
+ key: 'local proxy',
3619
+ value: 'on · no local.json mappings',
3620
+ detail: localProxy.signed
3621
+ ? 'hubspot.fetch leaves this machine signed with the app secret'
3622
+ : 'hubspot.fetch leaves this machine unsigned; set HUBSPOT_CLIENT_SECRET to sign',
3623
+ status: localProxy.signed ? 'ok' : 'warn',
3624
+ });
3625
+ }
3626
+ return rows;
3627
+ }
3628
+ function describeLocalProxyIssue(localProxy, probe) {
3629
+ if (localProxy.mode === 'never') {
3630
+ return 'ignored (--local-proxy never); hubspot.fetch stays on HubSpot proxy';
3631
+ }
3632
+ if (!probe.applied) {
3633
+ return `${probe.to} not listening; hubspot.fetch stays on HubSpot proxy — start it and restart, or --local-proxy always`;
3634
+ }
3635
+ return `${probe.to} not listening — every hubspot.fetch to this origin fails until it starts (--local-proxy always)`;
3636
+ }
3323
3637
  //# sourceMappingURL=dev.js.map