@hs-x/cli 0.3.7 → 0.4.0

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 (130) hide show
  1. package/README.md +120 -0
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +38 -10
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/command-catalog.d.ts +2 -2
  6. package/dist/command-catalog.d.ts.map +1 -1
  7. package/dist/command-catalog.js +1 -0
  8. package/dist/command-catalog.js.map +1 -1
  9. package/dist/commands/completion.d.ts.map +1 -1
  10. package/dist/commands/completion.js +268 -16
  11. package/dist/commands/completion.js.map +1 -1
  12. package/dist/commands/connect.d.ts.map +1 -1
  13. package/dist/commands/connect.js +177 -20
  14. package/dist/commands/connect.js.map +1 -1
  15. package/dist/commands/control-plane-read.d.ts +28 -0
  16. package/dist/commands/control-plane-read.d.ts.map +1 -1
  17. package/dist/commands/control-plane-read.js +134 -28
  18. package/dist/commands/control-plane-read.js.map +1 -1
  19. package/dist/commands/deploy.d.ts +5 -0
  20. package/dist/commands/deploy.d.ts.map +1 -1
  21. package/dist/commands/deploy.js +605 -102
  22. package/dist/commands/deploy.js.map +1 -1
  23. package/dist/commands/dev.d.ts +19 -1
  24. package/dist/commands/dev.d.ts.map +1 -1
  25. package/dist/commands/dev.js +1335 -83
  26. package/dist/commands/dev.js.map +1 -1
  27. package/dist/commands/doctor.d.ts.map +1 -1
  28. package/dist/commands/doctor.js +27 -10
  29. package/dist/commands/doctor.js.map +1 -1
  30. package/dist/commands/help-command.d.ts.map +1 -1
  31. package/dist/commands/help-command.js +145 -35
  32. package/dist/commands/help-command.js.map +1 -1
  33. package/dist/commands/project.d.ts +107 -0
  34. package/dist/commands/project.d.ts.map +1 -0
  35. package/dist/commands/project.js +586 -0
  36. package/dist/commands/project.js.map +1 -0
  37. package/dist/commands/status.d.ts.map +1 -1
  38. package/dist/commands/status.js +15 -5
  39. package/dist/commands/status.js.map +1 -1
  40. package/dist/commands/validate.d.ts +19 -0
  41. package/dist/commands/validate.d.ts.map +1 -1
  42. package/dist/commands/validate.js +26 -7
  43. package/dist/commands/validate.js.map +1 -1
  44. package/dist/constants.d.ts +1 -1
  45. package/dist/constants.js +1 -1
  46. package/dist/dev/account-selection.d.ts +16 -0
  47. package/dist/dev/account-selection.d.ts.map +1 -0
  48. package/dist/dev/account-selection.js +22 -0
  49. package/dist/dev/account-selection.js.map +1 -0
  50. package/dist/dev/browser-log-bridge.d.ts +32 -0
  51. package/dist/dev/browser-log-bridge.d.ts.map +1 -0
  52. package/dist/dev/browser-log-bridge.js +156 -0
  53. package/dist/dev/browser-log-bridge.js.map +1 -0
  54. package/dist/dev/compat-shim.d.ts +3 -3
  55. package/dist/dev/compat-shim.d.ts.map +1 -1
  56. package/dist/dev/compat-shim.js +7 -6
  57. package/dist/dev/compat-shim.js.map +1 -1
  58. package/dist/dev/component-selection.d.ts +25 -0
  59. package/dist/dev/component-selection.d.ts.map +1 -0
  60. package/dist/dev/component-selection.js +79 -0
  61. package/dist/dev/component-selection.js.map +1 -0
  62. package/dist/dev/console-capture.d.ts +18 -0
  63. package/dist/dev/console-capture.d.ts.map +1 -0
  64. package/dist/dev/console-capture.js +57 -0
  65. package/dist/dev/console-capture.js.map +1 -0
  66. package/dist/dev/event-bus.d.ts +1 -1
  67. package/dist/dev/event-bus.js +1 -1
  68. package/dist/dev/frontend-log-relay.d.ts +27 -0
  69. package/dist/dev/frontend-log-relay.d.ts.map +1 -0
  70. package/dist/dev/frontend-log-relay.js +102 -0
  71. package/dist/dev/frontend-log-relay.js.map +1 -0
  72. package/dist/dev/keyboard.d.ts +1 -0
  73. package/dist/dev/keyboard.d.ts.map +1 -1
  74. package/dist/dev/keyboard.js +9 -3
  75. package/dist/dev/keyboard.js.map +1 -1
  76. package/dist/dev/log-server.js +3 -0
  77. package/dist/dev/log-server.js.map +1 -1
  78. package/dist/dev/managed-session.d.ts +124 -0
  79. package/dist/dev/managed-session.d.ts.map +1 -0
  80. package/dist/dev/managed-session.js +597 -0
  81. package/dist/dev/managed-session.js.map +1 -0
  82. package/dist/dev/port-process.d.ts +21 -0
  83. package/dist/dev/port-process.d.ts.map +1 -0
  84. package/dist/dev/port-process.js +145 -0
  85. package/dist/dev/port-process.js.map +1 -0
  86. package/dist/dev/session-manager.d.ts +11 -1
  87. package/dist/dev/session-manager.d.ts.map +1 -1
  88. package/dist/dev/session-manager.js +62 -12
  89. package/dist/dev/session-manager.js.map +1 -1
  90. package/dist/dev/stream-event-format.d.ts +14 -0
  91. package/dist/dev/stream-event-format.d.ts.map +1 -0
  92. package/dist/dev/stream-event-format.js +262 -0
  93. package/dist/dev/stream-event-format.js.map +1 -0
  94. package/dist/dev/stream-filter.d.ts.map +1 -1
  95. package/dist/dev/stream-filter.js +16 -1
  96. package/dist/dev/stream-filter.js.map +1 -1
  97. package/dist/dev/stream-renderer.d.ts +4 -2
  98. package/dist/dev/stream-renderer.d.ts.map +1 -1
  99. package/dist/dev/stream-renderer.js +224 -84
  100. package/dist/dev/stream-renderer.js.map +1 -1
  101. package/dist/errors-registry.d.ts.map +1 -1
  102. package/dist/errors-registry.js +213 -0
  103. package/dist/errors-registry.js.map +1 -1
  104. package/dist/hubspot-app-logs.d.ts +38 -5
  105. package/dist/hubspot-app-logs.d.ts.map +1 -1
  106. package/dist/hubspot-app-logs.js +88 -17
  107. package/dist/hubspot-app-logs.js.map +1 -1
  108. package/dist/hubspot-auth.d.ts +19 -1
  109. package/dist/hubspot-auth.d.ts.map +1 -1
  110. package/dist/hubspot-auth.js +228 -83
  111. package/dist/hubspot-auth.js.map +1 -1
  112. package/dist/hubspot-developer-client.d.ts +87 -0
  113. package/dist/hubspot-developer-client.d.ts.map +1 -1
  114. package/dist/hubspot-developer-client.js +232 -1
  115. package/dist/hubspot-developer-client.js.map +1 -1
  116. package/dist/hubspot-project-archive.d.ts +21 -0
  117. package/dist/hubspot-project-archive.d.ts.map +1 -0
  118. package/dist/hubspot-project-archive.js +281 -0
  119. package/dist/hubspot-project-archive.js.map +1 -0
  120. package/dist/hubspot-project.d.ts +32 -0
  121. package/dist/hubspot-project.d.ts.map +1 -0
  122. package/dist/hubspot-project.js +269 -0
  123. package/dist/hubspot-project.js.map +1 -0
  124. package/dist/local-project-picker.js +1 -1
  125. package/dist/local-project-picker.js.map +1 -1
  126. package/dist/output-context.js +1 -1
  127. package/dist/output-context.js.map +1 -1
  128. package/dist/services/hubspot-dev-client.js +3 -0
  129. package/dist/services/hubspot-dev-client.js.map +1 -1
  130. package/package.json +14 -12
@@ -1,7 +1,9 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { randomBytes } from 'node:crypto';
3
+ import { closeSync, existsSync, openSync } from 'node:fs';
2
4
  import { readFile, readdir, stat } from 'node:fs/promises';
3
5
  import { createServer } from 'node:http';
4
- import { join } from 'node:path';
6
+ import { join, resolve as resolvePath } from 'node:path';
5
7
  import * as Args from '@effect/cli/Args';
6
8
  import * as Command from '@effect/cli/Command';
7
9
  import * as Options from '@effect/cli/Options';
@@ -13,13 +15,20 @@ import { cwdOption, jsonOption, runHandler, runQuarantined } from '../cli/kit.js
13
15
  import { formatConfigUrl, loadCliConfig } from '../config.js';
14
16
  import { CLI_VERSION } from '../constants.js';
15
17
  import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
18
+ import { selectHubSpotDirectDevAccounts } from '../dev/account-selection.js';
19
+ import { listHubSpotDevComponentCandidates, resolveHubSpotDevComponentSelectors, } from '../dev/component-selection.js';
20
+ import { createManagedDevEventWriter, createManagedDevSession, markManagedDevSessionReady, markManagedDevSessionState, prepareManagedDevSessionRestart, publicManagedDevSession, readManagedDevEvents, readManagedDevSession, reconcileManagedDevSession, setManagedDevSessionPid, stopManagedDevSessionProcess, waitForManagedDevSessionOwner, } from '../dev/managed-session.js';
21
+ import { findListeningProcess, isHubSpotDevPortConflict, stopProcessCommand, terminateProcessGracefully, } from '../dev/port-process.js';
16
22
  import { executeCliHttp } from '../effect-http.js';
23
+ import { discoverHubSpotAccounts, exchangePak } from '../hubspot-auth.js';
24
+ import { detectLocalProjectMode, validateHubSpotProject } from '../hubspot-project.js';
17
25
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
18
26
  import { requestMutationConsent } from '../mutation-consent.js';
19
27
  import { isMachineOutput } from '../output-context.js';
20
28
  import { isInteractive, promptConfirm, promptMultiSelect, promptSelect } from '../prompt.js';
21
29
  import { createReporter } from '../reporter/index.js';
22
30
  import { Cwd } from '../services/cwd.js';
