@hs-x/cli 0.2.7 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +25 -8
  3. package/dist/audit-interactive.d.ts +29 -0
  4. package/dist/audit-interactive.d.ts.map +1 -0
  5. package/dist/audit-interactive.js +156 -0
  6. package/dist/audit-interactive.js.map +1 -0
  7. package/dist/bin/hs-x.js.map +1 -1
  8. package/dist/checkpoint-interactive.d.ts +24 -0
  9. package/dist/checkpoint-interactive.d.ts.map +1 -0
  10. package/dist/checkpoint-interactive.js +65 -0
  11. package/dist/checkpoint-interactive.js.map +1 -0
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +266 -39
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/kit.d.ts +1 -1
  16. package/dist/cli/kit.d.ts.map +1 -1
  17. package/dist/cli/kit.js +19 -3
  18. package/dist/cli/kit.js.map +1 -1
  19. package/dist/cloudflare-auth.d.ts +8 -0
  20. package/dist/cloudflare-auth.d.ts.map +1 -1
  21. package/dist/cloudflare-auth.js +12 -2
  22. package/dist/cloudflare-auth.js.map +1 -1
  23. package/dist/cloudflare-oauth-store.d.ts +9 -1
  24. package/dist/cloudflare-oauth-store.d.ts.map +1 -1
  25. package/dist/cloudflare-oauth-store.js +23 -4
  26. package/dist/cloudflare-oauth-store.js.map +1 -1
  27. package/dist/cloudflare-oauth.d.ts +1 -0
  28. package/dist/cloudflare-oauth.d.ts.map +1 -1
  29. package/dist/cloudflare-oauth.js +6 -2
  30. package/dist/cloudflare-oauth.js.map +1 -1
  31. package/dist/commands/account.d.ts +28 -13
  32. package/dist/commands/account.d.ts.map +1 -1
  33. package/dist/commands/account.js +219 -69
  34. package/dist/commands/account.js.map +1 -1
  35. package/dist/commands/api.d.ts.map +1 -1
  36. package/dist/commands/api.js +7 -11
  37. package/dist/commands/api.js.map +1 -1
  38. package/dist/commands/completion.d.ts.map +1 -1
  39. package/dist/commands/completion.js +13 -7
  40. package/dist/commands/completion.js.map +1 -1
  41. package/dist/commands/connect.d.ts.map +1 -1
  42. package/dist/commands/connect.js +239 -125
  43. package/dist/commands/connect.js.map +1 -1
  44. package/dist/commands/control-plane-read.d.ts +71 -10
  45. package/dist/commands/control-plane-read.d.ts.map +1 -1
  46. package/dist/commands/control-plane-read.js +1035 -58
  47. package/dist/commands/control-plane-read.js.map +1 -1
  48. package/dist/commands/deploy-promote.d.ts +25 -3
  49. package/dist/commands/deploy-promote.d.ts.map +1 -1
  50. package/dist/commands/deploy-promote.js +314 -38
  51. package/dist/commands/deploy-promote.js.map +1 -1
  52. package/dist/commands/deploy.d.ts +69 -4
  53. package/dist/commands/deploy.d.ts.map +1 -1
  54. package/dist/commands/deploy.js +1512 -276
  55. package/dist/commands/deploy.js.map +1 -1
  56. package/dist/commands/dev.d.ts +5 -1
  57. package/dist/commands/dev.d.ts.map +1 -1
  58. package/dist/commands/dev.js +502 -107
  59. package/dist/commands/dev.js.map +1 -1
  60. package/dist/commands/doctor.d.ts +14 -0
  61. package/dist/commands/doctor.d.ts.map +1 -1
  62. package/dist/commands/doctor.js +131 -20
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/flags.d.ts +108 -6
  65. package/dist/commands/flags.d.ts.map +1 -1
  66. package/dist/commands/flags.js +996 -75
  67. package/dist/commands/flags.js.map +1 -1
  68. package/dist/commands/help-command.d.ts +14 -0
  69. package/dist/commands/help-command.d.ts.map +1 -1
  70. package/dist/commands/help-command.js +144 -44
  71. package/dist/commands/help-command.js.map +1 -1
  72. package/dist/commands/history.js +14 -10
  73. package/dist/commands/history.js.map +1 -1
  74. package/dist/commands/init.d.ts +4 -2
  75. package/dist/commands/init.d.ts.map +1 -1
  76. package/dist/commands/init.js +98 -17
  77. package/dist/commands/init.js.map +1 -1
  78. package/dist/commands/link.d.ts +2 -3
  79. package/dist/commands/link.d.ts.map +1 -1
  80. package/dist/commands/link.js +201 -101
  81. package/dist/commands/link.js.map +1 -1
  82. package/dist/commands/login.d.ts.map +1 -1
  83. package/dist/commands/login.js +22 -5
  84. package/dist/commands/login.js.map +1 -1
  85. package/dist/commands/migrate.d.ts +37 -17
  86. package/dist/commands/migrate.d.ts.map +1 -1
  87. package/dist/commands/migrate.js +500 -73
  88. package/dist/commands/migrate.js.map +1 -1
  89. package/dist/commands/rollback.d.ts +42 -2
  90. package/dist/commands/rollback.d.ts.map +1 -1
  91. package/dist/commands/rollback.js +422 -58
  92. package/dist/commands/rollback.js.map +1 -1
  93. package/dist/commands/secrets.d.ts +76 -3
  94. package/dist/commands/secrets.d.ts.map +1 -1
  95. package/dist/commands/secrets.js +328 -41
  96. package/dist/commands/secrets.js.map +1 -1
  97. package/dist/commands/status.d.ts +13 -2
  98. package/dist/commands/status.d.ts.map +1 -1
  99. package/dist/commands/status.js +154 -17
  100. package/dist/commands/status.js.map +1 -1
  101. package/dist/commands/unlink.d.ts +2 -2
  102. package/dist/commands/unlink.d.ts.map +1 -1
  103. package/dist/commands/unlink.js +2 -2
  104. package/dist/commands/unlink.js.map +1 -1
  105. package/dist/commands/update.d.ts.map +1 -1
  106. package/dist/commands/update.js +4 -3
  107. package/dist/commands/update.js.map +1 -1
  108. package/dist/commands/validate.d.ts +12 -3
  109. package/dist/commands/validate.d.ts.map +1 -1
  110. package/dist/commands/validate.js +70 -8
  111. package/dist/commands/validate.js.map +1 -1
  112. package/dist/constants.d.ts +1 -1
  113. package/dist/constants.js +1 -1
  114. package/dist/control-plane-fetch.d.ts.map +1 -1
  115. package/dist/control-plane-fetch.js +5 -4
  116. package/dist/control-plane-fetch.js.map +1 -1
  117. package/dist/deploy-revision-capture.d.ts +48 -0
  118. package/dist/deploy-revision-capture.d.ts.map +1 -0
  119. package/dist/deploy-revision-capture.js +125 -0
  120. package/dist/deploy-revision-capture.js.map +1 -0
  121. package/dist/dev/event-bus.d.ts +5 -2
  122. package/dist/dev/event-bus.d.ts.map +1 -1
  123. package/dist/dev/event-bus.js +4 -1
  124. package/dist/dev/event-bus.js.map +1 -1
  125. package/dist/dev/hubspot-logs-poller.d.ts +35 -0
  126. package/dist/dev/hubspot-logs-poller.d.ts.map +1 -0
  127. package/dist/dev/hubspot-logs-poller.js +94 -0
  128. package/dist/dev/hubspot-logs-poller.js.map +1 -0
  129. package/dist/dev/invoke.d.ts +9 -1
  130. package/dist/dev/invoke.d.ts.map +1 -1
  131. package/dist/dev/invoke.js +69 -20
  132. package/dist/dev/invoke.js.map +1 -1
  133. package/dist/dev/keyboard.d.ts +31 -0
  134. package/dist/dev/keyboard.d.ts.map +1 -0
  135. package/dist/dev/keyboard.js +143 -0
  136. package/dist/dev/keyboard.js.map +1 -0
  137. package/dist/dev/log-server.d.ts +7 -0
  138. package/dist/dev/log-server.d.ts.map +1 -1
  139. package/dist/dev/log-server.js +68 -31
  140. package/dist/dev/log-server.js.map +1 -1
  141. package/dist/dev/migrated-card.d.ts +17 -0
  142. package/dist/dev/migrated-card.d.ts.map +1 -0
  143. package/dist/dev/migrated-card.js +114 -0
  144. package/dist/dev/migrated-card.js.map +1 -0
  145. package/dist/dev/stream-filter.d.ts +40 -0
  146. package/dist/dev/stream-filter.d.ts.map +1 -0
  147. package/dist/dev/stream-filter.js +93 -0
  148. package/dist/dev/stream-filter.js.map +1 -0
  149. package/dist/dev/stream-renderer.d.ts +34 -0
  150. package/dist/dev/stream-renderer.d.ts.map +1 -1
  151. package/dist/dev/stream-renderer.js +222 -8
  152. package/dist/dev/stream-renderer.js.map +1 -1
  153. package/dist/errors-registry.d.ts.map +1 -1
  154. package/dist/errors-registry.js +280 -36
  155. package/dist/errors-registry.js.map +1 -1
  156. package/dist/errors.d.ts.map +1 -1
  157. package/dist/errors.js.map +1 -1
  158. package/dist/help.d.ts.map +1 -1
  159. package/dist/help.js +8 -0
  160. package/dist/help.js.map +1 -1
  161. package/dist/hubspot-app-logs.d.ts +76 -0
  162. package/dist/hubspot-app-logs.d.ts.map +1 -0
  163. package/dist/hubspot-app-logs.js +323 -0
  164. package/dist/hubspot-app-logs.js.map +1 -0
  165. package/dist/hubspot-auth.d.ts +8 -2
  166. package/dist/hubspot-auth.d.ts.map +1 -1
  167. package/dist/hubspot-auth.js +9 -9
  168. package/dist/hubspot-auth.js.map +1 -1
  169. package/dist/hubspot-developer-client.js +119 -11
  170. package/dist/hubspot-developer-client.js.map +1 -1
  171. package/dist/index.d.ts +1 -0
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +1 -0
  174. package/dist/index.js.map +1 -1
  175. package/dist/init/templates.d.ts +11 -1
  176. package/dist/init/templates.d.ts.map +1 -1
  177. package/dist/init/templates.js +228 -15
  178. package/dist/init/templates.js.map +1 -1
  179. package/dist/link-migration-journal.d.ts +131 -0
  180. package/dist/link-migration-journal.d.ts.map +1 -0
  181. package/dist/link-migration-journal.js +395 -0
  182. package/dist/link-migration-journal.js.map +1 -0
  183. package/dist/link-preflight.d.ts +78 -0
  184. package/dist/link-preflight.d.ts.map +1 -0
  185. package/dist/link-preflight.js +231 -0
  186. package/dist/link-preflight.js.map +1 -0
  187. package/dist/local-project-picker.d.ts +34 -0
  188. package/dist/local-project-picker.d.ts.map +1 -0
  189. package/dist/local-project-picker.js +121 -0
  190. package/dist/local-project-picker.js.map +1 -0
  191. package/dist/logs-interactive.d.ts +37 -0
  192. package/dist/logs-interactive.d.ts.map +1 -0
  193. package/dist/logs-interactive.js +110 -0
  194. package/dist/logs-interactive.js.map +1 -0
  195. package/dist/output-context.d.ts +13 -0
  196. package/dist/output-context.d.ts.map +1 -0
  197. package/dist/output-context.js +46 -0
  198. package/dist/output-context.js.map +1 -0
  199. package/dist/project-context.d.ts +55 -0
  200. package/dist/project-context.d.ts.map +1 -0
  201. package/dist/project-context.js +115 -0
  202. package/dist/project-context.js.map +1 -0
  203. package/dist/project-picker.d.ts +65 -0
  204. package/dist/project-picker.d.ts.map +1 -0
  205. package/dist/project-picker.js +227 -0
  206. package/dist/project-picker.js.map +1 -0
  207. package/dist/prompt.d.ts +8 -0
  208. package/dist/prompt.d.ts.map +1 -1
  209. package/dist/prompt.js +81 -13
  210. package/dist/prompt.js.map +1 -1
  211. package/dist/reporter/human.d.ts +10 -2
  212. package/dist/reporter/human.d.ts.map +1 -1
  213. package/dist/reporter/human.js +82 -14
  214. package/dist/reporter/human.js.map +1 -1
  215. package/dist/reporter/index.d.ts.map +1 -1
  216. package/dist/reporter/index.js +7 -5
  217. package/dist/reporter/index.js.map +1 -1
  218. package/dist/reporter/json.d.ts +2 -1
  219. package/dist/reporter/json.d.ts.map +1 -1
  220. package/dist/reporter/json.js +10 -7
  221. package/dist/reporter/json.js.map +1 -1
  222. package/dist/reporter/style.d.ts +19 -0
  223. package/dist/reporter/style.d.ts.map +1 -1
  224. package/dist/reporter/style.js +126 -6
  225. package/dist/reporter/style.js.map +1 -1
  226. package/dist/reporter/types.d.ts +34 -4
  227. package/dist/reporter/types.d.ts.map +1 -1
  228. package/dist/reporter/types.js.map +1 -1
  229. package/dist/services/account-store.d.ts +3 -1
  230. package/dist/services/account-store.d.ts.map +1 -1
  231. package/dist/services/account-store.js +2 -1
  232. package/dist/services/account-store.js.map +1 -1
  233. package/dist/services/app-paths.d.ts.map +1 -1
  234. package/dist/services/app-paths.js +3 -1
  235. package/dist/services/app-paths.js.map +1 -1
  236. package/dist/services/cloudflare-auth.d.ts +1 -1
  237. package/dist/services/cloudflare-auth.d.ts.map +1 -1
  238. package/dist/services/cloudflare-auth.js +2 -2
  239. package/dist/services/cloudflare-auth.js.map +1 -1
  240. package/dist/services/cloudflare-kv.d.ts.map +1 -1
  241. package/dist/services/cloudflare-kv.js.map +1 -1
  242. package/dist/services/command-history.d.ts.map +1 -1
  243. package/dist/services/command-history.js.map +1 -1
  244. package/dist/services/config.d.ts.map +1 -1
  245. package/dist/services/config.js +1 -1
  246. package/dist/services/config.js.map +1 -1
  247. package/dist/services/control-plane.d.ts.map +1 -1
  248. package/dist/services/control-plane.js +3 -3
  249. package/dist/services/control-plane.js.map +1 -1
  250. package/dist/services/env-loader.d.ts.map +1 -1
  251. package/dist/services/env-loader.js +1 -3
  252. package/dist/services/env-loader.js.map +1 -1
  253. package/dist/services/http.d.ts.map +1 -1
  254. package/dist/services/http.js.map +1 -1
  255. package/dist/services/hubspot-dev-client.d.ts.map +1 -1
  256. package/dist/services/hubspot-dev-client.js +30 -0
  257. package/dist/services/hubspot-dev-client.js.map +1 -1
  258. package/dist/services/live.d.ts +2 -2
  259. package/dist/services/live.d.ts.map +1 -1
  260. package/dist/services/live.js +1 -1
  261. package/dist/services/live.js.map +1 -1
  262. package/dist/services/machine-id.d.ts.map +1 -1
  263. package/dist/services/machine-id.js.map +1 -1
  264. package/dist/services/reporter.d.ts +8 -5
  265. package/dist/services/reporter.d.ts.map +1 -1
  266. package/dist/services/reporter.js +6 -4
  267. package/dist/services/reporter.js.map +1 -1
  268. package/dist/services/tenant-state.d.ts.map +1 -1
  269. package/dist/services/tenant-state.js.map +1 -1
  270. package/dist/state-store.d.ts +4 -0
  271. package/dist/state-store.d.ts.map +1 -1
  272. package/dist/state-store.js +15 -0
  273. package/dist/state-store.js.map +1 -1
  274. package/dist/status-drilldown.d.ts +24 -0
  275. package/dist/status-drilldown.d.ts.map +1 -0
  276. package/dist/status-drilldown.js +50 -0
  277. package/dist/status-drilldown.js.map +1 -0
  278. package/dist/tenant-state.d.ts.map +1 -1
  279. package/dist/tenant-state.js +3 -2
  280. package/dist/tenant-state.js.map +1 -1
  281. package/dist/token-key-custody.d.ts +91 -0
  282. package/dist/token-key-custody.d.ts.map +1 -0
  283. package/dist/token-key-custody.js +375 -0
  284. package/dist/token-key-custody.js.map +1 -0
  285. package/package.json +14 -15
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import { readFile, readdir, stat } from 'node:fs/promises';
2
3
  import { createServer } from 'node:http';
