@hs-x/cli 0.3.6 → 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 (211) hide show
  1. package/README.md +126 -0
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +58 -59
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/cli/kit.d.ts.map +1 -1
  6. package/dist/cli/kit.js +1 -1
  7. package/dist/cli/kit.js.map +1 -1
  8. package/dist/cloudflare-auth.d.ts +19 -0
  9. package/dist/cloudflare-auth.d.ts.map +1 -1
  10. package/dist/cloudflare-auth.js +83 -2
  11. package/dist/cloudflare-auth.js.map +1 -1
  12. package/dist/cloudflare-oauth.d.ts.map +1 -1
  13. package/dist/cloudflare-oauth.js +18 -19
  14. package/dist/cloudflare-oauth.js.map +1 -1
  15. package/dist/cloudflare-scoped-resource.d.ts +41 -0
  16. package/dist/cloudflare-scoped-resource.d.ts.map +1 -0
  17. package/dist/cloudflare-scoped-resource.js +74 -0
  18. package/dist/cloudflare-scoped-resource.js.map +1 -0
  19. package/dist/command-catalog.d.ts +13 -0
  20. package/dist/command-catalog.d.ts.map +1 -0
  21. package/dist/command-catalog.js +50 -0
  22. package/dist/command-catalog.js.map +1 -0
  23. package/dist/commands/completion.d.ts.map +1 -1
  24. package/dist/commands/completion.js +287 -47
  25. package/dist/commands/completion.js.map +1 -1
  26. package/dist/commands/connect.d.ts.map +1 -1
  27. package/dist/commands/connect.js +274 -38
  28. package/dist/commands/connect.js.map +1 -1
  29. package/dist/commands/control-plane-read.d.ts +31 -3
  30. package/dist/commands/control-plane-read.d.ts.map +1 -1
  31. package/dist/commands/control-plane-read.js +136 -37
  32. package/dist/commands/control-plane-read.js.map +1 -1
  33. package/dist/commands/deploy-retry.d.ts +20 -0
  34. package/dist/commands/deploy-retry.d.ts.map +1 -0
  35. package/dist/commands/deploy-retry.js +106 -0
  36. package/dist/commands/deploy-retry.js.map +1 -0
  37. package/dist/commands/deploy.d.ts +16 -8
  38. package/dist/commands/deploy.d.ts.map +1 -1
  39. package/dist/commands/deploy.js +1027 -373
  40. package/dist/commands/deploy.js.map +1 -1
  41. package/dist/commands/dev.d.ts +19 -1
  42. package/dist/commands/dev.d.ts.map +1 -1
  43. package/dist/commands/dev.js +1378 -88
  44. package/dist/commands/dev.js.map +1 -1
  45. package/dist/commands/doctor.d.ts.map +1 -1
  46. package/dist/commands/doctor.js +27 -10
  47. package/dist/commands/doctor.js.map +1 -1
  48. package/dist/commands/flags.d.ts +9 -4
  49. package/dist/commands/flags.d.ts.map +1 -1
  50. package/dist/commands/flags.js +208 -80
  51. package/dist/commands/flags.js.map +1 -1
  52. package/dist/commands/help-command.d.ts.map +1 -1
  53. package/dist/commands/help-command.js +177 -86
  54. package/dist/commands/help-command.js.map +1 -1
  55. package/dist/commands/init.d.ts.map +1 -1
  56. package/dist/commands/init.js +5 -0
  57. package/dist/commands/init.js.map +1 -1
  58. package/dist/commands/link.d.ts.map +1 -1
  59. package/dist/commands/link.js +74 -15
  60. package/dist/commands/link.js.map +1 -1
  61. package/dist/commands/project.d.ts +107 -0
  62. package/dist/commands/project.d.ts.map +1 -0
  63. package/dist/commands/project.js +586 -0
  64. package/dist/commands/project.js.map +1 -0
  65. package/dist/commands/secrets.d.ts +2 -2
  66. package/dist/commands/status.d.ts.map +1 -1
  67. package/dist/commands/status.js +15 -5
  68. package/dist/commands/status.js.map +1 -1
  69. package/dist/commands/validate.d.ts +19 -0
  70. package/dist/commands/validate.d.ts.map +1 -1
  71. package/dist/commands/validate.js +26 -7
  72. package/dist/commands/validate.js.map +1 -1
  73. package/dist/constants.d.ts +1 -1
  74. package/dist/constants.js +1 -1
  75. package/dist/control-plane-tenant-provisioning.d.ts +24 -0
  76. package/dist/control-plane-tenant-provisioning.d.ts.map +1 -0
  77. package/dist/control-plane-tenant-provisioning.js +85 -0
  78. package/dist/control-plane-tenant-provisioning.js.map +1 -0
  79. package/dist/deploy-targets.d.ts +24 -0
  80. package/dist/deploy-targets.d.ts.map +1 -0
  81. package/dist/deploy-targets.js +38 -0
  82. package/dist/deploy-targets.js.map +1 -0
  83. package/dist/dev/account-selection.d.ts +16 -0
  84. package/dist/dev/account-selection.d.ts.map +1 -0
  85. package/dist/dev/account-selection.js +22 -0
  86. package/dist/dev/account-selection.js.map +1 -0
  87. package/dist/dev/browser-log-bridge.d.ts +32 -0
  88. package/dist/dev/browser-log-bridge.d.ts.map +1 -0
  89. package/dist/dev/browser-log-bridge.js +156 -0
  90. package/dist/dev/browser-log-bridge.js.map +1 -0
  91. package/dist/dev/compat-shim.d.ts +3 -3
  92. package/dist/dev/compat-shim.d.ts.map +1 -1
  93. package/dist/dev/compat-shim.js +29 -4
  94. package/dist/dev/compat-shim.js.map +1 -1
  95. package/dist/dev/component-selection.d.ts +25 -0
  96. package/dist/dev/component-selection.d.ts.map +1 -0
  97. package/dist/dev/component-selection.js +79 -0
  98. package/dist/dev/component-selection.js.map +1 -0
  99. package/dist/dev/console-capture.d.ts +18 -0
  100. package/dist/dev/console-capture.d.ts.map +1 -0
  101. package/dist/dev/console-capture.js +57 -0
  102. package/dist/dev/console-capture.js.map +1 -0
  103. package/dist/dev/event-bus.d.ts +1 -1
  104. package/dist/dev/event-bus.js +1 -1
  105. package/dist/dev/frontend-log-relay.d.ts +27 -0
  106. package/dist/dev/frontend-log-relay.d.ts.map +1 -0
  107. package/dist/dev/frontend-log-relay.js +102 -0
  108. package/dist/dev/frontend-log-relay.js.map +1 -0
  109. package/dist/dev/invoke.d.ts +3 -4
  110. package/dist/dev/invoke.d.ts.map +1 -1
  111. package/dist/dev/invoke.js +9 -4
  112. package/dist/dev/invoke.js.map +1 -1
  113. package/dist/dev/keyboard.d.ts +1 -0
  114. package/dist/dev/keyboard.d.ts.map +1 -1
  115. package/dist/dev/keyboard.js +9 -3
  116. package/dist/dev/keyboard.js.map +1 -1
  117. package/dist/dev/log-server.js +3 -0
  118. package/dist/dev/log-server.js.map +1 -1
  119. package/dist/dev/loopback-http-listener.d.ts +18 -0
  120. package/dist/dev/loopback-http-listener.d.ts.map +1 -0
  121. package/dist/dev/loopback-http-listener.js +91 -0
  122. package/dist/dev/loopback-http-listener.js.map +1 -0
  123. package/dist/dev/managed-session.d.ts +124 -0
  124. package/dist/dev/managed-session.d.ts.map +1 -0
  125. package/dist/dev/managed-session.js +597 -0
  126. package/dist/dev/managed-session.js.map +1 -0
  127. package/dist/dev/port-process.d.ts +21 -0
  128. package/dist/dev/port-process.d.ts.map +1 -0
  129. package/dist/dev/port-process.js +145 -0
  130. package/dist/dev/port-process.js.map +1 -0
  131. package/dist/dev/session-manager.d.ts +11 -1
  132. package/dist/dev/session-manager.d.ts.map +1 -1
  133. package/dist/dev/session-manager.js +65 -14
  134. package/dist/dev/session-manager.js.map +1 -1
  135. package/dist/dev/stream-event-format.d.ts +14 -0
  136. package/dist/dev/stream-event-format.d.ts.map +1 -0
  137. package/dist/dev/stream-event-format.js +262 -0
  138. package/dist/dev/stream-event-format.js.map +1 -0
  139. package/dist/dev/stream-filter.d.ts.map +1 -1
  140. package/dist/dev/stream-filter.js +16 -1
  141. package/dist/dev/stream-filter.js.map +1 -1
  142. package/dist/dev/stream-renderer.d.ts +4 -2
  143. package/dist/dev/stream-renderer.d.ts.map +1 -1
  144. package/dist/dev/stream-renderer.js +224 -84
  145. package/dist/dev/stream-renderer.js.map +1 -1
  146. package/dist/errors-registry.d.ts.map +1 -1
  147. package/dist/errors-registry.js +246 -0
  148. package/dist/errors-registry.js.map +1 -1
  149. package/dist/help.d.ts.map +1 -1
  150. package/dist/help.js +20 -3
  151. package/dist/help.js.map +1 -1
  152. package/dist/hubspot-app-logs.d.ts +38 -5
  153. package/dist/hubspot-app-logs.d.ts.map +1 -1
  154. package/dist/hubspot-app-logs.js +88 -17
  155. package/dist/hubspot-app-logs.js.map +1 -1
  156. package/dist/hubspot-auth.d.ts +19 -1
  157. package/dist/hubspot-auth.d.ts.map +1 -1
  158. package/dist/hubspot-auth.js +228 -83
  159. package/dist/hubspot-auth.js.map +1 -1
  160. package/dist/hubspot-developer-client.d.ts +87 -0
  161. package/dist/hubspot-developer-client.d.ts.map +1 -1
  162. package/dist/hubspot-developer-client.js +232 -1
  163. package/dist/hubspot-developer-client.js.map +1 -1
  164. package/dist/hubspot-project-archive.d.ts +21 -0
  165. package/dist/hubspot-project-archive.d.ts.map +1 -0
  166. package/dist/hubspot-project-archive.js +281 -0
  167. package/dist/hubspot-project-archive.js.map +1 -0
  168. package/dist/hubspot-project.d.ts +32 -0
  169. package/dist/hubspot-project.d.ts.map +1 -0
  170. package/dist/hubspot-project.js +269 -0
  171. package/dist/hubspot-project.js.map +1 -0
  172. package/dist/local-project-picker.js +1 -1
  173. package/dist/local-project-picker.js.map +1 -1
  174. package/dist/machine-artifact.d.ts +16 -0
  175. package/dist/machine-artifact.d.ts.map +1 -0
  176. package/dist/machine-artifact.js +18 -0
  177. package/dist/machine-artifact.js.map +1 -0
  178. package/dist/machine-id.d.ts +15 -0
  179. package/dist/machine-id.d.ts.map +1 -1
  180. package/dist/machine-id.js +53 -0
  181. package/dist/machine-id.js.map +1 -1
  182. package/dist/mutation-consent.d.ts +20 -0
  183. package/dist/mutation-consent.d.ts.map +1 -0
  184. package/dist/mutation-consent.js +9 -0
  185. package/dist/mutation-consent.js.map +1 -0
  186. package/dist/output-context.d.ts +3 -0
  187. package/dist/output-context.d.ts.map +1 -1
  188. package/dist/output-context.js +7 -1
  189. package/dist/output-context.js.map +1 -1
  190. package/dist/prompt.d.ts.map +1 -1
  191. package/dist/prompt.js +123 -48
  192. package/dist/prompt.js.map +1 -1
  193. package/dist/reporter/index.js +1 -1
  194. package/dist/reporter/index.js.map +1 -1
  195. package/dist/reporter/style.d.ts.map +1 -1
  196. package/dist/reporter/style.js +5 -0
  197. package/dist/reporter/style.js.map +1 -1
  198. package/dist/services/hubspot-dev-client.js +3 -0
  199. package/dist/services/hubspot-dev-client.js.map +1 -1
  200. package/dist/services/telemetry.d.ts +1 -2
  201. package/dist/services/telemetry.d.ts.map +1 -1
  202. package/dist/services/telemetry.js +1 -10
  203. package/dist/services/telemetry.js.map +1 -1
  204. package/dist/telemetry.d.ts.map +1 -1
  205. package/dist/telemetry.js +1 -7
  206. package/dist/telemetry.js.map +1 -1
  207. package/dist/tenant-data-plane-provisioning.d.ts +35 -0
  208. package/dist/tenant-data-plane-provisioning.d.ts.map +1 -0
  209. package/dist/tenant-data-plane-provisioning.js +38 -0
  210. package/dist/tenant-data-plane-provisioning.js.map +1 -0
  211. 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,11 +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