31
+ import { Reporter as ReporterService } from '../services/reporter.js';
23
32
  async function hostedHttp(input) {
24
33
  const headers = { ...(input.headers ?? {}) };
25
34
  let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
@@ -35,6 +44,21 @@ function isRecord(value) {
35
44
  function isErrnoException(value) {
36
45
  return value instanceof Error && typeof value.code === 'string';
37
46
  }
47
+ function describeDevSessionFailure(cause) {
48
+ const error = isRecord(cause) ? cause : undefined;
49
+ const message = cause instanceof Error ? cause.message : String(cause);
50
+ const status = typeof error?.status === 'number' ? `HTTP ${error.status}` : undefined;
51
+ const data = isRecord(error?.data) ? error.data : undefined;
52
+ const responseMessage = typeof data?.message === 'string' ? data.message : undefined;
53
+ const validationErrors = Array.isArray(error?.validationErrors)
54
+ ? error.validationErrors.filter((item) => typeof item === 'string')
55
+ : [];
56
+ const correlationId = typeof error?.correlationId === 'string' ? `correlation ${error.correlationId}` : undefined;
57
+ return [...new Set([message, status, responseMessage, ...validationErrors, correlationId])]
58
+ .filter((item) => Boolean(item))
59
+ .join(' · ');
60
+ }
61
+ const HUBSPOT_UI_DEV_PORT = 5173;
38
62
  function makeDevLogPaint(env, stderr) {
39
63
  const isTTY = Boolean(stderr.isTTY);
40
64
  const color = isTTY && env.NO_COLOR !== '1';
@@ -179,7 +203,512 @@ async function resolveDefaultPortalId(input) {
179
203
  }
180
204
  return undefined;
181
205
  }
182
- export async function devCommand({ argv, root, json, }) {
206
+ async function recoverHubSpotDevPort(input) {
207
+ const listener = await findListeningProcess(HUBSPOT_UI_DEV_PORT);
208
+ if (!listener)
209
+ return { kind: 'none' };
210
+ const summary = `Port ${listener.port} is held by PID ${listener.pid}${listener.command ? ` (${listener.command})` : ''}.`;
211
+ const recovery = stopProcessCommand(listener.pid);
212
+ if (input.json) {
213
+ write(`${JSON.stringify({
214
+ schema_version: 1,
215
+ command: 'dev',
216
+ ok: false,
217
+ error: {
218
+ code: 'HSX_E_DEV_PORT_IN_USE',
219
+ message: summary,
220
+ process: listener,
221
+ recovery,
222
+ },
223
+ }, null, 2)}\n`);
224
+ return { kind: 'exit', result: { exitCode: 1 } };
225
+ }
226
+ input.reporter.header('HubSpot dev server already running');
227
+ input.reporter.rows([
228
+ { key: 'port', value: String(listener.port), status: 'warn' },
229
+ { key: 'pid', value: String(listener.pid) },
230
+ ...(listener.cwd ? [{ key: 'directory', value: listener.cwd }] : []),
231
+ ...(listener.command ? [{ key: 'command', value: listener.command }] : []),
232
+ ]);
233
+ if (!isInteractive()) {
234
+ input.reporter.error('HSX_E_DEV_PORT_IN_USE', summary, {
235
+ hint: `Stop that exact process with \`${recovery}\`, or rerun in an interactive terminal to confirm and retry automatically.`,
236
+ docs_url: 'https://hs-x.dev/errors/HSX_E_DEV_PORT_IN_USE',
237
+ });
238
+ input.reporter.done(undefined, 1);
239
+ return { kind: 'exit', result: { exitCode: 1 } };
240
+ }
241
+ const confirmed = await promptConfirm({
242
+ message: `Stop PID ${listener.pid} and retry this dev session?`,
243
+ default: false,
244
+ });
245
+ if (confirmed === undefined) {
246
+ return { kind: 'exit', result: cancelledResult(input.argv, 'dev') };
247
+ }
248
+ if (!confirmed) {
249
+ input.reporter.info(`Left PID ${listener.pid} running.`);
250
+ input.reporter.done('Dev session not started', 1);
251
+ return { kind: 'exit', result: { exitCode: 1 } };
252
+ }
253
+ const step = input.reporter.step(`Stopping PID ${listener.pid}`);
254
+ const stopped = await terminateProcessGracefully(listener.pid);
255
+ if (!stopped.ok) {
256
+ step.warn(stopped.message ?? 'could not stop process');
257
+ input.reporter.error('HSX_E_DEV_PORT_IN_USE', `Could not stop PID ${listener.pid}; it was left running.`, { hint: `Stop it manually with \`${recovery}\`, then retry.` });
258
+ input.reporter.done(undefined, 1);
259
+ return { kind: 'exit', result: { exitCode: 1 } };
260
+ }
261
+ step.ok('stopped; retrying');
262
+ return { kind: 'stopped' };
263
+ }
264
+ async function hubSpotDirectDevCommand({ argv, root, json, }) {
265
+ const reporter = createReporter({ command: 'dev', argv, entry: !json });
266
+ const profile = resolveFlag(argv, '--profile');
267
+ const validation = await validateHubSpotProject({ root, ...(profile ? { profile } : {}) });
268
+ if (!validation.ok) {
269
+ const result = {
270
+ ok: false,
271
+ command: 'dev',
272
+ mode: 'hubspot-direct',
273
+ error: 'HubSpot project validation failed.',
274
+ diagnostics: validation.diagnostics,
275
+ };
276
+ if (json)
277
+ write(`${JSON.stringify(result, null, 2)}\n`);
278
+ else {
279
+ reporter.header('HubSpot direct');
280
+ reporter.rows(validation.diagnostics.map((diagnostic) => ({
281
+ key: diagnostic.code,
282
+ value: diagnostic.message,
283
+ status: diagnostic.severity === 'error' ? 'fail' : 'warn',
284
+ ...(diagnostic.file ? { detail: diagnostic.file } : {}),
285
+ })));
286
+ reporter.done('Validation failed', 1);
287
+ }
288
+ return { exitCode: 1 };
289
+ }
290
+ const discovered = await discoverHubSpotAccounts(root);
291
+ const accounts = discovered?.accounts ?? [];
292
+ const requestedProjectAccount = resolveFlag(argv, '--project-account') ??
293
+ resolveFlag(argv, '--account') ??
294
+ resolveFlag(argv, '--portal');
295
+ const requestedTestingAccount = resolveFlag(argv, '--testing-account');
296
+ const { projectAccount, testingAccount: selectedTestingAccount } = selectHubSpotDirectDevAccounts({
297
+ accounts,
298
+ ...(requestedProjectAccount ? { requestedProjectAccount } : {}),
299
+ ...(requestedTestingAccount ? { requestedTestingAccount } : {}),
300
+ });
301
+ if (requestedProjectAccount && !projectAccount) {
302
+ const message = `HubSpot project account "${requestedProjectAccount}" is not configured. Run \`hs account list\` or pass a configured --project-account.`;
303
+ if (json) {
304
+ write(`${JSON.stringify({
305
+ schema_version: 1,
306
+ command: 'dev',
307
+ ok: false,
308
+ error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
309
+ }, null, 2)}\n`);
310
+ }
311
+ else {
312
+ reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
313
+ reporter.done(undefined, 1);
314
+ }
315
+ return { exitCode: 1 };
316
+ }
317
+ if (!projectAccount) {
318
+ const message = 'HubSpot-direct dev needs an authenticated HubSpot project account. Run `hs account auth`, select a default with `hs account use`, or pass --project-account.';
319
+ if (json) {
320
+ write(`${JSON.stringify({
321
+ schema_version: 1,
322
+ command: 'dev',
323
+ ok: false,
324
+ error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
325
+ }, null, 2)}\n`);
326
+ }
327
+ else {
328
+ reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
329
+ reporter.done(undefined, 1);
330
+ }
331
+ return { exitCode: 1 };
332
+ }
333
+ if (requestedTestingAccount && !selectedTestingAccount) {
334
+ const message = `HubSpot testing account "${requestedTestingAccount}" is not configured. Run \`hs account list\` or pass a configured --testing-account.`;
335
+ if (json) {
336
+ write(`${JSON.stringify({
337
+ schema_version: 1,
338
+ command: 'dev',
339
+ ok: false,
340
+ error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
341
+ }, null, 2)}\n`);
342
+ }
343
+ else {
344
+ reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
345
+ reporter.done(undefined, 1);
346
+ }
347
+ return { exitCode: 1 };
348
+ }
349
+ const testingAccount = selectedTestingAccount ?? projectAccount;
350
+ if (testingAccount.personalAccessKey) {
351
+ try {
352
+ const exchanged = await exchangePak(testingAccount.personalAccessKey);
353
+ if (exchanged.portalId !== testingAccount.portalId) {
354
+ throw new Error(`the configured PAK belongs to account ${exchanged.portalId}, not ${testingAccount.portalId}`);
355
+ }
356
+ }
357
+ catch (cause) {
358
+ 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.`;
359
+ if (json) {
360
+ write(`${JSON.stringify({
361
+ schema_version: 1,
362
+ command: 'dev',
363
+ ok: false,
364
+ error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
365
+ }, null, 2)}\n`);
366
+ }
367
+ else {
368
+ reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
369
+ reporter.done(undefined, 1);
370
+ }
371
+ return { exitCode: 1 };
372
+ }
373
+ }
374
+ // Check the fixed HubSpot UI-extension port before translating the project;
375
+ // otherwise a conflict is discovered only after the comparatively expensive
376
+ // IR probe/build path has already run.
377
+ const existingDevServer = await recoverHubSpotDevPort({ argv, json, reporter });
378
+ if (existingDevServer.kind === 'exit')
379
+ return existingDevServer.result;
380
+ const uiExtensionNodes = await probeHubSpotUiNodes({
381
+ root,
382
+ portalIdFlag: String(projectAccount.portalId),
383
+ accountId: undefined,
384
+ ...(profile ? { profile } : {}),
385
+ });
386
+ const requestedComponents = resolveFlags(argv, '--component');
387
+ const resolvedComponents = resolveHubSpotDevComponentSelectors(uiExtensionNodes?.candidates ?? [], requestedComponents);
388
+ if (!resolvedComponents.ok) {
389
+ if (json) {
390
+ write(`${JSON.stringify({
391
+ schema_version: 1,
392
+ command: 'dev',
393
+ ok: false,
394
+ error: { code: 'HSX_E_DEV_SESSION_NO_NODES', message: resolvedComponents.message },
395
+ }, null, 2)}\n`);
396
+ }
397
+ else {
398
+ reporter.error('HSX_E_DEV_SESSION_NO_NODES', resolvedComponents.message);
399
+ reporter.done(undefined, 1);
400
+ }
401
+ return { exitCode: 1 };
402
+ }
403
+ let selectedNodeUids = uiExtensionNodes && uiExtensionNodes.candidates.length > 0
404
+ ? resolvedComponents.uids
405
+ : undefined;
406
+ if (requestedComponents.length === 0 &&
407
+ uiExtensionNodes &&
408
+ uiExtensionNodes.candidates.length > 1 &&
409
+ !json &&
410
+ isInteractive()) {
411
+ const picked = await promptMultiSelect({
412
+ message: `Which HubSpot components should run in dev mode? (testing account ${testingAccount.portalId})`,
413
+ initial: [],
414
+ minSelected: 1,
415
+ options: uiExtensionNodes.candidates.map((candidate) => ({
416
+ value: candidate.uid,
417
+ label: candidate.label,
418
+ description: candidate.description,
419
+ })),
420
+ });
421
+ if (picked === undefined)
422
+ return cancelledResult(argv, 'dev');
423
+ selectedNodeUids = picked;
424
+ }
425
+ let directDevLogStream;
426
+ if (!json) {
427
+ const hsxLogPortValue = resolveFlag(argv, '--hsx-log-port') ?? '9099';
428
+ const hsxLogPort = Number(hsxLogPortValue);
429
+ if (!Number.isInteger(hsxLogPort) || hsxLogPort < 1 || hsxLogPort > 65535) {
430
+ const code = 'HSX_E_INPUT_BAD_FIELD';
431
+ reporter.error(code, `--hsx-log-port must be an integer between 1 and 65535 (got \`${hsxLogPortValue}\`).`, {
432
+ hint: 'Example: hs-x dev --hsx-log-port 9099',
433
+ docs_url: `https://hs-x.dev/errors/${code}`,
434
+ });
435
+ reporter.done(undefined, 10);
436
+ return { exitCode: 10 };
437
+ }
438
+ const { parseOnlySelectors } = await import('../dev/stream-filter.js');
439
+ const onlyParsed = parseOnlySelectors(resolveFlags(argv, '--only'));
440
+ if (!onlyParsed.ok) {
441
+ const code = 'HSX_E_DEV_BAD_ONLY';
442
+ reporter.error(code, onlyParsed.message, {
443
+ hint: 'Selectors: frontend, hubspot, errors, warnings, or free text. Repeat --only to combine.',
444
+ docs_url: `https://hs-x.dev/errors/${code}`,
445
+ });
446
+ reporter.done(undefined, 2);
447
+ return { exitCode: 2 };
448
+ }
449
+ const managedSessionId = resolveFlag(argv, '--managed-session');
450
+ directDevLogStream = await startHsxDevLogStream({
451
+ port: hsxLogPort,
452
+ root,
453
+ reporter,
454
+ filter: onlyParsed.filter,
455
+ ...(managedSessionId ? { managedSessionId } : {}),
456
+ });
457
+ }
458
+ const { startDevSession } = await import('../dev/session-manager.js');
459
+ const startDirectSession = () => startDevSession({
460
+ projectAccountId: projectAccount.portalId,
461
+ testingAccountId: testingAccount.portalId,
462
+ projectDir: root,
463
+ ...(selectedNodeUids ? { selectedNodeUids } : {}),
464
+ ...(profile ? { profile } : {}),
465
+ ...(directDevLogStream
466
+ ? {
467
+ onFrontendLog: (entry) => {
468
+ directDevLogStream?.bus.emit({
469
+ lane: 'frontend',
470
+ level: entry.level,
471
+ ts: entry.ts,
472
+ msg: entry.msg,
473
+ ...(entry.source ? { source: entry.source } : {}),
474
+ ...(entry.fields ? { fields: entry.fields } : {}),
475
+ });
476
+ },
477
+ onBackendLog: (entry) => {
478
+ directDevLogStream?.bus.emit({
479
+ lane: 'backend',
480
+ level: entry.level,
481
+ ts: Date.now(),
482
+ msg: entry.message,
483
+ source: `console.${entry.method}`,
484
+ });
485
+ },
486
+ }
487
+ : {}),
488
+ });
489
+ let session;
490
+ let sessionFailure;
491
+ try {
492
+ session = await startDirectSession();
493
+ }
494
+ catch (initialCause) {
495
+ sessionFailure = initialCause;
496
+ if (isHubSpotDevPortConflict(initialCause)) {
497
+ const recovery = await recoverHubSpotDevPort({ argv, json, reporter });
498
+ if (recovery.kind === 'exit') {
499
+ try {
500
+ await directDevLogStream?.cleanup();
501
+ }
502
+ catch {
503
+ // The port-conflict response is already complete.
504
+ }
505
+ return recovery.result;
506
+ }
507
+ if (recovery.kind === 'stopped') {
508
+ try {
509
+ session = await startDirectSession();
510
+ sessionFailure = undefined;
511
+ }
512
+ catch (retryCause) {
513
+ sessionFailure = retryCause;
514
+ }
515
+ }
516
+ }
517
+ }
518
+ if (!session) {
519
+ if (directDevLogStream) {
520
+ try {
521
+ await directDevLogStream.cleanup();
522
+ }
523
+ catch {
524
+ // The dev-session error below is the actionable failure.
525
+ }
526
+ }
527
+ const message = describeDevSessionFailure(sessionFailure);
528
+ if (json) {
529
+ write(`${JSON.stringify({
530
+ schema_version: 1,
531
+ command: 'dev',
532
+ ok: false,
533
+ error: { code: 'HSX_E_DEV_SESSION_REGISTER', message },
534
+ }, null, 2)}\n`);
535
+ }
536
+ else {
537
+ reporter.error('HSX_E_DEV_SESSION_REGISTER', message);
538
+ reporter.done(undefined, 1);
539
+ }
540
+ return { exitCode: 1 };
541
+ }
542
+ const managedSessionId = resolveFlag(argv, '--managed-session');
543
+ if (managedSessionId) {
544
+ await markManagedDevSessionReady(managedSessionId, {
545
+ projectAccountId: projectAccount.portalId,
546
+ testingAccountId: testingAccount.portalId,
547
+ components: session.nodeUids,
548
+ ...(directDevLogStream
549
+ ? { logPort: Number(resolveFlag(argv, '--hsx-log-port') ?? '9099') }
550
+ : {}),
551
+ });
552
+ }
553
+ const started = {
554
+ ok: true,
555
+ command: 'dev',
556
+ mode: 'hubspot-direct',
557
+ portalId: testingAccount.portalId,
558
+ projectAccountId: projectAccount.portalId,
559
+ testingAccountId: testingAccount.portalId,
560
+ sessionId: session.sessionId,
561
+ components: session.nodeUids,
562
+ controlPlaneUsed: false,
563
+ cloudflareUsed: false,
564
+ };
565
+ if (json)
566
+ write(`${JSON.stringify(started, null, 2)}\n`);
567
+ else {
568
+ reporter.header(`${projectAccount.name} → ${testingAccount.name}`);
569
+ reporter.rows([
570
+ { key: 'mode', value: 'HubSpot direct' },
571
+ { key: 'project-account', value: `${projectAccount.name} (${projectAccount.portalId})` },
572
+ { key: 'testing-account', value: `${testingAccount.name} (${testingAccount.portalId})` },
573
+ { key: 'ui-extensions', value: `session ${session.sessionId}`, status: 'ok' },
574
+ { key: 'components', value: String(session.nodeUids.length) },
575
+ ...(directDevLogStream
576
+ ? [
577
+ {
578
+ key: 'logs',
579
+ value: 'frontend + backend + HubSpot events',
580
+ detail: directDevLogStream.url,
581
+ status: 'ok',
582
+ },
583
+ ]
584
+ : []),
585
+ { key: 'Cloudflare', value: 'not used' },
586
+ { key: 'HS-X control plane', value: 'not used' },
587
+ ]);
588
+ if (!directDevLogStream?.renderer.hasStatusLine())
589
+ reporter.info('Press Ctrl+C to stop');
590
+ }
591
+ const directKeyboard = directDevLogStream
592
+ ? (await import('../dev/keyboard.js')).attachDevKeyboard({
593
+ renderer: directDevLogStream.renderer,
594
+ onQuit: () => process.kill(process.pid, 'SIGINT'),
595
+ })
596
+ : undefined;
597
+ directDevLogStream?.renderer.ready();
598
+ await new Promise((resolveClose) => {
599
+ let closing = false;
600
+ const close = async () => {
601
+ if (closing)
602
+ return;
603
+ closing = true;
604
+ directKeyboard?.stop();
605
+ if (managedSessionId) {
606
+ try {
607
+ await markManagedDevSessionState(managedSessionId, 'stopping', undefined, {
608
+ expectedPid: process.pid,
609
+ });
610
+ }
611
+ catch {
612
+ // Continue cleanup even if the managed state record is unavailable.
613
+ }
614
+ }
615
+ try {
616
+ await session.cleanup();
617
+ }
618
+ finally {
619
+ try {
620
+ await directDevLogStream?.cleanup();
621
+ }
622
+ finally {
623
+ if (managedSessionId) {
624
+ try {
625
+ await markManagedDevSessionState(managedSessionId, 'stopped', undefined, {
626
+ expectedPid: process.pid,
627
+ });
628
+ }
629
+ catch {
630
+ // The process exit will be reconciled by the next status request.
631
+ }
632
+ }
633
+ resolveClose();
634
+ }
635
+ }
636
+ };
637
+ process.once('SIGINT', close);
638
+ process.once('SIGTERM', close);
639
+ });
640
+ return { exitCode: 0 };
641
+ }
642
+ const MANAGED_DEV_SUBCOMMANDS = new Set(['start', 'logs', 'stop', 'restart']);
643
+ const KNOWN_DEV_SUBCOMMANDS = new Set([...MANAGED_DEV_SUBCOMMANDS, 'invoke', 'cleanup', 'status']);
644
+ const MANAGED_DEV_LANES = new Set(['frontend', 'backend', 'request', 'hubspot']);
645
+ const MANAGED_DEV_LEVELS = new Set(['debug', 'info', 'warn', 'error']);
646
+ export async function devCommand({ argv, root, json, reporter, }) {
647
+ const managedSessionId = resolveFlag(argv, '--managed-session');
648
+ if (managedSessionId) {
649
+ await waitForManagedDevSessionOwner(managedSessionId);
650
+ try {
651
+ const result = await devCommandCore({ argv, root, json: false });
652
+ if (result.exitCode !== 0) {
653
+ const current = await readManagedDevSession(managedSessionId);
654
+ if (current.state === 'starting') {
655
+ await markManagedDevSessionState(managedSessionId, 'failed', `hs-x dev exited with code ${result.exitCode} during startup.`, { expectedGeneration: current.generation, expectedPid: process.pid });
656
+ }
657
+ }
658
+ return result;
659
+ }
660
+ catch (cause) {
661
+ const message = cause instanceof Error ? cause.message : String(cause);
662
+ try {
663
+ const current = await readManagedDevSession(managedSessionId);
664
+ if (current.state !== 'stopped' && current.state !== 'failed') {
665
+ await markManagedDevSessionState(managedSessionId, 'failed', message, {
666
+ expectedPid: process.pid,
667
+ });
668
+ }
669
+ }
670
+ catch {
671
+ // Preserve the original startup failure if the state file is unavailable.
672
+ }
673
+ throw cause;
674
+ }
675
+ }
676
+ const subcommand = argv[1];
677
+ if (subcommand && !subcommand.startsWith('-') && !KNOWN_DEV_SUBCOMMANDS.has(subcommand)) {
678
+ const outputReporter = reporter ?? createReporter({ command: 'dev', argv });
679
+ outputReporter.error('HSX_E_INPUT_INVALID', `Unknown dev subcommand "${subcommand}".`, {
680
+ hint: 'Expected start, logs, stop, restart, invoke, cleanup, or status. Run `hs-x dev --help`.',
681
+ });
682
+ outputReporter.done(undefined, 2);
683
+ return { exitCode: 2 };
684
+ }
685
+ if (subcommand &&
686
+ KNOWN_DEV_SUBCOMMANDS.has(subcommand) &&
687
+ subcommand !== 'invoke' &&
688
+ argv[2] !== undefined &&
689
+ !argv[2].startsWith('-')) {
690
+ const outputReporter = reporter ?? createReporter({ command: `dev ${subcommand}`, argv });
691
+ outputReporter.error('HSX_E_INPUT_INVALID', `Unexpected argument "${argv[2]}" after \`hs-x dev ${subcommand}\`.`);
692
+ outputReporter.done(undefined, 2);
693
+ return { exitCode: 2 };
694
+ }
695
+ const reporterInput = reporter ? { reporter } : {};
696
+ if (subcommand === 'logs')
697
+ return managedDevLogsCommand({ argv, json, ...reporterInput });
698
+ if (subcommand === 'stop')
699
+ return managedDevStopCommand({ argv, json, ...reporterInput });
700
+ if (subcommand === 'restart') {
701
+ return managedDevRestartCommand({ argv, json, ...reporterInput });
702
+ }
703
+ if (subcommand === 'status' && resolveFlag(argv, '--session')) {
704
+ return managedDevStatusCommand({ argv, json, ...reporterInput });
705
+ }
706
+ if (subcommand === 'start' || argv.includes('--detach')) {
707
+ return managedDevStartCommand({ argv, root, json, ...reporterInput });
708
+ }
709
+ return devCommandCore({ argv, root, json });
710
+ }
711
+ async function devCommandCore({ argv, root, json, }) {
183
712
  if (argv[1] === 'cleanup') {
184
713
  return await devCleanupCommand({ argv, json });
185
714
  }
@@ -195,7 +724,7 @@ export async function devCommand({ argv, root, json, }) {
195
724
  prompts: { select: promptSelect },
196
725
  });
197
726
  if (selected && selected.root !== root) {
198
- return await devCommand({ argv, root: selected.root, json });
727
+ return await devCommandCore({ argv, root: selected.root, json });
199
728
  }
200
729
  }
201
730
  catch (error) {
@@ -207,6 +736,9 @@ export async function devCommand({ argv, root, json, }) {
207
736
  if (argv[1] === 'invoke') {
208
737
  return await devInvokeCommand({ argv, root, json });
209
738
  }
739
+ if ((await detectLocalProjectMode(root)) === 'hubspot-direct') {
740
+ return await hubSpotDirectDevCommand({ argv, root, json });
741
+ }
210
742
  const portFlag = resolveFlag(argv, '--port');
211
743
  const port = Number(portFlag ?? '8787');
212
744
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -665,11 +1197,13 @@ export async function devCommand({ argv, root, json, }) {
665
1197
  reporter.done(undefined, 2);
666
1198
  return { exitCode: 2 };
667
1199
  }
1200
+ const managedLogSessionId = resolveFlag(argv, '--managed-session');
668
1201
  const devLogStream = await startHsxDevLogStream({
669
1202
  port: hsxLogPort,
670
1203
  root,
671
1204
  reporter,
672
1205
  filter: onlyParsed.filter,
1206
+ ...(managedLogSessionId ? { managedSessionId: managedLogSessionId } : {}),
673
1207
  });
674
1208
  if (devLogStream) {
675
1209
  const paint = makeDevLogPaint(process.env, process.stderr);
@@ -729,6 +1263,14 @@ export async function devCommand({ argv, root, json, }) {
729
1263
  }),
730
1264
  });
731
1265
  }
1266
+ const managedSessionId = resolveFlag(argv, '--managed-session');
1267
+ if (managedSessionId) {
1268
+ await markManagedDevSessionReady(managedSessionId, {
1269
+ components: selectedUiNodeUids ?? capabilityIds,
1270
+ devPort: port,
1271
+ ...(devLogStream ? { logPort: hsxLogPort } : {}),
1272
+ });
1273
+ }
732
1274
  // Railed startup checklist: identity header, then one row per surface.
733
1275
  reporter.header(subject);
734
1276
  reporter.rows([
@@ -796,8 +1338,22 @@ export async function devCommand({ argv, root, json, }) {
796
1338
  : undefined;
797
1339
  devLogStream?.renderer.ready();
798
1340
  await new Promise((resolveClose) => {
1341
+ let closing = false;
799
1342
  const close = async () => {
1343
+ if (closing)
1344
+ return;
1345
+ closing = true;
800
1346
  keyboard?.stop();
1347
+ if (managedSessionId) {
1348
+ try {
1349
+ await markManagedDevSessionState(managedSessionId, 'stopping', undefined, {
1350
+ expectedPid: process.pid,
1351
+ });
1352
+ }
1353
+ catch {
1354
+ // Continue cleanup even if the managed state record is unavailable.
1355
+ }
1356
+ }
801
1357
  // Signal-driven shutdown can terminate the Bun process before awaited
802
1358
  // cleanup settles. Restore temporarily stamped migration sources
803
1359
  // synchronously first; the remaining network/process cleanup is
@@ -850,13 +1406,615 @@ export async function devCommand({ argv, root, json, }) {
850
1406
  // best-effort: sidecar shutdown failures should not block exit.
851
1407
  }
852
1408
  }
853
- server.close(() => resolveClose());
1409
+ server.close(async () => {
1410
+ if (managedSessionId) {
1411
+ try {
1412
+ await markManagedDevSessionState(managedSessionId, 'stopped', undefined, {
1413
+ expectedPid: process.pid,
1414
+ });
1415
+ }
1416
+ catch {
1417
+ // The process exit will be reconciled by the next status request.
1418
+ }
1419
+ }
1420
+ resolveClose();
1421
+ });
854
1422
  };
855
1423
  process.once('SIGINT', close);
856
1424
  process.once('SIGTERM', close);
857
1425
  });
858
1426
  return { exitCode: 0 };
859
1427
  }
1428
+ async function managedDevStartCommand(input) {
1429
+ const reporter = input.reporter ??
1430
+ createReporter({ command: 'dev start', argv: input.argv, entry: !input.json });
1431
+ let waitMs;
1432
+ try {
1433
+ waitMs = parsePositiveSeconds(input.argv, '--wait-seconds', 60) * 1_000;
1434
+ }
1435
+ catch (cause) {
1436
+ return managedDevCommandFailure(reporter, cause, 'HSX_E_INPUT_BAD_FIELD');
1437
+ }
1438
+ const root = resolvePath(input.root);
1439
+ const consent = await requestMutationConsent({
1440
+ yes: input.argv.some((argument) => argument === '--yes' || argument === '-y'),
1441
+ machineOutput: input.json || isMachineOutput(),
1442
+ interactive: isInteractive(),
1443
+ message: `Start an agent-managed background dev session in ${root}?`,
1444
+ confirm: promptConfirm,
1445
+ });
1446
+ if (consent !== 'proceed') {
1447
+ const message = consent === 'require-yes'
1448
+ ? 'Starting a managed dev session in non-interactive or machine-output mode requires --yes.'
1449
+ : 'Start cancelled.';
1450
+ reporter.data({ target: { root } });
1451
+ reporter.error('HSX_E_INPUT_INVALID', message, {
1452
+ hint: 'Review the target root, then rerun `hs-x dev start --yes` with the same dev flags.',
1453
+ });
1454
+ reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
1455
+ return { exitCode: consent === 'require-yes' ? 2 : 130 };
1456
+ }
1457
+ const launchArgv = managedDevLaunchArgv(input.argv);
1458
+ const session = await createManagedDevSession({
1459
+ root,
1460
+ launchArgv,
1461
+ });
1462
+ try {
1463
+ await launchManagedDevChild(session);
1464
+ const result = await waitForManagedDevStartup(session.sessionId, waitMs);
1465
+ const safe = await publicManagedDevSession(result);
1466
+ if (safe.state === 'failed' || safe.state === 'stopped') {
1467
+ reporter.data({ session: safe });
1468
+ reporter.error('HSX_E_DEV_MANAGED_START', safe.error ?? `Managed dev session ${safe.sessionId} stopped before it became ready.`, { hint: `Inspect ${safe.stderrPath} for the complete startup output.` });
1469
+ reporter.done(undefined, 1);
1470
+ return { exitCode: 1 };
1471
+ }
1472
+ renderManagedDevSession(reporter, safe, 'started');
1473
+ reporter.data({
1474
+ session: safe,
1475
+ commands: managedDevSessionCommands(safe.sessionId),
1476
+ });
1477
+ if (safe.state !== 'ready') {
1478
+ reporter.warn('HSX_W_DEV_MANAGED_STARTING', `Managed dev session ${safe.sessionId} is still starting. Check it with \`hs-x dev status --session ${safe.sessionId}\`.`);
1479
+ }
1480
+ else if (!safe.logPort) {
1481
+ reporter.warn('HSX_W_DEV_MANAGED_LOG_STREAM', 'The dev server is ready, but its unified log sidecar is unavailable. Inspect stderrPath and restart with a free --hsx-log-port.');
1482
+ }
1483
+ reporter.done(safe.state === 'ready' ? 'Dev session ready' : 'Dev session still starting');
1484
+ return { exitCode: 0 };
1485
+ }
1486
+ catch (cause) {
1487
+ const message = cause instanceof Error ? cause.message : String(cause);
1488
+ try {
1489
+ await markManagedDevSessionState(session.sessionId, 'failed', message, {
1490
+ expectedGeneration: session.generation,
1491
+ expectedStates: ['starting'],
1492
+ });
1493
+ }
1494
+ catch {
1495
+ // Keep the launch error as the primary failure.
1496
+ }
1497
+ reporter.error('HSX_E_DEV_MANAGED_START', message, {
1498
+ hint: `Inspect ${session.stderrPath} for startup output.`,
1499
+ });
1500
+ reporter.done(undefined, 1);
1501
+ return { exitCode: 1 };
1502
+ }
1503
+ }
1504
+ async function managedDevStatusCommand(input) {
1505
+ const reporter = input.reporter ?? createReporter({ command: 'dev status', argv: input.argv });
1506
+ const sessionId = requireManagedSessionId(input.argv, reporter);
1507
+ if (!sessionId)
1508
+ return { exitCode: 2 };
1509
+ try {
1510
+ const safe = await publicManagedDevSession(await reconcileManagedDevSession(sessionId));
1511
+ renderManagedDevSession(reporter, safe, 'status');
1512
+ reporter.data({ session: safe, commands: managedDevSessionCommands(sessionId) });
1513
+ reporter.done();
1514
+ return { exitCode: 0 };
1515
+ }
1516
+ catch (cause) {
1517
+ return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_SESSION');
1518
+ }
1519
+ }
1520
+ async function managedDevStopCommand(input) {
1521
+ const reporter = input.reporter ?? createReporter({ command: 'dev stop', argv: input.argv });
1522
+ const sessionId = requireManagedSessionId(input.argv, reporter);
1523
+ if (!sessionId)
1524
+ return { exitCode: 2 };
1525
+ try {
1526
+ const current = await reconcileManagedDevSession(sessionId);
1527
+ const safeCurrent = await publicManagedDevSession(current);
1528
+ renderManagedDevSession(reporter, safeCurrent, 'stop target');
1529
+ if (current.state === 'stopped' || current.state === 'failed') {
1530
+ reporter.data({ session: safeCurrent });
1531
+ reporter.done('Dev session is already stopped');
1532
+ return { exitCode: 0 };
1533
+ }
1534
+ const consent = await requestMutationConsent({
1535
+ yes: input.argv.includes('--yes'),
1536
+ machineOutput: input.json || isMachineOutput(),
1537
+ interactive: isInteractive(),
1538
+ message: `Stop managed dev session ${sessionId} (PID ${current.pid})?`,
1539
+ confirm: promptConfirm,
1540
+ });
1541
+ if (consent !== 'proceed') {
1542
+ const message = consent === 'require-yes'
1543
+ ? 'Stopping a managed dev session in non-interactive or machine-output mode requires --yes.'
1544
+ : 'Stop cancelled.';
1545
+ reporter.data({ session: safeCurrent });
1546
+ reporter.error('HSX_E_INPUT_INVALID', message, {
1547
+ hint: `Review the session above, then run \`hs-x dev stop --session ${sessionId} --yes\`.`,
1548
+ });
1549
+ reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
1550
+ return { exitCode: consent === 'require-yes' ? 2 : 130 };
1551
+ }
1552
+ const stopped = await stopManagedDevSessionProcess(sessionId, {
1553
+ force: input.argv.includes('--force'),
1554
+ });
1555
+ const safe = await publicManagedDevSession(stopped);
1556
+ reporter.data({ session: safe });
1557
+ reporter.step(`Stopped managed dev session ${sessionId}`).ok(`PID ${current.pid}`);
1558
+ reporter.done('Dev session stopped');
1559
+ return { exitCode: 0 };
1560
+ }
1561
+ catch (cause) {
1562
+ return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_STOP');
1563
+ }
1564
+ }
1565
+ async function managedDevRestartCommand(input) {
1566
+ const reporter = input.reporter ?? createReporter({ command: 'dev restart', argv: input.argv });
1567
+ const sessionId = requireManagedSessionId(input.argv, reporter);
1568
+ if (!sessionId)
1569
+ return { exitCode: 2 };
1570
+ try {
1571
+ const waitMs = parsePositiveSeconds(input.argv, '--wait-seconds', 60) * 1_000;
1572
+ const current = await reconcileManagedDevSession(sessionId);
1573
+ const safeCurrent = await publicManagedDevSession(current);
1574
+ renderManagedDevSession(reporter, safeCurrent, 'restart target');
1575
+ const consent = await requestMutationConsent({
1576
+ yes: input.argv.includes('--yes'),
1577
+ machineOutput: input.json || isMachineOutput(),
1578
+ interactive: isInteractive(),
1579
+ message: `Restart managed dev session ${sessionId}${safeCurrent.running ? ` (PID ${current.pid})` : ''}?`,
1580
+ confirm: promptConfirm,
1581
+ });
1582
+ if (consent !== 'proceed') {
1583
+ const message = consent === 'require-yes'
1584
+ ? 'Restarting a managed dev session in non-interactive or machine-output mode requires --yes.'
1585
+ : 'Restart cancelled.';
1586
+ reporter.data({ session: safeCurrent });
1587
+ reporter.error('HSX_E_INPUT_INVALID', message, {
1588
+ hint: `Review the session above, then run \`hs-x dev restart --session ${sessionId} --yes\`.`,
1589
+ });
1590
+ reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
1591
+ return { exitCode: consent === 'require-yes' ? 2 : 130 };
1592
+ }
1593
+ const restartBase = safeCurrent.running
1594
+ ? await stopManagedDevSessionProcess(sessionId, { force: input.argv.includes('--force') })
1595
+ : current;
1596
+ const restarting = await prepareManagedDevSessionRestart(sessionId, {
1597
+ expectedGeneration: restartBase.generation,
1598
+ expectedPid: restartBase.pid,
1599
+ expectedStates: [restartBase.state],
1600
+ });
1601
+ await launchManagedDevChild(restarting);
1602
+ const result = await waitForManagedDevStartup(sessionId, waitMs);
1603
+ const safe = await publicManagedDevSession(result);
1604
+ if (safe.state === 'failed' || safe.state === 'stopped') {
1605
+ reporter.data({ session: safe });
1606
+ reporter.error('HSX_E_DEV_MANAGED_RESTART', safe.error ?? `Managed dev session ${sessionId} stopped while restarting.`, { hint: `Inspect ${safe.stderrPath} for the complete restart output.` });
1607
+ reporter.done(undefined, 1);
1608
+ return { exitCode: 1 };
1609
+ }
1610
+ renderManagedDevSession(reporter, safe, 'restarted');
1611
+ reporter.data({ session: safe, commands: managedDevSessionCommands(sessionId) });
1612
+ if (safe.state === 'ready' && !safe.logPort) {
1613
+ reporter.warn('HSX_W_DEV_MANAGED_LOG_STREAM', 'The dev server restarted, but its unified log sidecar is unavailable. Inspect stderrPath and retry with a free --hsx-log-port.');
1614
+ }
1615
+ reporter.done(safe.state === 'ready' ? 'Dev session restarted' : 'Dev session still starting');
1616
+ return { exitCode: 0 };
1617
+ }
1618
+ catch (cause) {
1619
+ return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_RESTART');
1620
+ }
1621
+ }
1622
+ async function managedDevLogsCommand(input) {
1623
+ const reporter = input.reporter ?? createReporter({ command: 'dev logs', argv: input.argv });
1624
+ const sessionId = requireManagedSessionId(input.argv, reporter);
1625
+ if (!sessionId)
1626
+ return { exitCode: 2 };
1627
+ if (input.argv.includes('--follow') && input.json) {
1628
+ reporter.error('HSX_E_DEV_LOGS_OUTPUT', '`--follow` is an event stream and cannot be combined with `--json`.', { hint: 'Use `--json-stream` for machine-readable follow mode.' });
1629
+ reporter.done(undefined, 2);
1630
+ return { exitCode: 2 };
1631
+ }
1632
+ try {
1633
+ let session = await reconcileManagedDevSession(sessionId);
1634
+ const cursor = parseNonNegativeInteger(input.argv, '--cursor', 0);
1635
+ const limit = parsePositiveInteger(input.argv, '--limit', 200, 10_000);
1636
+ const filter = parseManagedDevEventFilter(input.argv);
1637
+ if (!input.argv.includes('--follow')) {
1638
+ const result = await readManagedDevEvents({ session, cursor, limit, filter });
1639
+ if (reporter.mode === 'json' || reporter.mode === 'ndjson') {
1640
+ reporter.data({
1641
+ session_id: sessionId,
1642
+ cursor,
1643
+ next_cursor: result.nextCursor,
1644
+ end_cursor: result.endCursor,
1645
+ events: result.events,
1646
+ });
1647
+ }
1648
+ else {
1649
+ reporter.header(`${sessionId} · ${result.events.length} event${result.events.length === 1 ? '' : 's'}`);
1650
+ const view = await startManagedDevLogView({ follow: false, filter });
1651
+ try {
1652
+ for (const match of result.events)
1653
+ view.emit(match.event);
1654
+ }
1655
+ finally {
1656
+ view.stop();
1657
+ }
1658
+ reporter.info(`Next cursor: ${result.nextCursor}`);
1659
+ }
1660
+ reporter.done();
1661
+ return { exitCode: 0 };
1662
+ }
1663
+ let nextCursor = cursor;
1664
+ let interrupted = false;
1665
+ let terminalSince;
1666
+ const interrupt = () => {
1667
+ interrupted = true;
1668
+ };
1669
+ process.once('SIGINT', interrupt);
1670
+ process.once('SIGTERM', interrupt);
1671
+ if (reporter.mode !== 'ndjson') {
1672
+ reporter.header(`${sessionId} · following from cursor ${cursor}`);
1673
+ }
1674
+ const followReadFilter = reporter.mode === 'ndjson' ? filter : managedDevFollowReadFilter(filter);
1675
+ let view;
1676
+ try {
1677
+ view =
1678
+ reporter.mode === 'ndjson'
1679
+ ? undefined
1680
+ : await startManagedDevLogView({ follow: true, filter, onQuit: interrupt });
1681
+ view?.ready();
1682
+ while (!interrupted) {
1683
+ const previousCursor = nextCursor;
1684
+ const result = await readManagedDevEvents({
1685
+ session,
1686
+ cursor: nextCursor,
1687
+ limit,
1688
+ ...(followReadFilter ? { filter: followReadFilter } : {}),
1689
+ });
1690
+ for (const match of result.events) {
1691
+ if (reporter.mode === 'ndjson') {
1692
+ reporter.event('dev.logs.event', {
1693
+ session_id: sessionId,
1694
+ cursor: match.cursor,
1695
+ event: match.event,
1696
+ });
1697
+ }
1698
+ else {
1699
+ view?.emit(match.event);
1700
+ }
1701
+ }
1702
+ nextCursor = result.nextCursor;
1703
+ session = await reconcileManagedDevSession(sessionId);
1704
+ const terminal = session.state === 'stopped' || session.state === 'failed';
1705
+ if (terminal && nextCursor >= result.endCursor) {
1706
+ terminalSince ??= Date.now();
1707
+ if (Date.now() - terminalSince >= 1_000)
1708
+ break;
1709
+ }
1710
+ else {
1711
+ terminalSince = undefined;
1712
+ }
1713
+ if (nextCursor >= result.endCursor || nextCursor === previousCursor)
1714
+ await delayMs(250);
1715
+ }
1716
+ }
1717
+ finally {
1718
+ process.removeListener('SIGINT', interrupt);
1719
+ process.removeListener('SIGTERM', interrupt);
1720
+ view?.stop();
1721
+ }
1722
+ if (reporter.mode === 'ndjson') {
1723
+ reporter.event('dev.logs.cursor', { session_id: sessionId, next_cursor: nextCursor });
1724
+ }
1725
+ else {
1726
+ reporter.info(`Detached at cursor ${nextCursor}`);
1727
+ }
1728
+ reporter.done();
1729
+ return { exitCode: 0 };
1730
+ }
1731
+ catch (cause) {
1732
+ return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_LOGS');
1733
+ }
1734
+ }
1735
+ function managedDevLaunchArgv(argv) {
1736
+ const output = [];
1737
+ const orchestrationValueFlags = new Set([
1738
+ '--managed-session',
1739
+ '--session',
1740
+ '--cursor',
1741
+ '--lane',
1742
+ '--level',
1743
+ '--search',
1744
+ '--since',
1745
+ '--limit',
1746
+ '--wait-seconds',
1747
+ ]);
1748
+ const orchestrationBooleanFlags = new Set([
1749
+ '--detach',
1750
+ '--follow',
1751
+ '--json',
1752
+ '--json-stream',
1753
+ '--yes',
1754
+ '-y',
1755
+ ]);
1756
+ for (let index = 0; index < argv.length; index += 1) {
1757
+ const value = argv[index];
1758
+ if (!value)
1759
+ continue;
1760
+ if (index === 1 && value === 'start')
1761
+ continue;
1762
+ if (orchestrationBooleanFlags.has(value))
1763
+ continue;
1764
+ if (orchestrationValueFlags.has(value)) {
1765
+ index += 1;
1766
+ continue;
1767
+ }
1768
+ if ([...orchestrationValueFlags].some((flag) => value.startsWith(`${flag}=`)))
1769
+ continue;
1770
+ output.push(value);
1771
+ }
1772
+ return output;
1773
+ }
1774
+ async function launchManagedDevChild(session) {
1775
+ const entryCandidate = process.argv[1];
1776
+ const entry = entryCandidate ? resolvePath(entryCandidate) : undefined;
1777
+ const entryArgs = entry && existsSync(entry) ? [entry] : [];
1778
+ const stdoutFd = openSync(session.stdoutPath, 'a', 0o600);
1779
+ const stderrFd = openSync(session.stderrPath, 'a', 0o600);
1780
+ try {
1781
+ const child = spawn(process.execPath, [...entryArgs, ...session.launchArgv, '--managed-session', session.sessionId], {
1782
+ cwd: session.root,
1783
+ env: process.env,
1784
+ detached: true,
1785
+ stdio: ['ignore', stdoutFd, stderrFd],
1786
+ });
1787
+ await new Promise((resolveSpawn, rejectSpawn) => {
1788
+ child.once('spawn', resolveSpawn);
1789
+ child.once('error', rejectSpawn);
1790
+ });
1791
+ if (!child.pid)
1792
+ throw new Error('Detached hs-x dev process did not report a PID.');
1793
+ try {
1794
+ await setManagedDevSessionPid(session.sessionId, child.pid, {
1795
+ expectedGeneration: session.generation,
1796
+ expectedPid: 0,
1797
+ expectedStates: ['starting'],
1798
+ });
1799
+ }
1800
+ catch (cause) {
1801
+ try {
1802
+ child.kill('SIGTERM');
1803
+ }
1804
+ catch {
1805
+ // The guarded state update is still the primary failure.
1806
+ }
1807
+ throw cause;
1808
+ }
1809
+ child.unref();
1810
+ return child.pid;
1811
+ }
1812
+ finally {
1813
+ closeSync(stdoutFd);
1814
+ closeSync(stderrFd);
1815
+ }
1816
+ }
1817
+ async function waitForManagedDevStartup(sessionId, timeoutMs) {
1818
+ const deadline = Date.now() + timeoutMs;
1819
+ let session = await reconcileManagedDevSession(sessionId);
1820
+ while (Date.now() < deadline && session.state === 'starting') {
1821
+ await delayMs(100);
1822
+ session = await reconcileManagedDevSession(sessionId);
1823
+ }
1824
+ return session;
1825
+ }
1826
+ function managedDevSessionCommands(sessionId) {
1827
+ return {
1828
+ logs: `hs-x dev logs --session ${sessionId} --follow`,
1829
+ logs_json: `hs-x dev logs --session ${sessionId} --follow --json-stream`,
1830
+ status: `hs-x dev status --session ${sessionId} --json`,
1831
+ restart: `hs-x dev restart --session ${sessionId} --yes`,
1832
+ stop: `hs-x dev stop --session ${sessionId} --yes`,
1833
+ };
1834
+ }
1835
+ function renderManagedDevSession(reporter, session, subject) {
1836
+ if (reporter.mode === 'json' || reporter.mode === 'ndjson')
1837
+ return;
1838
+ reporter.header(`${subject} · ${session.sessionId}`);
1839
+ reporter.rows([
1840
+ {
1841
+ key: 'state',
1842
+ value: session.state,
1843
+ status: session.state === 'ready' ? 'ok' : session.state === 'failed' ? 'fail' : 'warn',
1844
+ },
1845
+ { key: 'pid', value: session.pid > 1 ? String(session.pid) : 'not assigned' },
1846
+ { key: 'generation', value: String(session.generation) },
1847
+ { key: 'root', value: session.root },
1848
+ ...(session.projectAccountId
1849
+ ? [{ key: 'project-account', value: String(session.projectAccountId) }]
1850
+ : []),
1851
+ ...(session.testingAccountId
1852
+ ? [{ key: 'testing-account', value: String(session.testingAccountId) }]
1853
+ : []),
1854
+ ...(session.components
1855
+ ? [{ key: 'components', value: session.components.join(', ') || 'none' }]
1856
+ : []),
1857
+ ...(session.logPort ? [{ key: 'log-port', value: String(session.logPort) }] : []),
1858
+ ]);
1859
+ if (session.error)
1860
+ reporter.info(`Last error: ${session.error}`);
1861
+ reporter.info(`Logs: hs-x dev logs --session ${session.sessionId} --follow`);
1862
+ }
1863
+ function requireManagedSessionId(argv, reporter) {
1864
+ const sessionId = resolveFlag(argv, '--session');
1865
+ if (sessionId)
1866
+ return sessionId;
1867
+ reporter.error('HSX_E_INPUT_INVALID', 'A managed dev session id is required.', {
1868
+ hint: 'Pass the id returned by `hs-x dev start`, for example `--session dev_…`.',
1869
+ });
1870
+ reporter.done(undefined, 2);
1871
+ return undefined;
1872
+ }
1873
+ function managedDevCommandFailure(reporter, cause, code) {
1874
+ reporter.error(code, cause instanceof Error ? cause.message : String(cause));
1875
+ reporter.done(undefined, 1);
1876
+ return { exitCode: 1 };
1877
+ }
1878
+ function parseManagedDevEventFilter(argv) {
1879
+ const laneValues = resolveFlags(argv, '--lane');
1880
+ for (const lane of laneValues) {
1881
+ if (!MANAGED_DEV_LANES.has(lane)) {
1882
+ throw new Error(`Unknown --lane "${lane}". Expected frontend, backend, request, or hubspot.`);
1883
+ }
1884
+ }
1885
+ const levelValues = resolveFlags(argv, '--level');
1886
+ for (const level of levelValues) {
1887
+ if (!MANAGED_DEV_LEVELS.has(level)) {
1888
+ throw new Error(`Unknown --level "${level}". Expected debug, info, warn, or error.`);
1889
+ }
1890
+ }
1891
+ const sinceValue = resolveFlag(argv, '--since');
1892
+ const searchValue = resolveFlag(argv, '--search');
1893
+ return {
1894
+ ...(laneValues.length > 0
1895
+ ? { lanes: new Set(laneValues) }
1896
+ : {}),
1897
+ ...(levelValues.length > 0
1898
+ ? { levels: new Set(levelValues) }
1899
+ : {}),
1900
+ ...(searchValue ? { search: searchValue } : {}),
1901
+ ...(sinceValue ? { since: parseManagedSince(sinceValue) } : {}),
1902
+ };
1903
+ }
1904
+ function parseManagedSince(value) {
1905
+ const relative = /^(\d+)(ms|s|m|h|d)$/.exec(value);
1906
+ if (relative) {
1907
+ const amount = Number(relative[1]);
1908
+ const unit = relative[2];
1909
+ const multiplier = unit === 'ms'
1910
+ ? 1
1911
+ : unit === 's'
1912
+ ? 1_000
1913
+ : unit === 'm'
1914
+ ? 60_000
1915
+ : unit === 'h'
1916
+ ? 3_600_000
1917
+ : 86_400_000;
1918
+ return Date.now() - amount * multiplier;
1919
+ }
1920
+ const parsed = Date.parse(value);
1921
+ if (!Number.isFinite(parsed)) {
1922
+ throw new Error(`Invalid --since value "${value}". Use an ISO timestamp or a duration like 30s, 5m, or 2h.`);
1923
+ }
1924
+ return parsed;
1925
+ }
1926
+ function parseNonNegativeInteger(argv, flag, fallback) {
1927
+ const raw = resolveFlag(argv, flag);
1928
+ if (raw === undefined)
1929
+ return fallback;
1930
+ const value = Number(raw);
1931
+ if (!Number.isInteger(value) || value < 0)
1932
+ throw new Error(`${flag} must be a non-negative integer.`);
1933
+ return value;
1934
+ }
1935
+ function parsePositiveInteger(argv, flag, fallback, maximum = Number.MAX_SAFE_INTEGER) {
1936
+ const raw = resolveFlag(argv, flag);
1937
+ if (raw === undefined)
1938
+ return fallback;
1939
+ const value = Number(raw);
1940
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
1941
+ throw new Error(`${flag} must be an integer between 1 and ${maximum}.`);
1942
+ }
1943
+ return value;
1944
+ }
1945
+ function parsePositiveSeconds(argv, flag, fallback) {
1946
+ return parsePositiveInteger(argv, flag, fallback, 600);
1947
+ }
1948
+ async function startManagedDevLogView(input) {
1949
+ const [eventBusModule, keyboardModule, formatModule, rendererModule] = await Promise.all([
1950
+ import('../dev/event-bus.js'),
1951
+ import('../dev/keyboard.js'),
1952
+ import('../dev/stream-event-format.js'),
1953
+ import('../dev/stream-renderer.js'),
1954
+ ]);
1955
+ const bus = eventBusModule.createDevEventBus();
1956
+ const renderer = rendererModule.startDevStreamRenderer({
1957
+ bus,
1958
+ filter: managedDevEventFilterToStreamFilter(input.filter),
1959
+ ...(input.follow ? { deferStatus: true } : { isTTY: false }),
1960
+ });
1961
+ const keyboard = input.follow
1962
+ ? keyboardModule.attachDevKeyboard({
1963
+ renderer,
1964
+ onQuit: input.onQuit ?? (() => { }),
1965
+ })
1966
+ : undefined;
1967
+ return {
1968
+ emit(event) {
1969
+ bus.emit(formatModule.managedDevEventToDevEvent(event));
1970
+ },
1971
+ ready() {
1972
+ renderer.ready();
1973
+ },
1974
+ stop() {
1975
+ keyboard?.stop();
1976
+ renderer.stop();
1977
+ },
1978
+ };
1979
+ }
1980
+ function managedDevEventFilterToStreamFilter(filter) {
1981
+ const levels = filter.levels;
1982
+ const level = levels?.size === 1 && levels.has('error')
1983
+ ? 'error'
1984
+ : levels?.size === 2 && levels.has('warn') && levels.has('error')
1985
+ ? 'warn'
1986
+ : 'all';
1987
+ return {
1988
+ lanes: filter.lanes,
1989
+ level,
1990
+ texts: filter.search ? [filter.search] : [],
1991
+ };
1992
+ }
1993
+ /**
1994
+ * Human follow mode keeps lane/search/threshold filtering in the renderer so
1995
+ * keyboard controls can widen the retained view. Timestamp and exact level
1996
+ * sets that the threshold model cannot express remain storage-side. Machine
1997
+ * streams never use this split and retain their original cursor/filter path.
1998
+ */
1999
+ function managedDevFollowReadFilter(filter) {
2000
+ const levels = filter.levels;
2001
+ const rendererCanRepresentLevels = levels === undefined ||
2002
+ (levels.size === 1 && levels.has('error')) ||
2003
+ (levels.size === 2 && levels.has('warn') && levels.has('error')) ||
2004
+ (levels.size === 4 &&
2005
+ levels.has('debug') &&
2006
+ levels.has('info') &&
2007
+ levels.has('warn') &&
2008
+ levels.has('error'));
2009
+ const readFilter = {
2010
+ ...(filter.since !== undefined ? { since: filter.since } : {}),
2011
+ ...(!rendererCanRepresentLevels && levels ? { levels } : {}),
2012
+ };
2013
+ return Object.keys(readFilter).length > 0 ? readFilter : undefined;
2014
+ }
2015
+ function delayMs(ms) {
2016
+ return new Promise((resolve) => setTimeout(resolve, ms));
2017
+ }
860
2018
  async function startHsxDevLogStream(input) {
861
2019
  try {
862
2020
  const [{ createDevEventBus }, { startDevLogServer }, { startDevStreamRenderer }] = await Promise.all([
@@ -872,6 +2030,12 @@ async function startHsxDevLogStream(input) {
872
2030
  deferStatus: true,
873
2031
  ...(input.filter ? { filter: input.filter } : {}),
874
2032
  });
2033
+ const managedEventWriter = input.managedSessionId
2034
+ ? createManagedDevEventWriter(await readManagedDevSession(input.managedSessionId))
2035
+ : undefined;
2036
+ const unsubscribeManagedEvents = managedEventWriter
2037
+ ? bus.subscribe((event) => managedEventWriter.log(event))
2038
+ : undefined;
875
2039
  // HubSpot-side lane: renders/proxy failures that never reach our Worker
876
2040
  // only exist in HubSpot's app logs — poll them into the same stream.
877
2041
  // Missing binding/credentials just means the lane stays off; `hs-x dev`
@@ -903,8 +2067,9 @@ async function startHsxDevLogStream(input) {
903
2067
  bus,
904
2068
  async cleanup() {
905
2069
  hubspotPoller?.stop();
2070
+ unsubscribeManagedEvents?.();
906
2071
  renderer.stop();
907
- await server.close();
2072
+ await Promise.all([server.close(), managedEventWriter?.close()]);
908
2073
  },
909
2074
  };
910
2075
  }
@@ -954,8 +2119,9 @@ async function probeHubSpotUiNodes(input) {
954
2119
  projectSourceDir: pathJoin(input.root, projectConfig.srcDir),
955
2120
  platformVersion: projectConfig.platformVersion,
956
2121
  accountId: portalId,
957
- }, {});
958
- return { uids: Object.keys(ir.intermediateNodesIndexedByUid), portalId };
2122
+ }, input.profile ? { profile: input.profile } : {});
2123
+ const candidates = listHubSpotDevComponentCandidates(ir.intermediateNodesIndexedByUid);
2124
+ return { uids: candidates.map((candidate) => candidate.uid), candidates, portalId };
959
2125
  }