3
4
  import { join } from 'node:path';
@@ -7,15 +8,16 @@ import * as Options from '@effect/cli/Options';
7
8
  import { generateProjectArtifacts } from '@hs-x/codegen';
8
9
  import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
9
10
  import { Effect, Option } from 'effect';
10
- import { BRAND_ORANGE_SGR, CLI_VERSION } from '../constants.js';
11
11
  import { exitWith } from '../cli-error.js';
12
+ import { cwdOption, jsonOption, runHandler, runQuarantined } from '../cli/kit.js';
13
+ import { formatConfigUrl, loadCliConfig } from '../config.js';
14
+ import { CLI_VERSION } from '../constants.js';
12
15
  import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
13
16
  import { executeCliHttp } from '../effect-http.js';
14
- import { formatConfigUrl, loadCliConfig } from '../config.js';
15
- import { jsonOption, cwdOption, runHandler, runQuarantined } from '../cli/kit.js';
16
- import { Cwd } from '../services/cwd.js';
17
+ import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
18
+ import { isInteractive, promptMultiSelect, promptSelect } from '../prompt.js';
17
19
  import { createReporter } from '../reporter/index.js';
18
- import { isInteractive, promptMultiSelect, promptSelect, promptText } from '../prompt.js';
20
+ import { Cwd } from '../services/cwd.js';
19
21
  async function hostedHttp(input) {
20
22
  const headers = { ...(input.headers ?? {}) };
21
23
  let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
@@ -31,40 +33,46 @@ function isRecord(value) {
31
33
  function isErrnoException(value) {
32
34
  return value instanceof Error && typeof value.code === 'string';
33
35
  }
34
- function renderDevHeader(env, stderr, { subject, url }) {
36
+ function makeDevLogPaint(env, stderr) {
35
37
  const isTTY = Boolean(stderr.isTTY);
36
38
  const color = isTTY && env.NO_COLOR !== '1';
37
- const orange = (s) => (color ? `\x1b[${BRAND_ORANGE_SGR}m${s}\x1b[0m` : s);
38
- const bold = (s) => (color ? `\x1b[1m${s}\x1b[0m` : s);
39
- const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
40
- const arrow = '';
41
- const sep = '';
42
- stderr.write(`\n ${orange(arrow)} ${bold('hs-x dev')} ${bold(subject)} ${dim(sep)} ${url}\n\n`);
39
+ const wrap = (open) => (s) => (color ? `\x1b[${open}m${s}\x1b[0m` : s);
40
+ return {
41
+ dim: wrap('2'),
42
+ green: wrap('32'),
43
+ red: wrap('31'),
44
+ yellow: wrap('33'),
45
+ cyan: wrap('36'),
46
+ magenta: wrap('35'),
47
+ };
48
+ }
49
+ /**
50
+ * One request-summary line (plus an optional `└ detail` second line), colored
51
+ * via `paint`. Shared by the direct fallback logger and the event-bus lane so
52
+ * the columns look identical wherever the line lands.
53
+ */
54
+ function formatDevLogEntry(entry, paint, defaultAccountId) {
55
+ const { method, path, status, durationMs, source = 'local', accountId, detail } = entry;
56
+ const statusColor = status >= 500 ? paint.red : status >= 400 ? paint.yellow : paint.green;
57
+ const symbol = status >= 400 ? '✗' : '✓';
58
+ const methodStr = method.padEnd(5);
59
+ const pathStr = path.length > 35 ? path : path.padEnd(35);
60
+ const statusStr = statusColor(String(status).padStart(3));
61
+ const durationStr = `${durationMs}ms`.padStart(7);
62
+ const effectiveAccountId = accountId ?? defaultAccountId;
63
+ const acct = effectiveAccountId ? ` ${paint.dim(effectiveAccountId)}` : '';
64
+ const sourceColor = source === 'prod' ? paint.magenta : paint.cyan;
65
+ const sourceLabel = sourceColor(source.padEnd(5));
66
+ const line = `${methodStr} ${pathStr} ${statusStr} ${durationStr}${acct} ${statusColor(symbol)} ${sourceLabel}`;
67
+ return detail ? `${line}\n ${paint.dim('└')} ${paint.dim(detail)}` : line;
43
68
  }
44
69
  function makeDevRequestLogger(env, stderr, defaultAccountId) {
45
- const isTTY = Boolean(stderr.isTTY);
46
- const color = isTTY && env.NO_COLOR !== '1';
47
- const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
48
- const green = (s) => (color ? `\x1b[32m${s}\x1b[0m` : s);
49
- const red = (s) => (color ? `\x1b[31m${s}\x1b[0m` : s);
50
- const yellow = (s) => (color ? `\x1b[33m${s}\x1b[0m` : s);
51
- const cyan = (s) => (color ? `\x1b[36m${s}\x1b[0m` : s);
52
- const magenta = (s) => (color ? `\x1b[35m${s}\x1b[0m` : s);
53
- return ({ method, path, status, durationMs, source = 'local', accountId, detail }) => {
54
- const statusColor = status >= 500 ? red : status >= 400 ? yellow : green;
55
- const symbol = status >= 400 ? '✗' : '✓';
56
- const methodStr = method.padEnd(5);
57
- const pathStr = path.length > 35 ? path : path.padEnd(35);
58
- const statusStr = statusColor(String(status).padStart(3));
59
- const durationStr = `${durationMs}ms`.padStart(7);
60
- const effectiveAccountId = accountId ?? defaultAccountId;
61
- const acct = effectiveAccountId ? ` ${dim(effectiveAccountId)}` : '';
62
- const sourceColor = source === 'prod' ? magenta : cyan;
63
- const sourceLabel = sourceColor(source.padEnd(5));
64
- stderr.write(` ${methodStr} ${pathStr} ${statusStr} ${durationStr}${acct} ${statusColor(symbol)} ${sourceLabel}\n`);
65
- if (detail) {
66
- stderr.write(` ${dim('└')} ${dim(detail)}\n`);
67
- }
70
+ const paint = makeDevLogPaint(env, stderr);
71
+ return (entry) => {
72
+ const [line, detailLine] = formatDevLogEntry(entry, paint, defaultAccountId).split('\n');
73
+ stderr.write(` ${line}\n`);
74
+ if (detailLine)
75
+ stderr.write(` ${detailLine}\n`);
68
76
  };
69
77
  }
70
78
  async function resolveAccountIdOrPrompt(input) {
@@ -87,14 +95,18 @@ async function resolveAccountIdOrPrompt(input) {
87
95
  if (ids.length === 0)
88
96
  return { fromPrompt: false };
89
97
  const defaultId = store.defaultAccountId;
90
- // Non-interactive, --json, or --yes: accept the configured default rather than
91
- // silently degrading to "no account picked" that path led deploy to exit 0
92
- // after building artifacts only, looking like success.
93
- const yes = input.argv.includes('--yes') || input.argv.includes('-y');
94
- if (input.json || !isInteractive() || yes) {
95
- if (defaultId && ids.includes(defaultId)) {
96
- return { accountId: defaultId, fromPrompt: false };
97
- }
98
+ // A configured default — or a single account is an answer, not a question:
99
+ // use it silently, exactly like non-interactive runs. `--account-id` and
100
+ // `hs-x accounts switch` override; the picker remains only for the genuinely
101
+ // ambiguous case (multiple accounts, no default).
102
+ if (defaultId && ids.includes(defaultId)) {
103
+ return { accountId: defaultId, fromPrompt: false };
104
+ }
105
+ const soleAccount = ids.length === 1 ? ids[0] : undefined;
106
+ if (soleAccount) {
107
+ return { accountId: soleAccount, fromPrompt: false };
108
+ }
109
+ if (input.json || !isInteractive()) {
98
110
  return { fromPrompt: false };
99
111
  }
100
112
  const picked = await promptSelect({
@@ -113,15 +125,6 @@ async function resolveAccountIdOrPrompt(input) {
113
125
  });
114
126
  return picked ? { accountId: picked, fromPrompt: true } : { fromPrompt: false };
115
127
  }
116
- async function promptMissingText(message, current, validate) {
117
- if (current && current.length > 0)
118
- return current;
119
- const answer = await promptText({
120
- message,
121
- ...(validate ? { validate } : {}),
122
- });
123
- return answer === undefined ? undefined : answer;
124
- }
125
128
  async function readHsxConfigProjectId(root) {
126
129
  try {
127
130
  const contents = await readFile(join(root, 'hsx.config.ts'), 'utf8');
@@ -144,6 +147,36 @@ function cancelledResult(argv, command) {
144
147
  reporter.info('Cancelled.');
145
148
  return { exitCode: 130 };
146
149
  }
150
+ /**
151
+ * The default portal for a dev session, discovered rather than prompted:
152
+ * the resolved HS-X account's bound portal first (that pairing is explicit),
153
+ * then the HubSpot CLI config's default account — the same portal
154
+ * `hs project dev` would use. Undefined means local-only dev.
155
+ */
156
+ async function resolveDefaultPortalId(input) {
157
+ try {
158
+ const { loadStore } = await import('../account-store.js');
159
+ const store = await loadStore();
160
+ const account = (input.accountId ? store.accounts[input.accountId] : undefined) ??
161
+ (store.defaultAccountId ? store.accounts[store.defaultAccountId] : undefined);
162
+ if (account?.hubspotPortalId)
163
+ return String(account.hubspotPortalId);
164
+ }
165
+ catch {
166
+ // fall through to the HubSpot CLI config
167
+ }
168
+ try {
169
+ const { discoverHubSpotAccounts } = await import('../hubspot-auth.js');
170
+ const discovered = await discoverHubSpotAccounts(input.root);
171
+ const account = discovered?.accounts.find((a) => a.isDefault) ?? discovered?.accounts[0];
172
+ if (account)
173
+ return String(account.portalId);
174
+ }
175
+ catch {
176
+ // no portal anywhere — run local-only
177
+ }
178
+ return undefined;
179
+ }
147
180
  export async function devCommand({ argv, root, json, }) {
148
181
  if (argv[1] === 'cleanup') {
149
182
  return await devCleanupCommand({ argv, json });
@@ -151,6 +184,24 @@ export async function devCommand({ argv, root, json, }) {
151
184
  if (argv[1] === 'status') {
152
185
  return await devStatusCommand({ argv, json });
153
186
  }
187
+ if (!json && isInteractive()) {
188
+ try {
189
+ const selected = await resolveLocalProject({
190
+ root,
191
+ allowPrompt: true,
192
+ message: 'Which local project should run in dev mode?',
193
+ prompts: { select: promptSelect },
194
+ });
195
+ if (selected && selected.root !== root) {
196
+ return await devCommand({ argv, root: selected.root, json });
197
+ }
198
+ }
199
+ catch (error) {
200
+ if (error instanceof LocalProjectPickerCancelled)
201
+ return cancelledResult(argv, 'dev');
202
+ throw error;
203
+ }
204
+ }
154
205
  if (argv[1] === 'invoke') {
155
206
  return await devInvokeCommand({ argv, root, json });
156
207
  }
@@ -169,7 +220,9 @@ export async function devCommand({ argv, root, json, }) {
169
220
  const workers = await discoverWorkerManifests(root);
170
221
  await generateProjectArtifacts({ root, workers });
171
222
  let portalId = resolveFlag(argv, '--portal');
172
- let targetOrigin = resolveFlag(argv, '--target-origin');
223
+ const targetOrigin = resolveFlag(argv, '--target-origin');
224
+ const telemetryOrigin = resolveFlag(argv, '--telemetry-origin');
225
+ const forceDevOverride = argv.includes('--force');
173
226
  const resolvedDevAcct = await resolveAccountIdOrPrompt({
174
227
  argv,
175
228
  json,
@@ -191,11 +244,10 @@ export async function devCommand({ argv, root, json, }) {
191
244
  // Control plane is a single deployed instance; default to it. Overrides
192
245
  // (env or flag) exist for self-host / local-cp test runs only.
193
246
  const controlPlaneUrl = formatConfigUrl(devCliConfig.controlPlaneUrl);
194
- const developerId = resolveFlag(argv, '--developer-id') ??
195
- devCliConfig.userId;
247
+ const developerId = resolveFlag(argv, '--developer-id') ?? devCliConfig.userId;
196
248
  const sessionId = resolveFlag(argv, '--session-id') ??
197
249
  `devsess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
198
- let ttlSeconds = Number(resolveFlag(argv, '--ttl-seconds') ?? '7200');
250
+ const ttlSeconds = Number(resolveFlag(argv, '--ttl-seconds') ?? '7200');
199
251
  const selectedCapabilities = resolveFlags(argv, '--capability');
200
252
  const allCapabilities = workers.flatMap((worker) => worker.capabilities
201
253
  .filter((capability) => capability.runtimeNeeds.includes('worker'))
@@ -205,37 +257,23 @@ export async function devCommand({ argv, root, json, }) {
205
257
  worker: worker.name,
206
258
  })));
207
259
  const allCapabilityIds = allCapabilities.map((c) => c.id);
208
- if (!json && isInteractive() && (controlPlaneUrl || portalId || targetOrigin)) {
209
- portalId = await promptMissingText('HubSpot portal id for dev override', portalId, (value) => /^\d+$/.test(value) ? undefined : 'Portal id must be numeric.');
210
- if (portalId === undefined)
211
- return cancelledResult(argv, 'dev');
212
- targetOrigin = await promptMissingText('Public dev target origin', targetOrigin, (value) => {
213
- try {
214
- const url = new URL(value);
215
- return url.protocol === 'https:' || url.hostname === '127.0.0.1' || url.hostname === 'localhost'
216
- ? undefined
217
- : 'Use an https URL, localhost, or 127.0.0.1.';
218
- }
219
- catch {
220
- return 'Enter a valid URL.';
221
- }
260
+ // Zero-prompt startup, like `hs project dev`: the portal id resolves from
261
+ // --portal the HS-X account store the HubSpot CLI config default; the
262
+ // override defaults to observe mode (no public target origin needed) and
263
+ // the TTL to 2h. --target-origin/--ttl-seconds stay as advanced flags, and
264
+ // nothing here ever blocks on a question.
265
+ if (portalId && !/^\d+$/.test(portalId)) {
266
+ const reporter = createReporter({ command: 'dev', argv });
267
+ const code = 'HSX_E_INPUT_BAD_FIELD';
268
+ reporter.error(code, `--portal must be numeric (got \`${portalId}\`).`, {
269
+ hint: 'Example: hs-x dev --portal 46993937',
270
+ docs_url: `https://hs-x.dev/errors/${code}`,
222
271
  });
223
- if (targetOrigin === undefined)
224
- return cancelledResult(argv, 'dev');
225
- if (!resolveFlag(argv, '--ttl-seconds')) {
226
- const ttl = await promptSelect({
227
- message: 'Dev override TTL',
228
- default: '7200',
229
- options: [
230
- { value: '3600', label: '1 hour', description: 'short local session' },
231
- { value: '7200', label: '2 hours', description: 'default' },
232
- { value: '14400', label: '4 hours', description: 'long pairing/debugging session' },
233
- ],
234
- });
235
- if (ttl === undefined)
236
- return cancelledResult(argv, 'dev');
237
- ttlSeconds = Number(ttl);
238
- }
272
+ reporter.done(undefined, 10);
273
+ return { exitCode: 10 };
274
+ }
275
+ if (!portalId) {
276
+ portalId = await resolveDefaultPortalId({ root, accountId });
239
277
  }
240
278
  // Probe the HubSpot project IR up front so the picker can show UI-extension
241
279
  // nodes alongside HS-X capabilities. No-op if hsproject.json isn't present.
@@ -250,6 +288,7 @@ export async function devCommand({ argv, root, json, }) {
250
288
  const totalCandidates = allCapabilities.length + (uiExtensionNodes?.uids.length ?? 0);
251
289
  if (portalId &&
252
290
  !json &&
291
+ argv.includes('--pick') &&
253
292
  selectedCapabilities.length === 0 &&
254
293
  totalCandidates > 1 &&
255
294
  isInteractive()) {
@@ -284,16 +323,23 @@ export async function devCommand({ argv, root, json, }) {
284
323
  .map((v) => v.slice(UI_PREFIX.length));
285
324
  }
286
325
  let devOverrideRegistration;
326
+ let forcedDevOverrideCleanup = [];
287
327
  // We register a dev override whenever the user has a HubSpot portal in
288
328
  // play. `hs-x connect` gives us the portal id implicitly; only edge cases
289
329
  // (no connected account, no --portal flag) skip registration.
290
330
  const shouldRegisterOverride = Boolean(portalId);
291
331
  if (shouldRegisterOverride) {
332
+ const portalIdForOverride = portalId;
333
+ if (!portalIdForOverride) {
334
+ throw new Error('Dev override needs a HubSpot portal id. Pass --portal <id>.');
335
+ }
292
336
  if (!accountId) {
293
- throw new Error('Dev override needs a connected HubSpot account. Run `hs-x connect` or pass --account-id.');
337
+ throw new Error('Dev override needs a connected HubSpot account. An authenticated personal access key ' +
338
+ '(what `hs-x doctor` reports) is not the same thing — dev needs the account bound to ' +
339
+ 'this machine. Run `hs-x connect hubspot` or pass --account-id.');
294
340
  }
295
341
  if (!projectId) {
296
- throw new Error('Could not determine project id. Add `name: "..."` to `hsx.config.ts`, set `HSX_PROJECT_ID`, or pass --project-id.');
342
+ throw new Error('Could not find an HS-X source project. Run from a directory containing `hsx.config.ts`, pass --cwd <project>, or set --project-id/HSX_PROJECT_ID for scripted override registration.');
297
343
  }
298
344
  if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
299
345
  throw new Error('--ttl-seconds must be a positive number.');
@@ -309,13 +355,23 @@ export async function devCommand({ argv, root, json, }) {
309
355
  if (!targetOrigin) {
310
356
  throw new Error('Dev override registration in --json mode requires --target-origin <url>.');
311
357
  }
358
+ forcedDevOverrideCleanup = forceDevOverride
359
+ ? await forceClearDevOverrideSessions({
360
+ controlPlaneUrl,
361
+ accountId,
362
+ userId: developerId,
363
+ projectId,
364
+ portalId: portalIdForOverride,
365
+ capabilityIds,
366
+ })
367
+ : [];
312
368
  devOverrideRegistration = await requestDevOverrideRegistration({
313
369
  controlPlaneUrl,
314
370
  accountId,
315
371
  userId: developerId,
316
372
  request: Schema.decodeUnknownSync(schemas.DevOverrideRegisterRequest)({
317
373
  projectId,
318
- portalId,
374
+ portalId: portalIdForOverride,
319
375
  targetOrigin,
320
376
  developerId,
321
377
  sessionId,
@@ -331,6 +387,10 @@ export async function devCommand({ argv, root, json, }) {
331
387
  command: 'dev',
332
388
  url: `http://127.0.0.1:${port}`,
333
389
  workers,
390
+ ...(telemetryOrigin ? { telemetryOrigin } : {}),
391
+ ...(forcedDevOverrideCleanup.length > 0
392
+ ? { forcedCleanup: forcedDevOverrideCleanup }
393
+ : {}),
334
394
  ...(devOverrideRegistration
335
395
  ? { mode: 'control-plane-dev-override', devOverrideRegistration }
336
396
  : {}),
@@ -338,7 +398,17 @@ export async function devCommand({ argv, root, json, }) {
338
398
  return { exitCode: 0 };
339
399
  }
340
400
  const startupBegan = Date.now();
341
- const renderRequestLog = makeDevRequestLogger(process.env, process.stderr, accountId);
401
+ // Requests print directly until the log-stream bus exists, then route
402
+ // through it as the `request` lane — so they respect the active filter and
403
+ // never trample the pinned status line.
404
+ const directRequestLog = makeDevRequestLogger(process.env, process.stderr, accountId);
405
+ let requestSink;
406
+ const renderRequestLog = (entry) => {
407
+ if (requestSink)
408
+ requestSink(entry);
409
+ else
410
+ directRequestLog(entry);
411
+ };
342
412
  const server = createServer((request, response) => {
343
413
  const requestStarted = Date.now();
344
414
  const method = request.method ?? 'GET';
@@ -362,6 +432,79 @@ export async function devCommand({ argv, root, json, }) {
362
432
  response.end(JSON.stringify({ workers }, null, 2));
363
433
  return;
364
434
  }
435
+ // Migrated UI-extension compatibility bridge. During `hs-x dev` its
436
+ // temporary trycloudflare.com origin points here, preserving the same
437
+ // /_hsx/cards/<id> contract as the deployed runtime while dispatching the
438
+ // current local handler body.
439
+ const cardMatch = /^\/_hsx\/cards\/([^/?]+)$/.exec(path.split('?', 1)[0] ?? path);
440
+ if (cardMatch && method === 'POST') {
441
+ const capabilityId = decodeURIComponent(cardMatch[1] ?? '');
442
+ const isCardBackend = allCapabilities.some((capability) => capability.id === capabilityId && capability.kind === 'card-backend');
443
+ if (!isCardBackend) {
444
+ response.writeHead(404, { 'content-type': 'application/json' });
445
+ response.end(JSON.stringify({ ok: false, error: 'unknown_card_backend', capabilityId }));
446
+ return;
447
+ }
448
+ const chunks = [];
449
+ request.on('data', (chunk) => chunks.push(chunk));
450
+ request.on('end', () => {
451
+ void (async () => {
452
+ const respond = (status, body) => {
453
+ response.writeHead(status, { 'content-type': 'application/json' });
454
+ response.end(JSON.stringify(body, null, 2));
455
+ };
456
+ let parsed;
457
+ try {
458
+ const raw = Buffer.concat(chunks).toString('utf8');
459
+ const value = raw.length > 0 ? JSON.parse(raw) : {};
460
+ parsed = isRecord(value) ? value : {};
461
+ }
462
+ catch {
463
+ respond(400, { ok: false, error: 'invalid_json' });
464
+ return;
465
+ }
466
+ try {
467
+ const { invokeCapabilityLocally, DevInvokeError, describeInvocationError } = await import('../dev/invoke.js');
468
+ try {
469
+ const outcome = await invokeCapabilityLocally({
470
+ root,
471
+ capabilityId,
472
+ ...(parsed.input !== undefined ? { input: parsed.input } : {}),
473
+ ...(parsed.enrolledObject !== undefined
474
+ ? { enrolledObject: parsed.enrolledObject }
475
+ : {}),
476
+ ...(parsed.install !== undefined ? { install: parsed.install } : {}),
477
+ });
478
+ respond(outcome.status, outcome.body ?? null);
479
+ }
480
+ catch (error) {
481
+ if (error instanceof DevInvokeError) {
482
+ respond(error.code === 'HSX_E_DEV_UNKNOWN_CAPABILITY' ? 404 : 500, {
483
+ ok: false,
484
+ error: error.code,
485
+ message: error.message,
486
+ ...(error.hint ? { hint: error.hint } : {}),
487
+ });
488
+ return;
489
+ }
490
+ respond(500, {
491
+ ok: false,
492
+ error: 'HSX_E_INTERNAL',
493
+ message: describeInvocationError(error),
494
+ });
495
+ }
496
+ }
497
+ catch (error) {
498
+ respond(500, {
499
+ ok: false,
500
+ error: 'HSX_E_INTERNAL',
501
+ message: error instanceof Error ? error.message : String(error),
502
+ });
503
+ }
504
+ })();
505
+ });
506
+ return;
507
+ }
365
508
  // Invocation harness over HTTP (P2): same engine as `hs-x dev invoke`,
366
509
  // for agents/CI/programs. Body: {input?, enrolledObject?, install?}.
367
510
  const invokeMatch = /^\/_hsx\/invoke\/([^/?]+)$/.exec(path.split('?', 1)[0] ?? path);
@@ -472,6 +615,25 @@ export async function devCommand({ argv, root, json, }) {
472
615
  const capabilityCount = workers.reduce((c, w) => c + w.capabilities.length, 0);
473
616
  const subject = devOverrideRegistration ? `portal ${devOverrideRegistration.portalId}` : 'local';
474
617
  const url = `http://127.0.0.1:${port}`;
618
+ // A freshly migrated card has no deployed Worker origin yet. Give its
619
+ // generated compatibility shim a public quick-tunnel origin that points at
620
+ // this local server, and stamp the source/allow-list before HubSpot's
621
+ // UI-extension dev session translates and bundles the project.
622
+ const { hasMigratedCardServerlessShim, prepareMigratedCardDevArtifacts } = await import('../dev/migrated-card.js');
623
+ const shouldStartMigratedCardTunnel = Boolean(selectedUiNodeUids?.length) && (await hasMigratedCardServerlessShim(root));
624
+ const migratedCardTunnel = shouldStartMigratedCardTunnel
625
+ ? await startHsxDevTunnel({
626
+ port,
627
+ reporter,
628
+ label: 'Migrated card backend tunnel',
629
+ })
630
+ : undefined;
631
+ const migratedCardDevArtifacts = migratedCardTunnel
632
+ ? await prepareMigratedCardDevArtifacts({
633
+ root,
634
+ runtimeOrigin: migratedCardTunnel.url,
635
+ })
636
+ : undefined;
475
637
  // Optional: start a HubSpot UI-extension dev session if this directory
476
638
  // contains an hsproject.json AND the user selected at least one UI node.
477
639
  const hubSpotDevSession = selectedUiNodeUids && selectedUiNodeUids.length > 0
@@ -488,22 +650,64 @@ export async function devCommand({ argv, root, json, }) {
488
650
  // events) from inside the card iframe and surfaces them in the terminal.
489
651
  // Stable default port 9099 so card code can hard-default the dev-log URL.
490
652
  const hsxLogPort = Number(resolveFlag(argv, '--hsx-log-port') ?? '9099');
653
+ // `--only` selectors pre-filter the stream (same grammar as the interactive
654
+ // keyboard, see dev/stream-filter.ts). Invalid selectors are a usage error.
655
+ const { parseOnlySelectors } = await import('../dev/stream-filter.js');
656
+ const onlyParsed = parseOnlySelectors(resolveFlags(argv, '--only'));
657
+ if (!onlyParsed.ok) {
658
+ const code = 'HSX_E_DEV_BAD_ONLY';
659
+ reporter.error(code, onlyParsed.message, {
660
+ hint: 'Selectors: frontend, backend, request, hubspot, errors, warnings, cap:<id>, or free text. Repeat --only to combine.',
661
+ docs_url: `https://hs-x.dev/errors/${code}`,
662
+ });
663
+ reporter.done(undefined, 2);
664
+ return { exitCode: 2 };
665
+ }
491
666
  const devLogStream = await startHsxDevLogStream({
492
667
  port: hsxLogPort,
668
+ root,
493
669
  reporter,
670
+ filter: onlyParsed.filter,
494
671
  });
672
+ if (devLogStream) {
673
+ const paint = makeDevLogPaint(process.env, process.stderr);
674
+ requestSink = (entry) => {
675
+ devLogStream.bus.emit({
676
+ lane: 'request',
677
+ level: entry.status >= 500 ? 'error' : entry.status >= 400 ? 'warn' : 'info',
678
+ ts: Date.now(),
679
+ msg: formatDevLogEntry(entry, paint, accountId),
680
+ });
681
+ };
682
+ }
495
683
  // Auto-managed Cloudflare quick tunnel — gives the deployed Worker a public
496
684
  // URL to POST telemetry envelopes to the local sidecar. The developer must
497
685
  // never have to run cloudflared themselves; that's a non-negotiable default.
498
- const devTunnel = shouldRegisterOverride
686
+ const devTunnel = shouldRegisterOverride && !telemetryOrigin
499
687
  ? await startHsxDevTunnel({ port: hsxLogPort, reporter })
500
688
  : undefined;
689
+ const telemetryBaseUrl = telemetryOrigin ?? devTunnel?.url;
501
690
  // Now register the dev override. If the developer didn't pass
502
691
  // `--target-origin`, default to observe mode and let the deployed Worker
503
692
  // run as normal — we only need the tunnel for the telemetry tee.
504
693
  if (shouldRegisterOverride && controlPlaneUrl && portalId && accountId && projectId) {
694
+ if (forceDevOverride) {
695
+ forcedDevOverrideCleanup = await forceClearDevOverrideSessions({
696
+ controlPlaneUrl,
697
+ accountId,
698
+ userId: developerId,
699
+ projectId,
700
+ portalId,
701
+ capabilityIds,
702
+ });
703
+ if (forcedDevOverrideCleanup.length > 0) {
704
+ reporter
705
+ .step(`Cleared ${forcedDevOverrideCleanup.reduce((count, item) => count + item.cleared.length, 0)} existing dev override${forcedDevOverrideCleanup.length === 1 ? '' : 's'}`)
706
+ .ok();
707
+ }
708
+ }
505
709
  const overrideMode = targetOrigin ? 'proxy' : 'observe';
506
- const effectiveTargetOrigin = targetOrigin ?? devTunnel?.url ?? `http://127.0.0.1:${hsxLogPort}`;
710
+ const effectiveTargetOrigin = targetOrigin ?? telemetryBaseUrl ?? `http://127.0.0.1:${hsxLogPort}`;
507
711
  devOverrideRegistration = await requestDevOverrideRegistration({
508
712
  controlPlaneUrl,
509
713
  accountId,
@@ -517,18 +721,86 @@ export async function devCommand({ argv, root, json, }) {
517
721
  capabilityIds,
518
722
  ttlSeconds,
519
723
  mode: overrideMode,
520
- ...(devTunnel ? { telemetryUrl: `${devTunnel.url}/__hsx/event` } : {}),
724
+ ...(telemetryBaseUrl && devLogStream
725
+ ? { telemetryUrl: devTelemetryUrl(telemetryBaseUrl, devLogStream.authToken) }
726
+ : {}),
521
727
  }),
522
728
  });
523
729
  }
524
- // Vercel-style header: arrow, command, subject, URL
525
- renderDevHeader(process.env, process.stderr, { subject, url });
526
- reporter.info(`Ready in ${elapsed}ms — ${workers.length} worker${workers.length === 1 ? '' : 's'} · ${capabilityCount} capabilit${capabilityCount === 1 ? 'y' : 'ies'}${devOverrideRegistration ? ` · portal ${devOverrideRegistration.portalId}` : ''}${hubSpotDevSession ? ` · ui-extension session ${hubSpotDevSession.sessionId}` : ''}${devLogStream ? ` · log stream ${devLogStream.url}` : ''}${devTunnel ? ` · tunnel ${devTunnel.url}` : ''}`);
527
- reporter.info('');
528
- reporter.info('Press Ctrl+C to stop');
730
+ // Railed startup checklist: identity header, then one row per surface.
731
+ reporter.header(subject);
732
+ reporter.rows([
733
+ { status: 'ok', key: 'server', value: url, detail: `ready in ${elapsed}ms` },
734
+ {
735
+ status: 'ok',
736
+ key: 'workers',
737
+ value: `${workers.length} worker${workers.length === 1 ? '' : 's'} · ${capabilityCount} capabilit${capabilityCount === 1 ? 'y' : 'ies'}`,
738
+ },
739
+ ...(devOverrideRegistration
740
+ ? [
741
+ {
742
+ status: 'ok',
743
+ key: 'portal',
744
+ value: String(devOverrideRegistration.portalId),
745
+ detail: 'dev override registered',
746
+ },
747
+ ]
748
+ : []),
749
+ ...(hubSpotDevSession
750
+ ? [
751
+ {
752
+ status: 'ok',
753
+ key: 'ui-extensions',
754
+ value: `session ${hubSpotDevSession.sessionId}`,
755
+ },
756
+ ]
757
+ : []),
758
+ ...(devLogStream
759
+ ? [{ status: 'ok', key: 'logs', value: `streaming on ${devLogStream.url}` }]
760
+ : []),
761
+ ...(devTunnel ? [{ status: 'ok', key: 'tunnel', value: devTunnel.url }] : []),
762
+ ...(migratedCardDevArtifacts
763
+ ? [
764
+ {
765
+ status: 'ok',
766
+ key: 'card backends',
767
+ value: migratedCardDevArtifacts.runtimeOrigin,
768
+ detail: 'temporary dev origin',
769
+ },
770
+ ]
771
+ : []),
772
+ ...(telemetryOrigin
773
+ ? [{ status: 'ok', key: 'telemetry', value: telemetryOrigin }]
774
+ : []),
775
+ ]);
776
+ if (!portalId) {
777
+ reporter.info('No HubSpot portal found — running local-only. Pass `--portal <id>` (or set a default with `hs auth` / `hs-x login`) to mirror a live portal.');
778
+ }
779
+ // The pinned status line carries the key map (incl. quit); repeating it in
780
+ // an info line doubled the hints. Only sessions without the status line
781
+ // (non-TTY, NO_COLOR) get a stop instruction.
782
+ if (!devLogStream?.renderer.hasStatusLine()) {
783
+ reporter.info('');
784
+ reporter.info('Press Ctrl+C to stop');
785
+ }
529
786
  reporter.info('');
787
+ // Interactive stream controls — single keystrokes filter/pause the unified
788
+ // log stream. No-op when stdin is not a TTY.
789
+ const keyboard = devLogStream
790
+ ? (await import('../dev/keyboard.js')).attachDevKeyboard({
791
+ renderer: devLogStream.renderer,
792
+ onQuit: () => process.kill(process.pid, 'SIGINT'),
793
+ })
794
+ : undefined;
795
+ devLogStream?.renderer.ready();
530
796
  await new Promise((resolveClose) => {
531
797
  const close = async () => {
798
+ keyboard?.stop();
799
+ // Signal-driven shutdown can terminate the Bun process before awaited
800
+ // cleanup settles. Restore temporarily stamped migration sources
801
+ // synchronously first; the remaining network/process cleanup is
802
+ // best-effort and may safely await.
803
+ migratedCardDevArtifacts?.restoreSync();
532
804
  if (hubSpotDevSession) {
533
805
  try {
534
806
  await hubSpotDevSession.cleanup();
@@ -552,6 +824,22 @@ export async function devCommand({ argv, root, json, }) {
552
824
  // best-effort: tunnel cleanup failures should not block exit.
553
825
  }
554
826
  }
827
+ if (migratedCardDevArtifacts) {
828
+ try {
829
+ await migratedCardDevArtifacts.restore();
830
+ }
831
+ catch {
832
+ // best-effort: source restoration should not block shutdown.
833
+ }
834
+ }
835
+ if (migratedCardTunnel) {
836
+ try {
837
+ await migratedCardTunnel.cleanup();
838
+ }
839
+ catch {
840
+ // best-effort: tunnel cleanup failures should not block exit.
841
+ }
842
+ }
555
843
  if (devLogStream) {
556
844
  try {
557
845
  await devLogStream.cleanup();
@@ -575,11 +863,44 @@ async function startHsxDevLogStream(input) {
575
863
  import('../dev/stream-renderer.js'),
576
864
  ]);
577
865
  const bus = createDevEventBus();
578
- const server = await startDevLogServer({ bus, port: input.port });
579
- const renderer = startDevStreamRenderer({ bus });
866
+ const authToken = randomBytes(32).toString('base64url');
867
+ const server = await startDevLogServer({ bus, port: input.port, authToken });
868
+ const renderer = startDevStreamRenderer({
869
+ bus,
870
+ deferStatus: true,
871
+ ...(input.filter ? { filter: input.filter } : {}),
872
+ });
873
+ // HubSpot-side lane: renders/proxy failures that never reach our Worker
874
+ // only exist in HubSpot's app logs — poll them into the same stream.
875
+ // Missing binding/credentials just means the lane stays off; `hs-x dev`
876
+ // must work without HubSpot credentials.
877
+ let hubspotPoller;
878
+ try {
879
+ const [{ resolveHubSpotLogsSource }, { startHubSpotLogsPoller }] = await Promise.all([
880
+ import('../hubspot-app-logs.js'),
881
+ import('../dev/hubspot-logs-poller.js'),
882
+ ]);
883
+ const resolution = await resolveHubSpotLogsSource({ cwd: input.root });
884
+ if (resolution.ok) {
885
+ hubspotPoller = startHubSpotLogsPoller({ bus, source: resolution.source });
886
+ bus.emit({
887
+ lane: 'hubspot',
888
+ level: 'debug',
889
+ ts: Date.now(),
890
+ msg: `lane active — polling HubSpot app logs for app ${resolution.source.appId}`,
891
+ });
892
+ }
893
+ }
894
+ catch {
895
+ // The HubSpot lane is additive; never fail the dev stream over it.
896
+ }
580
897
  return {
581
898
  url: server.url,
899
+ authToken,
900
+ renderer,
901
+ bus,
582
902
  async cleanup() {
903
+ hubspotPoller?.stop();
583
904
  renderer.stop();
584
905
  await server.close();
585
906
  },
@@ -590,8 +911,13 @@ async function startHsxDevLogStream(input) {
590
911
  return undefined;
591
912
  }
592
913
  }
914
+ function devTelemetryUrl(tunnelUrl, authToken) {
915
+ const url = new URL('/__hsx/event', tunnelUrl);
916
+ url.searchParams.set('hsx_dev_token', authToken);
917
+ return url.toString();
918
+ }
593
919
  async function startHsxDevTunnel(input) {
594
- const step = input.reporter.step('Cloudflare quick tunnel');
920
+ const step = input.reporter.step(input.label ?? 'Cloudflare quick tunnel');
595
921
  try {
596
922
  const { startCloudflaredTunnel } = await import('../dev/tunnel.js');
597
923
  const tunnel = await startCloudflaredTunnel({ port: input.port });
@@ -803,12 +1129,45 @@ async function devInvokeCommand({ argv, root, json, }) {
803
1129
  else {
804
1130
  step.fail(`status ${outcome.status}`);
805
1131
  }
806
- for (const line of JSON.stringify(responseBody ?? null, null, 2).split('\n')) {
807
- reporter.info(` ${line}`);
1132
+ // Human view: the handler's output fields on the rail at full brightness —
1133
+ // the wire envelope stays on `--json`. Falls back to the raw body when the
1134
+ // response has no extractable output record (errors, bespoke shapes).
1135
+ const outputRecord = extractInvokeOutput(responseBody);
1136
+ if (invocationOk && outputRecord) {
1137
+ reporter.rows(Object.entries(outputRecord).map(([key, value]) => ({
1138
+ key,
1139
+ value: typeof value === 'string' ? value : JSON.stringify(value),
1140
+ })));
1141
+ reporter.info('');
1142
+ reporter.info('Full payload: re-run with `--json`');
1143
+ }
1144
+ else {
1145
+ for (const line of JSON.stringify(responseBody ?? null, null, 2).split('\n')) {
1146
+ reporter.info(` ${line}`);
1147
+ }
808
1148
  }
809
1149
  reporter.done(invocationOk ? 'Invoked' : 'Failed', invocationOk ? 0 : 1);
810
1150
  return { exitCode: invocationOk ? 0 : 1 };
811
1151
  }
1152
+ /**
1153
+ * Pull the handler's own output out of the invoke response envelope:
1154
+ * tools/actions nest it at `result.output`; card backends return the
1155
+ * handler's object as the body. Returns undefined when there is nothing
1156
+ * row-shaped to show.
1157
+ */
1158
+ function extractInvokeOutput(body) {
1159
+ if (!isRecordValue(body))
1160
+ return undefined;
1161
+ const result = body.result;
1162
+ if (isRecordValue(result) && isRecordValue(result.output)) {
1163
+ return result.output;
1164
+ }
1165
+ if (isRecordValue(result) && !('output' in result))
1166
+ return result;
1167
+ if (!('ok' in body) && !('result' in body))
1168
+ return body;
1169
+ return undefined;
1170
+ }
812
1171
  function isRecordValue(value) {
813
1172
  return typeof value === 'object' && value !== null;
814
1173
  }
@@ -1009,6 +1368,27 @@ async function requestDevOverrideCleanup({ controlPlaneUrl, accountId, userId, s
1009
1368
  }
1010
1369
  return Schema.decodeUnknownSync(schemas.DevOverrideClearResponse)(body);
1011
1370
  }
1371
+ async function forceClearDevOverrideSessions({ controlPlaneUrl, accountId, userId, projectId, portalId, capabilityIds, }) {
1372
+ const active = await requestDevOverrideStatus({ controlPlaneUrl, accountId, userId });
1373
+ const wantedCapabilities = new Set(capabilityIds);
1374
+ const sessions = [
1375
+ ...new Set(active.overrides
1376
+ .filter((override) => override.projectId === projectId &&
1377
+ override.portalId === portalId &&
1378
+ wantedCapabilities.has(override.capabilityId))
1379
+ .map((override) => override.sessionId)),
1380
+ ];
1381
+ const cleared = [];
1382
+ for (const sessionId of sessions) {
1383
+ cleared.push(await requestDevOverrideCleanup({
1384
+ controlPlaneUrl,
1385
+ accountId,
1386
+ userId,
1387
+ sessionId,
1388
+ }));
1389
+ }
1390
+ return cleared;
1391
+ }
1012
1392
  async function discoverWorkerManifests(root, options = {}) {
1013
1393
  const files = await collectFiles(join(root, 'src'));
1014
1394
  const workers = [];
@@ -1355,6 +1735,7 @@ const devUrlOption = Options.text('url').pipe(Options.optional);
1355
1735
  const portOption = Options.text('port').pipe(Options.optional);
1356
1736
  const portalOption = Options.text('portal').pipe(Options.optional);
1357
1737
  const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
1738
+ const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
1358
1739
  const accountIdOption = Options.text('account-id').pipe(Options.optional);
1359
1740
  const projectIdOption = Options.text('project-id').pipe(Options.optional);
1360
1741
  const developerIdOption = Options.text('developer-id').pipe(Options.optional);
@@ -1365,9 +1746,12 @@ const hsxLogPortOption = Options.text('hsx-log-port').pipe(Options.optional);
1365
1746
  const controlPlaneUrlOption = Options.text('control-plane-url').pipe(Options.optional);
1366
1747
  const runtimeControlPlaneUrlOption = Options.text('runtime-control-plane-url').pipe(Options.optional);
1367
1748
  const capabilityOption = Options.text('capability').pipe(Options.repeated);
1749
+ const onlyOption = Options.text('only').pipe(Options.repeated);
1750
+ const pickOption = Options.boolean('pick').pipe(Options.withDefault(false));
1368
1751
  const noHubspotOption = Options.boolean('no-hubspot').pipe(Options.withDefault(false));
1369
1752
  const noUiExtensionsOption = Options.boolean('no-ui-extensions').pipe(Options.withDefault(false));
1370
1753
  const yesOption = Options.boolean('yes').pipe(Options.withAlias('y'), Options.withDefault(false));
1754
+ const forceOption = Options.boolean('force').pipe(Options.withDefault(false));
1371
1755
  export const devCmd = Command.make('dev', {
1372
1756
  json: jsonOption,
1373
1757
  cwd: cwdOption,
@@ -1382,6 +1766,7 @@ export const devCmd = Command.make('dev', {
1382
1766
  port: portOption,
1383
1767
  portal: portalOption,
1384
1768
  targetOrigin: targetOriginOption,
1769
+ telemetryOrigin: telemetryOriginOption,
1385
1770
  accountId: accountIdOption,
1386
1771
  projectId: projectIdOption,
1387
1772
  developerId: developerIdOption,
@@ -1392,9 +1777,12 @@ export const devCmd = Command.make('dev', {
1392
1777
  controlPlaneUrl: controlPlaneUrlOption,
1393
1778
  runtimeControlPlaneUrl: runtimeControlPlaneUrlOption,
1394
1779
  capability: capabilityOption,
1780
+ only: onlyOption,
1781
+ pick: pickOption,
1395
1782
  noHubspot: noHubspotOption,
1396
1783
  noUiExtensions: noUiExtensionsOption,
1397
1784
  yes: yesOption,
1785
+ force: forceOption,
1398
1786
  }, (opts) => runHandler('dev', { json: opts.json }, Effect.gen(function* () {
1399
1787
  // root: injected cwd, overridden by --cwd.
1400
1788
  const injectedCwd = yield* Cwd;
@@ -1428,6 +1816,9 @@ export const devCmd = Command.make('dev', {
1428
1816
  if (Option.isSome(opts.targetOrigin)) {
1429
1817
  argv.push('--target-origin', opts.targetOrigin.value);
1430
1818
  }
1819
+ if (Option.isSome(opts.telemetryOrigin)) {
1820
+ argv.push('--telemetry-origin', opts.telemetryOrigin.value);
1821
+ }
1431
1822
  if (Option.isSome(opts.accountId))
1432
1823
  argv.push('--account-id', opts.accountId.value);
1433
1824
  if (Option.isSome(opts.projectId))
@@ -1451,13 +1842,19 @@ export const devCmd = Command.make('dev', {
1451
1842
  }
1452
1843
  for (const capability of opts.capability)
1453
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');
1454
1849
  if (opts.noHubspot)
1455
1850
  argv.push('--no-hubspot');
1456
1851
  if (opts.noUiExtensions)
1457
1852
  argv.push('--no-ui-extensions');
1458
1853
  if (opts.yes)
1459
1854
  argv.push('--yes');
1460
- yield* runQuarantined(opts.json, () => devCommand({ argv, root, json: opts.json }));
1855
+ if (opts.force)
1856
+ argv.push('--force');
1857
+ yield* runQuarantined('dev', opts.json, () => devCommand({ argv, root, json: opts.json }));
1461
1858
  })));
1462
1859
  /**
1463
1860
  * `hs-x dev invoke --remote`: dispatch at the DEPLOYED Worker, where install
@@ -1520,9 +1917,7 @@ async function devInvokeRemote(args) {
1520
1917
  });
1521
1918
  const durationMs = Date.now() - startedAt;
1522
1919
  const body = outcome.body;
1523
- const ok = outcome.status >= 200 &&
1524
- outcome.status < 300 &&
1525
- (!isRecordValue(body) || body.ok !== false);
1920
+ const ok = outcome.status >= 200 && outcome.status < 300 && (!isRecordValue(body) || body.ok !== false);
1526
1921
  const reporter = createReporter({ command: 'dev invoke', argv, entry: !json });
1527
1922
  if (json) {
1528
1923
  process.stdout.write(`${JSON.stringify({