- import { isInteractive, promptMultiSelect, promptSelect } from '../prompt.js';
26
+ import { requestMutationConsent } from '../mutation-consent.js';
27
+ import { isMachineOutput } from '../output-context.js';
28
+ import { isInteractive, promptConfirm, promptMultiSelect, promptSelect } from '../prompt.js';
19
29
  import { createReporter } from '../reporter/index.js';
20
30
  import { Cwd } from '../services/cwd.js';
31
+ import { Reporter as ReporterService } from '../services/reporter.js';
21
32
  async function hostedHttp(input) {
22
33
  const headers = { ...(input.headers ?? {}) };
23
34
  let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
@@ -33,6 +44,21 @@ function isRecord(value) {
33
44
  function isErrnoException(value) {
34
45
  return value instanceof Error && typeof value.code === 'string';
35
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;
36
62
  function makeDevLogPaint(env, stderr) {
37
63
  const isTTY = Boolean(stderr.isTTY);
38
64
  const color = isTTY && env.NO_COLOR !== '1';
@@ -177,7 +203,512 @@ async function resolveDefaultPortalId(input) {
177
203
  }
178
204
  return undefined;
179
205
  }
180
- 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, }) {
181
712
  if (argv[1] === 'cleanup') {
182
713
  return await devCleanupCommand({ argv, json });
183
714
  }
@@ -193,7 +724,7 @@ export async function devCommand({ argv, root, json, }) {
193
724
  prompts: { select: promptSelect },
194
725
  });
195
726
  if (selected && selected.root !== root) {
196
- return await devCommand({ argv, root: selected.root, json });
727
+ return await devCommandCore({ argv, root: selected.root, json });
197
728
  }
198
729
  }