960
2126
  catch {
961
2127
  // IR translate failed — caller falls back to capability-only behavior.
@@ -973,10 +2139,12 @@ async function maybeStartHubSpotDevSession(input) {
973
2139
  }
974
2140
  try {
975
2141
  const { startDevSession } = await import('../dev/session-manager.js');
2142
+ const profile = resolveFlag(input.argv, '--profile');
976
2143
  const session = await startDevSession({
977
2144
  accountId: portalId,
978
2145
  projectDir: input.root,
979
2146
  selectedNodeUids: input.selectedNodeUids,
2147
+ ...(profile ? { profile } : {}),
980
2148
  });
981
2149
  input.reporter
982
2150
  .step('HubSpot UI-extension session')
@@ -984,7 +2152,7 @@ async function maybeStartHubSpotDevSession(input) {
984
2152
  return session;
985
2153
  }
986
2154
  catch (error) {
987
- input.reporter.warn('HSX_W_DEV_HUBSPOT_SESSION_FAILED', `Could not start UI-extension dev session: ${error instanceof Error ? error.message : String(error)}`);
2155
+ input.reporter.warn('HSX_W_DEV_HUBSPOT_SESSION_FAILED', `Could not start UI-extension dev session: ${describeDevSessionFailure(error)}`);
988
2156
  return undefined;
989
2157
  }
990
2158
  }
@@ -1757,11 +2925,11 @@ function write(message) {
1757
2925
  // as a silent `exitWith(code)` (the legacy body renders its own output) so
1758
2926
  // `runHandler` does not re-render.
1759
2927
  //
1760
- // The first positional argument selects the `cleanup` / `status` subcommands
2928
+ // The first positional argument selects the managed-session and legacy subcommands
1761
2929
  // the legacy body branches on via `argv[1]`.
1762
2930
  // A positional (not an option) so `hs-x dev cleanup` / `dev status` parse — the
1763
2931
  // legacy body branches on `argv[1]`.
1764
- const devSubcommandArg = Args.text({ name: 'subcommand' }).pipe(Args.withDescription('Optional dev subcommand: invoke | cleanup | status'), Args.optional);
2932
+ const devSubcommandArg = Args.text({ name: 'subcommand' }).pipe(Args.withDescription('Optional dev subcommand: start | logs | stop | restart | invoke | cleanup | status'), Args.optional);
1765
2933
  // Second positional: the capability id for `dev invoke <capability-id>`.
1766
2934
  const devTargetArg = Args.text({ name: 'target' }).pipe(Args.withDescription('Capability id for `dev invoke`'), Args.optional);
1767
2935
  const devInputOption = Options.text('input').pipe(Options.optional);
@@ -1772,6 +2940,10 @@ const devRemoteOption = Options.boolean('remote').pipe(Options.withDefault(false
1772
2940
  const devUrlOption = Options.text('url').pipe(Options.optional);
1773
2941
  const portOption = Options.text('port').pipe(Options.optional);
1774
2942
  const portalOption = Options.text('portal').pipe(Options.optional);
2943
+ const accountOption = Options.text('account').pipe(Options.optional);
2944
+ const projectAccountOption = Options.text('project-account').pipe(Options.optional);
2945
+ const testingAccountOption = Options.text('testing-account').pipe(Options.optional);
2946
+ const profileOption = Options.text('profile').pipe(Options.withAlias('p'), Options.optional);
1775
2947
  const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
1776
2948
  const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
1777
2949
  const accountIdOption = Options.text('account-id').pipe(Options.optional);
@@ -1779,12 +2951,24 @@ const projectIdOption = Options.text('project-id').pipe(Options.optional);
1779
2951
  const developerIdOption = Options.text('developer-id').pipe(Options.optional);
1780
2952
  const userIdOption = Options.text('user-id').pipe(Options.optional);
1781
2953
  const sessionIdOption = Options.text('session-id').pipe(Options.optional);
2954
+ const managedSessionOption = Options.text('session').pipe(Options.optional);
2955
+ const internalManagedSessionOption = Options.text('managed-session').pipe(Options.optional);
1782
2956
  const ttlSecondsOption = Options.text('ttl-seconds').pipe(Options.optional);
1783
2957
  const hsxLogPortOption = Options.text('hsx-log-port').pipe(Options.optional);
1784
2958
  const controlPlaneUrlOption = Options.text('control-plane-url').pipe(Options.optional);
1785
2959
  const runtimeControlPlaneUrlOption = Options.text('runtime-control-plane-url').pipe(Options.optional);
1786
2960
  const capabilityOption = Options.text('capability').pipe(Options.repeated);
2961
+ const componentOption = Options.text('component').pipe(Options.repeated);
1787
2962
  const onlyOption = Options.text('only').pipe(Options.repeated);
2963
+ const laneOption = Options.text('lane').pipe(Options.repeated);
2964
+ const levelOption = Options.text('level').pipe(Options.repeated);
2965
+ const searchOption = Options.text('search').pipe(Options.optional);
2966
+ const sinceOption = Options.text('since').pipe(Options.optional);
2967
+ const cursorOption = Options.text('cursor').pipe(Options.optional);
2968
+ const limitOption = Options.text('limit').pipe(Options.optional);
2969
+ const waitSecondsOption = Options.text('wait-seconds').pipe(Options.optional);
2970
+ const detachOption = Options.boolean('detach').pipe(Options.withDefault(false));
2971
+ const followOption = Options.boolean('follow').pipe(Options.withDefault(false));
1788
2972
  const pickOption = Options.boolean('pick').pipe(Options.withDefault(false));
1789
2973
  const noHubspotOption = Options.boolean('no-hubspot').pipe(Options.withDefault(false));
1790
2974
  const noUiExtensionsOption = Options.boolean('no-ui-extensions').pipe(Options.withDefault(false));
@@ -1803,6 +2987,10 @@ export const devCmd = Command.make('dev', {
1803
2987
  url: devUrlOption,
1804
2988
  port: portOption,
1805
2989
  portal: portalOption,
2990
+ account: accountOption,
2991
+ projectAccount: projectAccountOption,
2992
+ testingAccount: testingAccountOption,
2993
+ profile: profileOption,
1806
2994
  targetOrigin: targetOriginOption,
1807
2995
  telemetryOrigin: telemetryOriginOption,
1808
2996
  accountId: accountIdOption,
@@ -1810,90 +2998,151 @@ export const devCmd = Command.make('dev', {
1810
2998
  developerId: developerIdOption,
1811
2999
  userId: userIdOption,
1812
3000
  sessionId: sessionIdOption,
3001
+ session: managedSessionOption,
3002
+ managedSession: internalManagedSessionOption,
1813
3003
  ttlSeconds: ttlSecondsOption,
1814
3004
  hsxLogPort: hsxLogPortOption,
1815
3005
  controlPlaneUrl: controlPlaneUrlOption,
1816
3006
  runtimeControlPlaneUrl: runtimeControlPlaneUrlOption,
1817
3007
  capability: capabilityOption,
3008
+ component: componentOption,
1818
3009
  only: onlyOption,
3010
+ lane: laneOption,
3011
+ level: levelOption,
3012
+ search: searchOption,
3013
+ since: sinceOption,
3014
+ cursor: cursorOption,
3015
+ limit: limitOption,
3016
+ waitSeconds: waitSecondsOption,
3017
+ detach: detachOption,
3018
+ follow: followOption,
1819
3019
  pick: pickOption,
1820
3020
  noHubspot: noHubspotOption,
1821
3021
  noUiExtensions: noUiExtensionsOption,
1822
3022
  yes: yesOption,
1823
3023
  force: forceOption,
1824
- }, (opts) => runHandler('dev', { json: opts.json }, Effect.gen(function* () {
1825
- // root: injected cwd, overridden by --cwd.
1826
- const injectedCwd = yield* Cwd;
1827
- const root = Option.getOrElse(opts.cwd, () => injectedCwd);
1828
- // Reconstruct the legacy argv the quarantined body parses (it reads
1829
- // its flags off argv via `resolveFlag`/`resolveFlags` and branches on
1830
- // `argv[1]` for the cleanup/status subcommands).
1831
- const argv = ['dev'];
1832
- if (Option.isSome(opts.subcommand))
1833
- argv.push(opts.subcommand.value);
1834
- if (Option.isSome(opts.target))
1835
- argv.push(opts.target.value);
1836
- if (Option.isSome(opts.input))
1837
- argv.push('--input', opts.input.value);
1838
- if (Option.isSome(opts.object))
1839
- argv.push('--object', opts.object.value);
1840
- if (Option.isSome(opts.install))
1841
- argv.push('--install', opts.install.value);
1842
- if (opts.last)
1843
- argv.push('--last');
1844
- if (opts.remote)
1845
- argv.push('--remote');
1846
- if (Option.isSome(opts.url))
1847
- argv.push('--url', opts.url.value);
1848
- if (opts.json)
1849
- argv.push('--json');
1850
- if (Option.isSome(opts.port))
1851
- argv.push('--port', opts.port.value);
1852
- if (Option.isSome(opts.portal))
1853
- argv.push('--portal', opts.portal.value);
1854
- if (Option.isSome(opts.targetOrigin)) {
1855
- argv.push('--target-origin', opts.targetOrigin.value);
1856
- }
1857
- if (Option.isSome(opts.telemetryOrigin)) {
1858
- argv.push('--telemetry-origin', opts.telemetryOrigin.value);
1859
- }
1860
- if (Option.isSome(opts.accountId))
1861
- argv.push('--account-id', opts.accountId.value);
1862
- if (Option.isSome(opts.projectId))
1863
- argv.push('--project-id', opts.projectId.value);
1864
- if (Option.isSome(opts.developerId)) {
1865
- argv.push('--developer-id', opts.developerId.value);
1866
- }
1867
- if (Option.isSome(opts.userId))
1868
- argv.push('--user-id', opts.userId.value);
1869
- if (Option.isSome(opts.sessionId))
1870
- argv.push('--session-id', opts.sessionId.value);
1871
- if (Option.isSome(opts.ttlSeconds))
1872
- argv.push('--ttl-seconds', opts.ttlSeconds.value);
1873
- if (Option.isSome(opts.hsxLogPort))
1874
- argv.push('--hsx-log-port', opts.hsxLogPort.value);
1875
- if (Option.isSome(opts.controlPlaneUrl)) {
1876
- argv.push('--control-plane-url', opts.controlPlaneUrl.value);
1877
- }
1878
- if (Option.isSome(opts.runtimeControlPlaneUrl)) {
1879
- argv.push('--runtime-control-plane-url', opts.runtimeControlPlaneUrl.value);
1880
- }
1881
- for (const capability of opts.capability)
1882
- argv.push('--capability', capability);
1883
- for (const only of opts.only)
1884
- argv.push('--only', only);
1885
- if (opts.pick)
1886
- argv.push('--pick');
1887
- if (opts.noHubspot)
1888
- argv.push('--no-hubspot');
1889
- if (opts.noUiExtensions)
1890
- argv.push('--no-ui-extensions');
1891
- if (opts.yes)
1892
- argv.push('--yes');
1893
- if (opts.force)
1894
- argv.push('--force');
1895
- yield* runQuarantined('dev', opts.json, () => devCommand({ argv, root, json: opts.json }));
1896
- })));
3024
+ }, (opts) => {
3025
+ const subcommand = Option.isSome(opts.subcommand) ? opts.subcommand.value : undefined;
3026
+ const managedSubcommand = subcommand !== undefined &&
3027
+ (MANAGED_DEV_SUBCOMMANDS.has(subcommand) ||
3028
+ (subcommand === 'status' && Option.isSome(opts.session)));
3029
+ const reportCommand = managedSubcommand ? `dev ${subcommand}` : 'dev';
3030
+ return runHandler(reportCommand, { json: opts.json }, Effect.gen(function* () {
3031
+ const effectReporter = yield* ReporterService;
3032
+ // root: injected cwd, overridden by --cwd.
3033
+ const injectedCwd = yield* Cwd;
3034
+ const root = Option.getOrElse(opts.cwd, () => injectedCwd);
3035
+ // Reconstruct the legacy argv the quarantined body parses (it reads
3036
+ // its flags off argv via `resolveFlag`/`resolveFlags` and branches on
3037
+ // `argv[1]` for the cleanup/status subcommands).
3038
+ const argv = ['dev'];
3039
+ if (Option.isSome(opts.subcommand))
3040
+ argv.push(opts.subcommand.value);
3041
+ if (Option.isSome(opts.target))
3042
+ argv.push(opts.target.value);
3043
+ if (Option.isSome(opts.input))
3044
+ argv.push('--input', opts.input.value);
3045
+ if (Option.isSome(opts.object))
3046
+ argv.push('--object', opts.object.value);
3047
+ if (Option.isSome(opts.install))
3048
+ argv.push('--install', opts.install.value);
3049
+ if (opts.last)
3050
+ argv.push('--last');
3051
+ if (opts.remote)
3052
+ argv.push('--remote');
3053
+ if (Option.isSome(opts.url))
3054
+ argv.push('--url', opts.url.value);
3055
+ if (opts.json)
3056
+ argv.push('--json');
3057
+ if (Option.isSome(opts.port))
3058
+ argv.push('--port', opts.port.value);
3059
+ if (Option.isSome(opts.portal))
3060
+ argv.push('--portal', opts.portal.value);
3061
+ if (Option.isSome(opts.account))
3062
+ argv.push('--account', opts.account.value);
3063
+ if (Option.isSome(opts.projectAccount)) {
3064
+ argv.push('--project-account', opts.projectAccount.value);
3065
+ }
3066
+ if (Option.isSome(opts.testingAccount)) {
3067
+ argv.push('--testing-account', opts.testingAccount.value);
3068
+ }
3069
+ if (Option.isSome(opts.profile))
3070
+ argv.push('--profile', opts.profile.value);
3071
+ if (Option.isSome(opts.targetOrigin)) {
3072
+ argv.push('--target-origin', opts.targetOrigin.value);
3073
+ }
3074
+ if (Option.isSome(opts.telemetryOrigin)) {
3075
+ argv.push('--telemetry-origin', opts.telemetryOrigin.value);
3076
+ }
3077
+ if (Option.isSome(opts.accountId))
3078
+ argv.push('--account-id', opts.accountId.value);
3079
+ if (Option.isSome(opts.projectId))
3080
+ argv.push('--project-id', opts.projectId.value);
3081
+ if (Option.isSome(opts.developerId)) {
3082
+ argv.push('--developer-id', opts.developerId.value);
3083
+ }
3084
+ if (Option.isSome(opts.userId))
3085
+ argv.push('--user-id', opts.userId.value);
3086
+ if (Option.isSome(opts.sessionId))
3087
+ argv.push('--session-id', opts.sessionId.value);
3088
+ if (Option.isSome(opts.session))
3089
+ argv.push('--session', opts.session.value);
3090
+ if (Option.isSome(opts.managedSession)) {
3091
+ argv.push('--managed-session', opts.managedSession.value);
3092
+ }
3093
+ if (Option.isSome(opts.ttlSeconds))
3094
+ argv.push('--ttl-seconds', opts.ttlSeconds.value);
3095
+ if (Option.isSome(opts.hsxLogPort))
3096
+ argv.push('--hsx-log-port', opts.hsxLogPort.value);
3097
+ if (Option.isSome(opts.controlPlaneUrl)) {
3098
+ argv.push('--control-plane-url', opts.controlPlaneUrl.value);
3099
+ }
3100
+ if (Option.isSome(opts.runtimeControlPlaneUrl)) {
3101
+ argv.push('--runtime-control-plane-url', opts.runtimeControlPlaneUrl.value);
3102
+ }
3103
+ for (const capability of opts.capability)
3104
+ argv.push('--capability', capability);
3105
+ for (const component of opts.component)
3106
+ argv.push('--component', component);
3107
+ for (const only of opts.only)
3108
+ argv.push('--only', only);
3109
+ for (const lane of opts.lane)
3110
+ argv.push('--lane', lane);
3111
+ for (const level of opts.level)
3112
+ argv.push('--level', level);
3113
+ if (Option.isSome(opts.search))
3114
+ argv.push('--search', opts.search.value);
3115
+ if (Option.isSome(opts.since))
3116
+ argv.push('--since', opts.since.value);
3117
+ if (Option.isSome(opts.cursor))
3118
+ argv.push('--cursor', opts.cursor.value);
3119
+ if (Option.isSome(opts.limit))
3120
+ argv.push('--limit', opts.limit.value);
3121
+ if (Option.isSome(opts.waitSeconds)) {
3122
+ argv.push('--wait-seconds', opts.waitSeconds.value);
3123
+ }
3124
+ if (opts.detach)
3125
+ argv.push('--detach');
3126
+ if (opts.follow)
3127
+ argv.push('--follow');
3128
+ if (opts.pick)
3129
+ argv.push('--pick');
3130
+ if (opts.noHubspot)
3131
+ argv.push('--no-hubspot');
3132
+ if (opts.noUiExtensions)
3133
+ argv.push('--no-ui-extensions');
3134
+ if (opts.yes)
3135
+ argv.push('--yes');
3136
+ if (opts.force)
3137
+ argv.push('--force');
3138
+ yield* runQuarantined(reportCommand, opts.json, () => devCommand({
3139
+ argv,
3140
+ root,
3141
+ json: opts.json,
3142
+ reporter: effectReporter.implementation,
3143
+ }));
3144
+ }));
3145
+ });
1897
3146
  /**
1898
3147
  * `hs-x dev invoke --remote`: dispatch at the DEPLOYED Worker, where install
1899
3148
  * tokens live — handlers using ctx.hubspot run against the real portal. URL
@@ -1910,6 +3159,9 @@ async function devInvokeRemote(args) {
1910
3159
  }
1911
3160
  let url = explicitUrl;
1912
3161
  if (!url) {
3162
+ if (!mapping) {
3163
+ return fail('HSX_E_DEV_UNKNOWN_CAPABILITY', `No capability \`${capabilityId}\` in .hs-x/manifest.json.`);
3164
+ }
1913
3165
  const { resolveCloudflareCredentials } = await import('../cloudflare-kv.js');
1914
3166
  const credentials = resolveCloudflareCredentials(argv);
1915
3167
  const { loadCloudflarePointer } = await import('../cloudflare-pointer.js');