199
730
  catch (error) {
@@ -205,6 +736,9 @@ export async function devCommand({ argv, root, json, }) {
205
736
  if (argv[1] === 'invoke') {
206
737
  return await devInvokeCommand({ argv, root, json });
207
738
  }
739
+ if ((await detectLocalProjectMode(root)) === 'hubspot-direct') {
740
+ return await hubSpotDirectDevCommand({ argv, root, json });
741
+ }
208
742
  const portFlag = resolveFlag(argv, '--port');
209
743
  const port = Number(portFlag ?? '8787');
210
744
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -663,11 +1197,13 @@ export async function devCommand({ argv, root, json, }) {
663
1197
  reporter.done(undefined, 2);
664
1198
  return { exitCode: 2 };
665
1199
  }
1200
+ const managedLogSessionId = resolveFlag(argv, '--managed-session');
666
1201
  const devLogStream = await startHsxDevLogStream({
667
1202
  port: hsxLogPort,
668
1203
  root,
669
1204
  reporter,
670
1205
  filter: onlyParsed.filter,
1206
+ ...(managedLogSessionId ? { managedSessionId: managedLogSessionId } : {}),
671
1207
  });
672
1208
  if (devLogStream) {
673
1209
  const paint = makeDevLogPaint(process.env, process.stderr);
@@ -727,6 +1263,14 @@ export async function devCommand({ argv, root, json, }) {
727
1263
  }),
728
1264
  });
729
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
+ }
730
1274
  // Railed startup checklist: identity header, then one row per surface.
731
1275
  reporter.header(subject);
732
1276
  reporter.rows([
@@ -794,8 +1338,22 @@ export async function devCommand({ argv, root, json, }) {
794
1338
  : undefined;
795
1339
  devLogStream?.renderer.ready();
796
1340
  await new Promise((resolveClose) => {
1341
+ let closing = false;
797
1342
  const close = async () => {
1343
+ if (closing)
1344
+ return;
1345
+ closing = true;
798
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
+ }
799
1357
  // Signal-driven shutdown can terminate the Bun process before awaited
800
1358
  // cleanup settles. Restore temporarily stamped migration sources
801
1359
  // synchronously first; the remaining network/process cleanup is
@@ -848,13 +1406,615 @@ export async function devCommand({ argv, root, json, }) {
848
1406
  // best-effort: sidecar shutdown failures should not block exit.
849
1407
  }
850
1408
  }
851
- 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
+ });
852
1422
  };
853
1423
  process.once('SIGINT', close);
854
1424
  process.once('SIGTERM', close);
855
1425
  });
856
1426
  return { exitCode: 0 };
857
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
+ }
858
2018
  async function startHsxDevLogStream(input) {
859
2019
  try {
860
2020
  const [{ createDevEventBus }, { startDevLogServer }, { startDevStreamRenderer }] = await Promise.all([
@@ -870,6 +2030,12 @@ async function startHsxDevLogStream(input) {
870
2030
  deferStatus: true,
871
2031
  ...(input.filter ? { filter: input.filter } : {}),
872
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;
873
2039
  // HubSpot-side lane: renders/proxy failures that never reach our Worker
874
2040
  // only exist in HubSpot's app logs — poll them into the same stream.
875
2041
  // Missing binding/credentials just means the lane stays off; `hs-x dev`
@@ -901,8 +2067,9 @@ async function startHsxDevLogStream(input) {
901
2067
  bus,
902
2068
  async cleanup() {
903
2069
  hubspotPoller?.stop();
2070
+ unsubscribeManagedEvents?.();
904
2071
  renderer.stop();
905
- await server.close();
2072
+ await Promise.all([server.close(), managedEventWriter?.close()]);
906
2073
  },
907
2074
  };
908
2075
  }
@@ -952,8 +2119,9 @@ async function probeHubSpotUiNodes(input) {
952
2119
  projectSourceDir: pathJoin(input.root, projectConfig.srcDir),
953
2120
  platformVersion: projectConfig.platformVersion,
954
2121
  accountId: portalId,
955
- }, {});
956
- 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 };
957
2125
  }
958
2126
  catch {
959
2127
  // IR translate failed — caller falls back to capability-only behavior.
@@ -971,10 +2139,12 @@ async function maybeStartHubSpotDevSession(input) {
971
2139
  }
972
2140
  try {
973
2141
  const { startDevSession } = await import('../dev/session-manager.js');
2142
+ const profile = resolveFlag(input.argv, '--profile');
974
2143
  const session = await startDevSession({
975
2144
  accountId: portalId,
976
2145
  projectDir: input.root,
977
2146
  selectedNodeUids: input.selectedNodeUids,
2147
+ ...(profile ? { profile } : {}),
978
2148
  });
979
2149
  input.reporter
980
2150
  .step('HubSpot UI-extension session')
@@ -982,7 +2152,7 @@ async function maybeStartHubSpotDevSession(input) {
982
2152
  return session;
983
2153
  }
984
2154
  catch (error) {
985
- 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)}`);
986
2156
  return undefined;
987
2157
  }
988
2158
  }
@@ -1301,6 +2471,43 @@ async function devCleanupCommand({ argv, json, }) {
1301
2471
  if (!accountId || !controlPlaneUrl || !sessionId) {
1302
2472
  throw new Error('Dev cleanup requires --control-plane-url, --account-id, and --session-id.');
1303
2473
  }
2474
+ const reporter = createReporter({ command: 'dev cleanup', argv });
2475
+ const machineOutput = json || isMachineOutput();
2476
+ const interactive = isInteractive();
2477
+ const yes = argv.some((arg) => arg === '--yes' || arg === '-y');
2478
+ if (!machineOutput && interactive) {
2479
+ reporter.block([
2480
+ 'Dev cleanup plan',
2481
+ `Account: ${accountId}`,
2482
+ `Session: ${sessionId}`,
2483
+ `Control plane: ${controlPlaneUrl}`,
2484
+ 'Change: clear every dev override registered by this session.',
2485
+ ].join('\n'));
2486
+ }
2487
+ const consent = await requestMutationConsent({
2488
+ yes,
2489
+ machineOutput,
2490
+ interactive,
2491
+ message: `Clear dev overrides for session ${sessionId}?`,
2492
+ confirm: promptConfirm,
2493
+ });
2494
+ if (consent === 'require-yes') {
2495
+ const message = 'Dev cleanup requires explicit consent in machine or non-interactive mode. Re-run with --yes.';
2496
+ if (json) {
2497
+ reporter.raw(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
2498
+ }
2499
+ else {
2500
+ reporter.error('HSX_E_INPUT_INVALID', message, {
2501
+ hint: '`--yes` acknowledges this cleanup plan; it does not select or broaden the target session.',
2502
+ });
2503
+ // The Effect boundary emits the single terminal event for NDJSON.
2504
+ if (reporter.mode !== 'ndjson')
2505
+ reporter.done(undefined, 10);
2506
+ }
2507
+ return { exitCode: 10 };
2508
+ }
2509
+ if (consent === 'cancelled')
2510
+ return cancelledResult(argv, 'dev cleanup');
1304
2511
  const cleared = await requestDevOverrideCleanup({
1305
2512
  controlPlaneUrl,
1306
2513
  accountId,
@@ -1319,7 +2526,6 @@ async function devCleanupCommand({ argv, json, }) {
1319
2526
  write(`${JSON.stringify(result, null, 2)}\n`);
1320
2527
  }
1321
2528
  else {
1322
- const reporter = createReporter({ command: 'dev cleanup', argv });
1323
2529
  reporter.header(sessionId);
1324
2530
  reporter.step(`Cleared ${cleared.cleared.length} dev overrides`).ok();
1325
2531
  reporter.done();
@@ -1719,11 +2925,11 @@ function write(message) {
1719
2925
  // as a silent `exitWith(code)` (the legacy body renders its own output) so
1720
2926
  // `runHandler` does not re-render.
1721
2927
  //
1722
- // The first positional argument selects the `cleanup` / `status` subcommands
2928
+ // The first positional argument selects the managed-session and legacy subcommands
1723
2929
  // the legacy body branches on via `argv[1]`.
1724
2930
  // A positional (not an option) so `hs-x dev cleanup` / `dev status` parse — the
1725
2931
  // legacy body branches on `argv[1]`.
1726
- 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);
1727
2933
  // Second positional: the capability id for `dev invoke <capability-id>`.
1728
2934
  const devTargetArg = Args.text({ name: 'target' }).pipe(Args.withDescription('Capability id for `dev invoke`'), Args.optional);
1729
2935
  const devInputOption = Options.text('input').pipe(Options.optional);
@@ -1734,6 +2940,10 @@ const devRemoteOption = Options.boolean('remote').pipe(Options.withDefault(false
1734
2940
  const devUrlOption = Options.text('url').pipe(Options.optional);
1735
2941
  const portOption = Options.text('port').pipe(Options.optional);
1736
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);
1737
2947
  const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
1738
2948
  const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
1739
2949
  const accountIdOption = Options.text('account-id').pipe(Options.optional);
@@ -1741,12 +2951,24 @@ const projectIdOption = Options.text('project-id').pipe(Options.optional);
1741
2951
  const developerIdOption = Options.text('developer-id').pipe(Options.optional);
1742
2952
  const userIdOption = Options.text('user-id').pipe(Options.optional);
1743
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);
1744
2956
  const ttlSecondsOption = Options.text('ttl-seconds').pipe(Options.optional);
1745
2957
  const hsxLogPortOption = Options.text('hsx-log-port').pipe(Options.optional);
1746
2958
  const controlPlaneUrlOption = Options.text('control-plane-url').pipe(Options.optional);
1747
2959
  const runtimeControlPlaneUrlOption = Options.text('runtime-control-plane-url').pipe(Options.optional);
1748
2960
  const capabilityOption = Options.text('capability').pipe(Options.repeated);
2961
+ const componentOption = Options.text('component').pipe(Options.repeated);
1749
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));
1750
2972
  const pickOption = Options.boolean('pick').pipe(Options.withDefault(false));
1751
2973
  const noHubspotOption = Options.boolean('no-hubspot').pipe(Options.withDefault(false));
1752
2974
  const noUiExtensionsOption = Options.boolean('no-ui-extensions').pipe(Options.withDefault(false));
@@ -1765,6 +2987,10 @@ export const devCmd = Command.make('dev', {
1765
2987
  url: devUrlOption,
1766
2988
  port: portOption,
1767
2989
  portal: portalOption,
2990
+ account: accountOption,
2991
+ projectAccount: projectAccountOption,
2992
+ testingAccount: testingAccountOption,
2993
+ profile: profileOption,
1768
2994
  targetOrigin: targetOriginOption,
1769
2995
  telemetryOrigin: telemetryOriginOption,
1770
2996
  accountId: accountIdOption,
@@ -1772,90 +2998,151 @@ export const devCmd = Command.make('dev', {
1772
2998
  developerId: developerIdOption,
1773
2999
  userId: userIdOption,
1774
3000
  sessionId: sessionIdOption,
3001
+ session: managedSessionOption,
3002
+ managedSession: internalManagedSessionOption,
1775
3003
  ttlSeconds: ttlSecondsOption,
1776
3004
  hsxLogPort: hsxLogPortOption,
1777
3005
  controlPlaneUrl: controlPlaneUrlOption,
1778
3006
  runtimeControlPlaneUrl: runtimeControlPlaneUrlOption,
1779
3007
  capability: capabilityOption,
3008
+ component: componentOption,
1780
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,
1781
3019
  pick: pickOption,
1782
3020
  noHubspot: noHubspotOption,
1783
3021
  noUiExtensions: noUiExtensionsOption,
1784
3022
  yes: yesOption,
1785
3023
  force: forceOption,
1786
- }, (opts) => runHandler('dev', { json: opts.json }, Effect.gen(function* () {
1787
- // root: injected cwd, overridden by --cwd.
1788
- const injectedCwd = yield* Cwd;
1789
- const root = Option.getOrElse(opts.cwd, () => injectedCwd);
1790
- // Reconstruct the legacy argv the quarantined body parses (it reads
1791
- // its flags off argv via `resolveFlag`/`resolveFlags` and branches on
1792
- // `argv[1]` for the cleanup/status subcommands).
1793
- const argv = ['dev'];
1794
- if (Option.isSome(opts.subcommand))
1795
- argv.push(opts.subcommand.value);
1796
- if (Option.isSome(opts.target))
1797
- argv.push(opts.target.value);
1798
- if (Option.isSome(opts.input))
1799
- argv.push('--input', opts.input.value);
1800
- if (Option.isSome(opts.object))
1801
- argv.push('--object', opts.object.value);
1802
- if (Option.isSome(opts.install))
1803
- argv.push('--install', opts.install.value);
1804
- if (opts.last)
1805
- argv.push('--last');
1806
- if (opts.remote)
1807
- argv.push('--remote');
1808
- if (Option.isSome(opts.url))
1809
- argv.push('--url', opts.url.value);
1810
- if (opts.json)
1811
- argv.push('--json');
1812
- if (Option.isSome(opts.port))
1813
- argv.push('--port', opts.port.value);
1814
- if (Option.isSome(opts.portal))
1815
- argv.push('--portal', opts.portal.value);
1816
- if (Option.isSome(opts.targetOrigin)) {
1817
- argv.push('--target-origin', opts.targetOrigin.value);
1818
- }
1819
- if (Option.isSome(opts.telemetryOrigin)) {
1820
- argv.push('--telemetry-origin', opts.telemetryOrigin.value);
1821
- }
1822
- if (Option.isSome(opts.accountId))
1823
- argv.push('--account-id', opts.accountId.value);
1824
- if (Option.isSome(opts.projectId))
1825
- argv.push('--project-id', opts.projectId.value);
1826
- if (Option.isSome(opts.developerId)) {
1827
- argv.push('--developer-id', opts.developerId.value);
1828
- }
1829
- if (Option.isSome(opts.userId))
1830
- argv.push('--user-id', opts.userId.value);
1831
- if (Option.isSome(opts.sessionId))
1832
- argv.push('--session-id', opts.sessionId.value);
1833
- if (Option.isSome(opts.ttlSeconds))
1834
- argv.push('--ttl-seconds', opts.ttlSeconds.value);
1835
- if (Option.isSome(opts.hsxLogPort))
1836
- argv.push('--hsx-log-port', opts.hsxLogPort.value);
1837
- if (Option.isSome(opts.controlPlaneUrl)) {
1838
- argv.push('--control-plane-url', opts.controlPlaneUrl.value);
1839
- }
1840
- if (Option.isSome(opts.runtimeControlPlaneUrl)) {
1841
- argv.push('--runtime-control-plane-url', opts.runtimeControlPlaneUrl.value);
1842
- }
1843
- for (const capability of opts.capability)
1844
- argv.push('--capability', capability);
1845
- for (const only of opts.only)
1846
- argv.push('--only', only);
1847
- if (opts.pick)
1848
- argv.push('--pick');
1849
- if (opts.noHubspot)
1850
- argv.push('--no-hubspot');
1851
- if (opts.noUiExtensions)
1852
- argv.push('--no-ui-extensions');
1853
- if (opts.yes)
1854
- argv.push('--yes');
1855
- if (opts.force)
1856
- argv.push('--force');
1857
- yield* runQuarantined('dev', opts.json, () => devCommand({ argv, root, json: opts.json }));
1858
- })));
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
+ });
1859
3146
  /**
1860
3147
  * `hs-x dev invoke --remote`: dispatch at the DEPLOYED Worker, where install
1861
3148
  * tokens live — handlers using ctx.hubspot run against the real portal. URL
@@ -1872,6 +3159,9 @@ async function devInvokeRemote(args) {
1872
3159
  }
1873
3160
  let url = explicitUrl;
1874
3161
  if (!url) {
3162
+ if (!mapping) {
3163
+ return fail('HSX_E_DEV_UNKNOWN_CAPABILITY', `No capability \`${capabilityId}\` in .hs-x/manifest.json.`);
3164
+ }
1875
3165
  const { resolveCloudflareCredentials } = await import('../cloudflare-kv.js');
1876
3166
  const credentials = resolveCloudflareCredentials(argv);
1877
3167
  const { loadCloudflarePointer } = await import('../cloudflare-pointer.js');
@@ -1900,9 +3190,9 @@ async function devInvokeRemote(args) {
1900
3190
  : {}),
1901
3191
  ...(payloadOverrides.install !== undefined ? { install: payloadOverrides.install } : {}),
1902
3192
  };
1903
- // Secured deploys require a v3 signature on the invoke surface; sign with
1904
- // the app secret from .dev.vars when present (pre-OAuth deploys accept
1905
- // unsigned, so absence is fine).
3193
+ // Deployed invokes require a configured secret and valid v3 signature. If
3194
+ // .dev.vars has no secret, the remote bootstrap worker fails closed with 503;
3195
+ // only the separate in-process local adapter opts into unsigned execution.
1906
3196
  const devVars = await loadDevVars(root);
1907
3197
  const signatureSecret = typeof devVars.HSX_HUBSPOT_CLIENT_SECRET === 'string' &&
1908
3198
  devVars.HSX_HUBSPOT_CLIENT_SECRET.length > 0