@hs-x/cli 0.2.6 → 0.3.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 (298) 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/cloudflare-worker-versions.d.ts +184 -0
  32. package/dist/cloudflare-worker-versions.d.ts.map +1 -0
  33. package/dist/cloudflare-worker-versions.js +360 -0
  34. package/dist/cloudflare-worker-versions.js.map +1 -0
  35. package/dist/commands/account.d.ts +28 -13
  36. package/dist/commands/account.d.ts.map +1 -1
  37. package/dist/commands/account.js +219 -69
  38. package/dist/commands/account.js.map +1 -1
  39. package/dist/commands/api.d.ts +1 -1
  40. package/dist/commands/api.d.ts.map +1 -1
  41. package/dist/commands/api.js +6 -10
  42. package/dist/commands/api.js.map +1 -1
  43. package/dist/commands/completion.d.ts.map +1 -1
  44. package/dist/commands/completion.js +13 -7
  45. package/dist/commands/completion.js.map +1 -1
  46. package/dist/commands/connect.d.ts.map +1 -1
  47. package/dist/commands/connect.js +239 -125
  48. package/dist/commands/connect.js.map +1 -1
  49. package/dist/commands/control-plane-read.d.ts +72 -11
  50. package/dist/commands/control-plane-read.d.ts.map +1 -1
  51. package/dist/commands/control-plane-read.js +1035 -58
  52. package/dist/commands/control-plane-read.js.map +1 -1
  53. package/dist/commands/deploy-promote.d.ts +25 -3
  54. package/dist/commands/deploy-promote.d.ts.map +1 -1
  55. package/dist/commands/deploy-promote.js +314 -38
  56. package/dist/commands/deploy-promote.js.map +1 -1
  57. package/dist/commands/deploy.d.ts +67 -4
  58. package/dist/commands/deploy.d.ts.map +1 -1
  59. package/dist/commands/deploy.js +1333 -255
  60. package/dist/commands/deploy.js.map +1 -1
  61. package/dist/commands/dev.d.ts +5 -1
  62. package/dist/commands/dev.d.ts.map +1 -1
  63. package/dist/commands/dev.js +502 -107
  64. package/dist/commands/dev.js.map +1 -1
  65. package/dist/commands/doctor.d.ts +15 -1
  66. package/dist/commands/doctor.d.ts.map +1 -1
  67. package/dist/commands/doctor.js +131 -20
  68. package/dist/commands/doctor.js.map +1 -1
  69. package/dist/commands/flags.d.ts +108 -6
  70. package/dist/commands/flags.d.ts.map +1 -1
  71. package/dist/commands/flags.js +996 -75
  72. package/dist/commands/flags.js.map +1 -1
  73. package/dist/commands/help-command.d.ts +14 -0
  74. package/dist/commands/help-command.d.ts.map +1 -1
  75. package/dist/commands/help-command.js +144 -44
  76. package/dist/commands/help-command.js.map +1 -1
  77. package/dist/commands/history.js +14 -10
  78. package/dist/commands/history.js.map +1 -1
  79. package/dist/commands/init.d.ts +4 -2
  80. package/dist/commands/init.d.ts.map +1 -1
  81. package/dist/commands/init.js +69 -12
  82. package/dist/commands/init.js.map +1 -1
  83. package/dist/commands/link.d.ts +2 -3
  84. package/dist/commands/link.d.ts.map +1 -1
  85. package/dist/commands/link.js +201 -101
  86. package/dist/commands/link.js.map +1 -1
  87. package/dist/commands/login.d.ts +2 -2
  88. package/dist/commands/login.d.ts.map +1 -1
  89. package/dist/commands/login.js +22 -5
  90. package/dist/commands/login.js.map +1 -1
  91. package/dist/commands/migrate.d.ts +23 -3
  92. package/dist/commands/migrate.d.ts.map +1 -1
  93. package/dist/commands/migrate.js +500 -73
  94. package/dist/commands/migrate.js.map +1 -1
  95. package/dist/commands/rollback.d.ts +42 -2
  96. package/dist/commands/rollback.d.ts.map +1 -1
  97. package/dist/commands/rollback.js +422 -58
  98. package/dist/commands/rollback.js.map +1 -1
  99. package/dist/commands/secrets.d.ts +76 -3
  100. package/dist/commands/secrets.d.ts.map +1 -1
  101. package/dist/commands/secrets.js +328 -41
  102. package/dist/commands/secrets.js.map +1 -1
  103. package/dist/commands/status.d.ts +13 -2
  104. package/dist/commands/status.d.ts.map +1 -1
  105. package/dist/commands/status.js +154 -17
  106. package/dist/commands/status.js.map +1 -1
  107. package/dist/commands/unlink.d.ts +2 -2
  108. package/dist/commands/unlink.d.ts.map +1 -1
  109. package/dist/commands/unlink.js +2 -2
  110. package/dist/commands/unlink.js.map +1 -1
  111. package/dist/commands/update.d.ts.map +1 -1
  112. package/dist/commands/update.js +4 -3
  113. package/dist/commands/update.js.map +1 -1
  114. package/dist/commands/validate.d.ts +12 -3
  115. package/dist/commands/validate.d.ts.map +1 -1
  116. package/dist/commands/validate.js +70 -8
  117. package/dist/commands/validate.js.map +1 -1
  118. package/dist/constants.d.ts +1 -1
  119. package/dist/constants.js +1 -1
  120. package/dist/control-plane-fetch.d.ts.map +1 -1
  121. package/dist/control-plane-fetch.js +5 -4
  122. package/dist/control-plane-fetch.js.map +1 -1
  123. package/dist/deploy-revision-capture.d.ts +48 -0
  124. package/dist/deploy-revision-capture.d.ts.map +1 -0
  125. package/dist/deploy-revision-capture.js +125 -0
  126. package/dist/deploy-revision-capture.js.map +1 -0
  127. package/dist/dev/compat-shim.d.ts.map +1 -1
  128. package/dist/dev/compat-shim.js +10 -35
  129. package/dist/dev/compat-shim.js.map +1 -1
  130. package/dist/dev/event-bus.d.ts +5 -2
  131. package/dist/dev/event-bus.d.ts.map +1 -1
  132. package/dist/dev/event-bus.js +4 -1
  133. package/dist/dev/event-bus.js.map +1 -1
  134. package/dist/dev/hubspot-logs-poller.d.ts +35 -0
  135. package/dist/dev/hubspot-logs-poller.d.ts.map +1 -0
  136. package/dist/dev/hubspot-logs-poller.js +94 -0
  137. package/dist/dev/hubspot-logs-poller.js.map +1 -0
  138. package/dist/dev/invoke.d.ts +9 -1
  139. package/dist/dev/invoke.d.ts.map +1 -1
  140. package/dist/dev/invoke.js +69 -20
  141. package/dist/dev/invoke.js.map +1 -1
  142. package/dist/dev/keyboard.d.ts +31 -0
  143. package/dist/dev/keyboard.d.ts.map +1 -0
  144. package/dist/dev/keyboard.js +143 -0
  145. package/dist/dev/keyboard.js.map +1 -0
  146. package/dist/dev/log-server.d.ts +7 -0
  147. package/dist/dev/log-server.d.ts.map +1 -1
  148. package/dist/dev/log-server.js +68 -31
  149. package/dist/dev/log-server.js.map +1 -1
  150. package/dist/dev/migrated-card.d.ts +17 -0
  151. package/dist/dev/migrated-card.d.ts.map +1 -0
  152. package/dist/dev/migrated-card.js +114 -0
  153. package/dist/dev/migrated-card.js.map +1 -0
  154. package/dist/dev/stream-filter.d.ts +40 -0
  155. package/dist/dev/stream-filter.d.ts.map +1 -0
  156. package/dist/dev/stream-filter.js +93 -0
  157. package/dist/dev/stream-filter.js.map +1 -0
  158. package/dist/dev/stream-renderer.d.ts +34 -0
  159. package/dist/dev/stream-renderer.d.ts.map +1 -1
  160. package/dist/dev/stream-renderer.js +222 -8
  161. package/dist/dev/stream-renderer.js.map +1 -1
  162. package/dist/errors-registry.d.ts.map +1 -1
  163. package/dist/errors-registry.js +280 -36
  164. package/dist/errors-registry.js.map +1 -1
  165. package/dist/errors.d.ts.map +1 -1
  166. package/dist/errors.js.map +1 -1
  167. package/dist/help.d.ts.map +1 -1
  168. package/dist/help.js +8 -0
  169. package/dist/help.js.map +1 -1
  170. package/dist/hubspot-app-logs.d.ts +76 -0
  171. package/dist/hubspot-app-logs.d.ts.map +1 -0
  172. package/dist/hubspot-app-logs.js +323 -0
  173. package/dist/hubspot-app-logs.js.map +1 -0
  174. package/dist/hubspot-auth.d.ts +8 -2
  175. package/dist/hubspot-auth.d.ts.map +1 -1
  176. package/dist/hubspot-auth.js +36 -12
  177. package/dist/hubspot-auth.js.map +1 -1
  178. package/dist/hubspot-developer-client.js +119 -11
  179. package/dist/hubspot-developer-client.js.map +1 -1
  180. package/dist/index.d.ts +1 -0
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +1 -0
  183. package/dist/index.js.map +1 -1
  184. package/dist/init/templates.d.ts +10 -0
  185. package/dist/init/templates.d.ts.map +1 -1
  186. package/dist/init/templates.js +157 -14
  187. package/dist/init/templates.js.map +1 -1
  188. package/dist/link-migration-journal.d.ts +131 -0
  189. package/dist/link-migration-journal.d.ts.map +1 -0
  190. package/dist/link-migration-journal.js +395 -0
  191. package/dist/link-migration-journal.js.map +1 -0
  192. package/dist/link-preflight.d.ts +78 -0
  193. package/dist/link-preflight.d.ts.map +1 -0
  194. package/dist/link-preflight.js +231 -0
  195. package/dist/link-preflight.js.map +1 -0
  196. package/dist/local-project-picker.d.ts +34 -0
  197. package/dist/local-project-picker.d.ts.map +1 -0
  198. package/dist/local-project-picker.js +121 -0
  199. package/dist/local-project-picker.js.map +1 -0
  200. package/dist/logs-interactive.d.ts +37 -0
  201. package/dist/logs-interactive.d.ts.map +1 -0
  202. package/dist/logs-interactive.js +110 -0
  203. package/dist/logs-interactive.js.map +1 -0
  204. package/dist/output-context.d.ts +13 -0
  205. package/dist/output-context.d.ts.map +1 -0
  206. package/dist/output-context.js +46 -0
  207. package/dist/output-context.js.map +1 -0
  208. package/dist/project-context.d.ts +55 -0
  209. package/dist/project-context.d.ts.map +1 -0
  210. package/dist/project-context.js +115 -0
  211. package/dist/project-context.js.map +1 -0
  212. package/dist/project-picker.d.ts +65 -0
  213. package/dist/project-picker.d.ts.map +1 -0
  214. package/dist/project-picker.js +227 -0
  215. package/dist/project-picker.js.map +1 -0
  216. package/dist/prompt.d.ts +8 -0
  217. package/dist/prompt.d.ts.map +1 -1
  218. package/dist/prompt.js +81 -13
  219. package/dist/prompt.js.map +1 -1
  220. package/dist/reporter/human.d.ts +10 -2
  221. package/dist/reporter/human.d.ts.map +1 -1
  222. package/dist/reporter/human.js +82 -14
  223. package/dist/reporter/human.js.map +1 -1
  224. package/dist/reporter/index.d.ts.map +1 -1
  225. package/dist/reporter/index.js +7 -5
  226. package/dist/reporter/index.js.map +1 -1
  227. package/dist/reporter/json.d.ts +2 -1
  228. package/dist/reporter/json.d.ts.map +1 -1
  229. package/dist/reporter/json.js +10 -7
  230. package/dist/reporter/json.js.map +1 -1
  231. package/dist/reporter/style.d.ts +19 -0
  232. package/dist/reporter/style.d.ts.map +1 -1
  233. package/dist/reporter/style.js +126 -6
  234. package/dist/reporter/style.js.map +1 -1
  235. package/dist/reporter/types.d.ts +34 -4
  236. package/dist/reporter/types.d.ts.map +1 -1
  237. package/dist/reporter/types.js.map +1 -1
  238. package/dist/rollback-traffic.d.ts +85 -0
  239. package/dist/rollback-traffic.d.ts.map +1 -0
  240. package/dist/rollback-traffic.js +204 -0
  241. package/dist/rollback-traffic.js.map +1 -0
  242. package/dist/services/account-store.d.ts +3 -1
  243. package/dist/services/account-store.d.ts.map +1 -1
  244. package/dist/services/account-store.js +2 -1
  245. package/dist/services/account-store.js.map +1 -1
  246. package/dist/services/app-paths.d.ts.map +1 -1
  247. package/dist/services/app-paths.js +3 -1
  248. package/dist/services/app-paths.js.map +1 -1
  249. package/dist/services/cloudflare-auth.d.ts +1 -1
  250. package/dist/services/cloudflare-auth.d.ts.map +1 -1
  251. package/dist/services/cloudflare-auth.js +2 -2
  252. package/dist/services/cloudflare-auth.js.map +1 -1
  253. package/dist/services/cloudflare-kv.d.ts.map +1 -1
  254. package/dist/services/cloudflare-kv.js.map +1 -1
  255. package/dist/services/command-history.d.ts.map +1 -1
  256. package/dist/services/command-history.js.map +1 -1
  257. package/dist/services/config.d.ts.map +1 -1
  258. package/dist/services/config.js +1 -1
  259. package/dist/services/config.js.map +1 -1
  260. package/dist/services/control-plane.d.ts.map +1 -1
  261. package/dist/services/control-plane.js +3 -3
  262. package/dist/services/control-plane.js.map +1 -1
  263. package/dist/services/env-loader.d.ts.map +1 -1
  264. package/dist/services/env-loader.js +1 -3
  265. package/dist/services/env-loader.js.map +1 -1
  266. package/dist/services/http.d.ts.map +1 -1
  267. package/dist/services/http.js.map +1 -1
  268. package/dist/services/hubspot-dev-client.d.ts.map +1 -1
  269. package/dist/services/hubspot-dev-client.js +30 -0
  270. package/dist/services/hubspot-dev-client.js.map +1 -1
  271. package/dist/services/live.d.ts +2 -2
  272. package/dist/services/live.d.ts.map +1 -1
  273. package/dist/services/live.js +1 -1
  274. package/dist/services/live.js.map +1 -1
  275. package/dist/services/machine-id.d.ts.map +1 -1
  276. package/dist/services/machine-id.js.map +1 -1
  277. package/dist/services/reporter.d.ts +8 -5
  278. package/dist/services/reporter.d.ts.map +1 -1
  279. package/dist/services/reporter.js +6 -4
  280. package/dist/services/reporter.js.map +1 -1
  281. package/dist/services/tenant-state.d.ts.map +1 -1
  282. package/dist/services/tenant-state.js.map +1 -1
  283. package/dist/state-store.d.ts +4 -0
  284. package/dist/state-store.d.ts.map +1 -1
  285. package/dist/state-store.js +15 -0
  286. package/dist/state-store.js.map +1 -1
  287. package/dist/status-drilldown.d.ts +24 -0
  288. package/dist/status-drilldown.d.ts.map +1 -0
  289. package/dist/status-drilldown.js +50 -0
  290. package/dist/status-drilldown.js.map +1 -0
  291. package/dist/tenant-state.d.ts.map +1 -1
  292. package/dist/tenant-state.js +3 -2
  293. package/dist/tenant-state.js.map +1 -1
  294. package/dist/token-key-custody.d.ts +91 -0
  295. package/dist/token-key-custody.d.ts.map +1 -0
  296. package/dist/token-key-custody.js +375 -0
  297. package/dist/token-key-custody.js.map +1 -0
  298. package/package.json +13 -14
@@ -1,14 +1,14 @@
1
- import { join } from 'node:path';
2
1
  import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
3
  import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
4
- import { loadCreateControlPlane } from '../control-plane-loader.js';
5
4
  import { Effect } from 'effect';
6
- import { emitHelp } from '../help.js';
5
+ import { isCustomControlPlane, resolveControlPlaneUrl } from '../config.js';
7
6
  import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
7
+ import { loadCreateControlPlane } from '../control-plane-loader.js';
8
8
  import { executeCliHttp } from '../effect-http.js';
9
- import { isCustomControlPlane, resolveControlPlaneUrl } from '../config.js';
10
- import { createReporter } from '../reporter/index.js';
9
+ import { emitHelp } from '../help.js';
11
10
  import { isInteractive, promptConfirm, promptSelect, promptText } from '../prompt.js';
11
+ import { createReporter } from '../reporter/index.js';
12
12
  import { connectHelpText } from './help-command.js';
13
13
  async function hostedHttp(input) {
14
14
  const headers = { ...(input.headers ?? {}) };
@@ -107,26 +107,25 @@ async function connectWizard({ argv, cwd, json, }) {
107
107
  async function connectHubspotCommand({ argv, cwd, json, }) {
108
108
  const interactive = !json && isInteractive();
109
109
  const authMethod = resolveFlag(argv, '--auth-method') ?? 'pak';
110
- // A HubSpot account binds 1:1 into ONE HS-X account, so the hosted connect
111
- // requires a logged-in HS-X session. Gate the *interactive* flow up front —
112
- // otherwise the user discovers accounts and pastes a PAK only to fail at the
113
- // control-plane call with HSX_E_NOT_LOGGED_IN. Non-interactive/scripted
114
- // callers (and `--no-control-plane` / `--local-control-plane`, which exercise
115
- // the offline/local contract) are left to the server / fail fast at the call.
116
- const usesHostedControlPlane = !argv.includes('--no-control-plane') && !argv.includes('--local-control-plane');
117
- if (interactive && usesHostedControlPlane) {
118
- const { isLinked, loadStore } = await import('../account-store.js');
119
- if (!(await isLinked())) {
120
- const reporter = createReporter({ command: 'connect hubspot', argv });
121
- const code = 'HSX_E_NOT_LOGGED_IN';
122
- reporter.error(code, 'Log in to an HS-X account before connecting HubSpot.', {
123
- hint: 'Connecting a HubSpot account binds it 1:1 into one HS-X account. Run `hs-x login` first, then re-run `hs-x connect hubspot`.',
124
- docs_url: `https://hs-x.dev/errors/${code}`,
125
- });
126
- reporter.done(undefined, 10);
127
- return { exitCode: 10 };
128
- }
129
- // Surface which HS-X account this HubSpot account will bind into.
110
+ // ADR-025: the default `connect` is stored-direct (mode 2) capture the
111
+ // credential, store it in local custody, and report success WITHOUT an HS-X
112
+ // account. The linked/hosted platform path (mode 3) is opt-in: taken only
113
+ // when the user is logged in (`isLinked()`), explicitly targets a custom
114
+ // control plane (`--control-plane-url` / `HSX_CONTROL_PLANE_URL`), or
115
+ // exercises the local contract handler (`--local-control-plane`).
116
+ // `--no-control-plane` always forces stored-direct. A HubSpot account still
117
+ // binds 1:1 into an HS-X account, but only on the opt-in linked path.
118
+ const localControlPlane = argv.includes('--local-control-plane');
119
+ const noControlPlane = argv.includes('--no-control-plane');
120
+ const { isLinked, loadStore } = await import('../account-store.js');
121
+ const linked = localControlPlane
122
+ ? true
123
+ : noControlPlane
124
+ ? false
125
+ : isCustomControlPlane(resolveControlPlaneUrl(argv)) || (await isLinked());
126
+ if (interactive && linked && !localControlPlane) {
127
+ // Opt-in linked path: surface which HS-X account this HubSpot account
128
+ // will bind into.
130
129
  const store = await loadStore();
131
130
  const active = store.defaultAccountId ? store.accounts[store.defaultAccountId] : undefined;
132
131
  if (active) {
@@ -317,11 +316,88 @@ async function connectHubspotCommand({ argv, cwd, json, }) {
317
316
  displayName: request.displayName,
318
317
  authMethod: request.authMethod,
319
318
  };
320
- const noControlPlane = argv.includes('--no-control-plane');
321
- const controlPlaneUrl = !noControlPlane && !argv.includes('--local-control-plane')
322
- ? resolveControlPlaneUrl(argv)
323
- : undefined;
324
- if (controlPlaneUrl) {
319
+ // Stored-direct (ADR-025 mode 2) — the DEFAULT. No HS-X account, no control
320
+ // plane: capture the credential into local custody (HS-X store + HubSpot CLI
321
+ // config, where `hs-x deploy` reads it) and report success. Network-free: the
322
+ // PAK is validated at deploy time, not here, so a scripted `--pak` connect
323
+ // works offline.
324
+ if (!linked) {
325
+ await persistConnectedAccount({
326
+ id: accountId,
327
+ displayName: request.displayName,
328
+ hubspotPortalId: Number(developerAccountId),
329
+ });
330
+ const hscliResult = personalAccessKey && Number.isFinite(Number(developerAccountId))
331
+ ? await persistPakToHubSpotCli({
332
+ portalId: Number(developerAccountId),
333
+ pak: personalAccessKey,
334
+ ...(discoveredDefault?.persistInfo?.hubName
335
+ ? { hubName: discoveredDefault.persistInfo.hubName }
336
+ : {}),
337
+ })
338
+ : undefined;
339
+ const result = {
340
+ ok: true,
341
+ command: 'connect hubspot',
342
+ mode: 'stored-direct',
343
+ accountId,
344
+ request: safeRequest,
345
+ };
346
+ if (json) {
347
+ write(`${JSON.stringify(result, null, 2)}\n`);
348
+ }
349
+ else {
350
+ const reporter = createReporter({ command: 'connect hubspot', argv });
351
+ reporter.header(accountId);
352
+ reporter.step('Stored HubSpot developer credential').ok(request.displayName);
353
+ renderCredentialDestinations(reporter, {
354
+ hsxStore: defaultStorePathForLog(),
355
+ ...(hscliResult?.written ? { hscli: hscliResult.path } : {}),
356
+ });
357
+ reporter.info('');
358
+ reporter.info('Stored locally — no HS-X account needed. Run `hs-x deploy` to ship. To link this into an HS-X account (dashboard, teams, hosted billing), run `hs-x login`, then re-run `hs-x connect hubspot`.');
359
+ reporter.done('Connected');
360
+ }
361
+ return { exitCode: 0 };
362
+ }
363
+ // Linked local contract handler (`--local-control-plane`).
364
+ if (localControlPlane) {
365
+ const connection = await requestLocalControlPlaneHubSpotConnect({ accountId, request });
366
+ const result = {
367
+ ok: true,
368
+ command: 'connect hubspot',
369
+ mode: 'local-control-plane-connect',
370
+ accountId,
371
+ request: safeRequest,
372
+ connection,
373
+ };
374
+ await persistConnectedAccount({
375
+ id: accountId,
376
+ displayName: connection.displayName,
377
+ hubspotPortalId: Number(developerAccountId),
378
+ });
379
+ const hscliResult = await maybePersistToHubSpotCli(discoveredDefault, personalAccessKey);
380
+ if (json) {
381
+ write(`${JSON.stringify(result, null, 2)}\n`);
382
+ }
383
+ else {
384
+ const reporter = createReporter({ command: 'connect hubspot', argv });
385
+ reporter.header(connection.accountId);
386
+ reporter
387
+ .step('Connected HubSpot developer account (local control plane)')
388
+ .ok(connection.displayName);
389
+ renderCredentialDestinations(reporter, {
390
+ hsxStore: defaultStorePathForLog(),
391
+ hscli: hscliResult?.written ? hscliResult.path : null,
392
+ controlPlane: 'local (in-memory)',
393
+ });
394
+ reporter.done();
395
+ }
396
+ return { exitCode: 0 };
397
+ }
398
+ // Linked hosted platform path (logged in, or explicit `--control-plane-url`).
399
+ {
400
+ const controlPlaneUrl = resolveControlPlaneUrl(argv);
325
401
  // Validate the PAK with a cheap exchange BEFORE storing it, so a typo'd or
326
402
  // revoked key is rejected here with a pointed error instead of persisting
327
403
  // garbage that fails on every later deploy. The browser flow has already
@@ -394,60 +470,6 @@ async function connectHubspotCommand({ argv, cwd, json, }) {
394
470
  }
395
471
  return { exitCode: 0 };
396
472
  }
397
- if (!argv.includes('--local-control-plane')) {
398
- const result = {
399
- ok: true,
400
- command: 'connect hubspot',
401
- mode: 'request',
402
- accountId,
403
- request: safeRequest,
404
- nextStep: 'Pass --control-plane-url to connect through the control plane or --local-control-plane to exercise the local contract handler.',
405
- };
406
- if (json) {
407
- write(`${JSON.stringify(result, null, 2)}\n`);
408
- }
409
- else {
410
- const reporter = createReporter({ command: 'connect hubspot', argv });
411
- reporter.header(accountId);
412
- reporter.info(`Prepared HubSpot connect request for ${request.displayName}.`);
413
- reporter.warn('HSX_W_CONNECT_NO_TRANSPORT', 'Hosted transport is not wired yet.');
414
- reporter.info('Pass --control-plane-url or --local-control-plane to complete the connect.');
415
- reporter.done('Prepared');
416
- }
417
- return { exitCode: 0 };
418
- }
419
- const connection = await requestLocalControlPlaneHubSpotConnect({ accountId, request });
420
- const result = {
421
- ok: true,
422
- command: 'connect hubspot',
423
- mode: 'local-control-plane-connect',
424
- accountId,
425
- request: safeRequest,
426
- connection,
427
- };
428
- await persistConnectedAccount({
429
- id: accountId,
430
- displayName: connection.displayName,
431
- hubspotPortalId: Number(developerAccountId),
432
- });
433
- const hscliResult = await maybePersistToHubSpotCli(discoveredDefault, personalAccessKey);
434
- if (json) {
435
- write(`${JSON.stringify(result, null, 2)}\n`);
436
- }
437
- else {
438
- const reporter = createReporter({ command: 'connect hubspot', argv });
439
- reporter.header(connection.accountId);
440
- reporter
441
- .step('Connected HubSpot developer account (local control plane)')
442
- .ok(connection.displayName);
443
- renderCredentialDestinations(reporter, {
444
- hsxStore: defaultStorePathForLog(),
445
- hscli: hscliResult?.written ? hscliResult.path : null,
446
- controlPlane: 'local (in-memory)',
447
- });
448
- reporter.done();
449
- }
450
- return { exitCode: 0 };
451
473
  }
452
474
  function renderCredentialDestinations(reporter, destinations) {
453
475
  reporter.info('');
@@ -489,6 +511,25 @@ async function maybePersistToHubSpotCli(discoveredDefault, pak) {
489
511
  return undefined;
490
512
  }
491
513
  }
514
+ /**
515
+ * Stored-direct persistence: write a raw PAK to the HubSpot CLI config (where
516
+ * `hs-x deploy` discovers it) without an OAuth exchange. Best-effort — a write
517
+ * failure never fails the connect; deploy also accepts `--pak` / `HSX_HUBSPOT_PAK`.
518
+ */
519
+ async function persistPakToHubSpotCli(input) {
520
+ try {
521
+ const { persistToHubSpotCliConfig } = await import('../hubspot-auth.js');
522
+ const result = await persistToHubSpotCliConfig({
523
+ portalId: input.portalId,
524
+ pak: input.pak,
525
+ ...(input.hubName ? { hubName: input.hubName } : {}),
526
+ });
527
+ return result.written ? result : undefined;
528
+ }
529
+ catch {
530
+ return undefined;
531
+ }
532
+ }
492
533
  async function resolveAccountIdOrPrompt(input) {
493
534
  const fromFlag = resolveFlag(input.argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
494
535
  if (fromFlag && fromFlag.length > 0)
@@ -624,7 +665,7 @@ async function runCloudflareOAuthFlow(input) {
624
665
  return Effect.runPromise(Effect.promise(() => runCloudflareOAuthFlowBody(input)).pipe(Effect.withSpan('cli.cloudflare_oauth.flow', {
625
666
  attributes: {
626
667
  account_id: input.hsxAccountId,
627
- brokered: Boolean(resolveControlPlaneUrl(input.argv)),
668
+ brokered: input.brokered,
628
669
  },
629
670
  })));
630
671
  }
@@ -677,7 +718,7 @@ async function runCloudflareOAuthFlowBody(input) {
677
718
  // browser on this machine completes the redirect.
678
719
  reporter.info('No TTY detected — not launching a browser.');
679
720
  reporter.info(`Authorize HS-X by opening: ${authorizeUrl}`);
680
- reporter.info(`Waiting up to ${Math.round(timeoutMs / 1000)}s for the OAuth callback. For headless runs use --auth-method api-token with HSX_CLOUDFLARE_API_TOKEN instead.`);
721
+ reporter.info(`Waiting up to ${Math.round(timeoutMs / 1000)}s for the OAuth callback. For headless runs use --auth-method api-token with CLOUDFLARE_API_TOKEN (alias: HSX_CLOUDFLARE_API_TOKEN) instead.`);
681
722
  }
682
723
  let code;
683
724
  try {
@@ -703,12 +744,15 @@ async function runCloudflareOAuthFlowBody(input) {
703
744
  throw error;
704
745
  }
705
746
  await listener.close();
706
- // Brokered path: control plane has a client id AND we didn't take the env
707
- // override. The control plane will exchange the code, store the refresh
708
- // token, and return the CloudflareConnection in one round trip.
747
+ // Brokered path (opt-in linked, mode 3): the control plane exchanges the
748
+ // code, stores the refresh token server-side against the HS-X account, and
749
+ // returns the CloudflareConnection in one round trip. Taken only when the
750
+ // caller opted into the linked path AND we didn't take the dev env override.
751
+ // The stored-direct default (`brokered` false) falls through to the local
752
+ // exchange below, which keeps the refresh token in local custody.
709
753
  const envHasOverride = typeof process.env.HSX_CLOUDFLARE_OAUTH_CLIENT_ID === 'string' &&
710
754
  process.env.HSX_CLOUDFLARE_OAUTH_CLIENT_ID.length > 0;
711
- if (controlPlaneUrl && !envHasOverride) {
755
+ if (input.brokered && controlPlaneUrl && !envHasOverride) {
712
756
  try {
713
757
  const connection = await postCloudflareOAuthComplete({
714
758
  controlPlaneUrl,
@@ -768,17 +812,27 @@ async function runCloudflareOAuthFlowBody(input) {
768
812
  }
769
813
  let discoveredAccountId;
770
814
  try {
771
- const { verifyCloudflareToken } = await import('../cloudflare-auth.js');
772
- const info = await verifyCloudflareToken(tokens.accessToken);
773
- discoveredAccountId = info.accountId;
815
+ // Resolve the account id via /accounts, NOT verifyCloudflareToken: OAuth
816
+ // access tokens 400 on the API-token verify endpoint, which left the id
817
+ // undefined and silently skipped persistence below (the credential is keyed
818
+ // by account id). /accounts works with the granted account-read scope.
819
+ const { resolveCloudflareAccountId } = await import('../cloudflare-auth.js');
820
+ discoveredAccountId = await resolveCloudflareAccountId(tokens.accessToken);
774
821
  }
775
822
  catch {
776
- // verification is optional here
823
+ // account discovery is best-effort
777
824
  }
778
- if (tokens.refreshToken && discoveredAccountId) {
825
+ // Persist the credential so a later `hs-x deploy` can reuse this connection
826
+ // without re-authorizing. Cloudflare OAuth returns no refresh token, so the
827
+ // access token IS what we store (with its expiry); deploy uses it directly
828
+ // while valid and prompts a reconnect once it lapses.
829
+ if (discoveredAccountId && (tokens.refreshToken || tokens.accessToken)) {
779
830
  try {
780
831
  await saveCloudflareOAuthCredential(discoveredAccountId, {
781
- refreshToken: tokens.refreshToken,
832
+ ...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
833
+ ...(tokens.accessToken
834
+ ? { accessToken: tokens.accessToken, accessTokenExpiresAt: tokens.expiresAt }
835
+ : {}),
782
836
  ...(tokens.scope ? { scope: tokens.scope } : {}),
783
837
  });
784
838
  }
@@ -894,19 +948,21 @@ function defaultStorePathForLog() {
894
948
  return join(process.env.HOME ?? '~', '.hsx', 'config.json');
895
949
  }
896
950
  async function connectCloudflareCommand({ argv, json, }) {
951
+ // ADR-025: the default is stored-direct (mode 2) — connect Cloudflare and
952
+ // keep the refresh token in local custody, with NO HS-X account. The linked
953
+ // path (mode 3) is opt-in: taken only when logged in, when a custom control
954
+ // plane is targeted, or when exercising the local contract. When there's no
955
+ // HS-X account we still proceed under a local placeholder id (the local
956
+ // OAuth store keys the credential by Cloudflare account id, not this id).
957
+ const localControlPlane = argv.includes('--local-control-plane');
958
+ const { isLinked } = await import('../account-store.js');
959
+ const linked = localControlPlane
960
+ ? true
961
+ : isCustomControlPlane(resolveControlPlaneUrl(argv)) || (await isLinked());
897
962
  const accountId = resolveFlag(argv, '--account-id') ??
898
963
  process.env.HSX_ACCOUNT_ID ??
899
- (await defaultHsxAccountId());
900
- if (!accountId) {
901
- const reporter = createReporter({ command: 'connect cloudflare', argv });
902
- const code = 'HSX_E_INPUT_NO_HSX_ACCOUNT';
903
- reporter.error(code, 'No HS-X account to bind Cloudflare to.', {
904
- hint: 'Run `hs-x connect` first to create an HS-X account.',
905
- docs_url: `https://hs-x.dev/errors/${code}`,
906
- });
907
- reporter.done(undefined, 10);
908
- return { exitCode: 10 };
909
- }
964
+ (await defaultHsxAccountId()) ??
965
+ 'acct_local';
910
966
  const interactive = !json && isInteractive();
911
967
  let cloudflareAccountId = resolveFlag(argv, '--cloudflare-account-id');
912
968
  let displayName = resolveFlag(argv, '--display-name');
@@ -925,6 +981,13 @@ async function connectCloudflareCommand({ argv, json, }) {
925
981
  reporter.done(undefined, 10);
926
982
  return { exitCode: 10 };
927
983
  }
984
+ // One visual timeline: identity header first, then the OAuth steps, then
985
+ // the railed result — with elapsed measured from here, not from a reporter
986
+ // constructed at summary time (which always read "0ms").
987
+ const connectStartedAt = Date.now();
988
+ if (!json) {
989
+ createReporter({ command: 'connect cloudflare', argv }).header(accountId);
990
+ }
928
991
  // No TTY pre-gate: runCloudflareOAuthFlow degrades on its own — without a
929
992
  // TTY it prints the authorize URL + instructions instead of spawning a
930
993
  // browser, so headless terminals can still complete consent.
@@ -932,6 +995,7 @@ async function connectCloudflareCommand({ argv, json, }) {
932
995
  argv,
933
996
  json,
934
997
  hsxAccountId: accountId,
998
+ brokered: linked,
935
999
  ...(displayName ? { displayName } : {}),
936
1000
  });
937
1001
  if (oauthOutcome.kind === 'cancelled') {
@@ -955,20 +1019,56 @@ async function connectCloudflareCommand({ argv, json, }) {
955
1019
  write(`${JSON.stringify(result, null, 2)}\n`);
956
1020
  }
957
1021
  else {
958
- const reporter = createReporter({ command: 'connect cloudflare', argv });
959
- reporter.header(accountId);
960
- reporter.block(renderCloudflareConnect(oauthOutcome.connection));
1022
+ const reporter = createReporter({
1023
+ command: 'connect cloudflare',
1024
+ argv,
1025
+ startedAt: connectStartedAt,
1026
+ });
1027
+ reporter.rows(cloudflareConnectRows(oauthOutcome.connection));
1028
+ const siblings = oauthOutcome.connection.visibleSiblingAccountIds;
1029
+ if (siblings && siblings.length > 0) {
1030
+ reporter.info(`Also connected to HS-X account(s) ${siblings.join(', ')} — resources are namespaced per HS-X account; Cloudflare quota is shared.`);
1031
+ }
961
1032
  reporter.done('Connected');
962
1033
  }
963
1034
  return { exitCode: 0 };
964
1035
  }
965
- // success-local: sandbox-client dev path (HSX_CLOUDFLARE_OAUTH_CLIENT_ID)
966
- // the CLI holds the OAuth access token and registers it via the connect
967
- // call below, still authMethod oauth.
1036
+ // success-local: the CLI exchanged the code itself and saved the refresh
1037
+ // token in local custody (cloudflare-oauth-store). Used for the stored-direct
1038
+ // default and the sandbox-client dev path (HSX_CLOUDFLARE_OAUTH_CLIENT_ID).
968
1039
  const apiToken = oauthOutcome.accessToken;
969
1040
  if (!cloudflareAccountId && oauthOutcome.cloudflareAccountId) {
970
1041
  cloudflareAccountId = oauthOutcome.cloudflareAccountId;
971
1042
  }
1043
+ // Stored-direct (ADR-025 mode 2): the credential is already persisted
1044
+ // locally; there's no HS-X account to register it against, so report success
1045
+ // and stop. The opt-in linked path continues to the CP connect call below.
1046
+ if (!linked) {
1047
+ const result = {
1048
+ ok: true,
1049
+ command: 'connect cloudflare',
1050
+ mode: 'stored-direct',
1051
+ accountId,
1052
+ ...(cloudflareAccountId ? { cloudflareAccountId } : {}),
1053
+ };
1054
+ if (json) {
1055
+ write(`${JSON.stringify(result, null, 2)}\n`);
1056
+ }
1057
+ else {
1058
+ const reporter = createReporter({
1059
+ command: 'connect cloudflare',
1060
+ argv,
1061
+ startedAt: connectStartedAt,
1062
+ });
1063
+ reporter
1064
+ .step('Stored Cloudflare credential')
1065
+ .ok(cloudflareAccountId ? `account ${cloudflareAccountId}` : 'token stored');
1066
+ reporter.info('');
1067
+ reporter.info('Stored locally — no HS-X account needed. Run `hs-x deploy` to ship. To link this into an HS-X account, run `hs-x login`, then re-run `hs-x connect cloudflare`.');
1068
+ reporter.done('Connected');
1069
+ }
1070
+ return { exitCode: 0 };
1071
+ }
972
1072
  if (interactive && !cloudflareAccountId) {
973
1073
  const answer = await promptText({
974
1074
  message: 'Cloudflare account id',
@@ -1038,9 +1138,16 @@ async function connectCloudflareCommand({ argv, json, }) {
1038
1138
  write(`${JSON.stringify(result, null, 2)}\n`);
1039
1139
  }
1040
1140
  else {
1041
- const reporter = createReporter({ command: 'connect cloudflare', argv });
1042
- reporter.header(accountId);
1043
- reporter.block(renderCloudflareConnect(connection));
1141
+ const reporter = createReporter({
1142
+ command: 'connect cloudflare',
1143
+ argv,
1144
+ startedAt: connectStartedAt,
1145
+ });
1146
+ reporter.rows(cloudflareConnectRows(connection));
1147
+ const siblings = connection.visibleSiblingAccountIds;
1148
+ if (siblings && siblings.length > 0) {
1149
+ reporter.info(`Also connected to HS-X account(s) ${siblings.join(', ')} — resources are namespaced per HS-X account; Cloudflare quota is shared.`);
1150
+ }
1044
1151
  reporter.done('Connected');
1045
1152
  }
1046
1153
  return { exitCode: 0 };
@@ -1133,11 +1240,18 @@ async function requestHostedControlPlaneCloudflareConnect({ accountId, request,
1133
1240
  }
1134
1241
  return Schema.decodeUnknownSync(schemas.CloudflareConnection)(body);
1135
1242
  }
1136
- function renderCloudflareConnect(connection) {
1137
- const siblingLine = connection.visibleSiblingAccountIds && connection.visibleSiblingAccountIds.length > 0
1138
- ? `\nThis Cloudflare account is also connected to HS-X account(s) ${connection.visibleSiblingAccountIds.join(', ')}. Resources will be namespaced by HS-X account id; Cloudflare quota is shared.\n`
1139
- : '\n';
1140
- return `Connected Cloudflare account ${connection.displayName} for ${connection.accountId}${siblingLine}`;
1243
+ function cloudflareConnectRows(connection) {
1244
+ return [
1245
+ {
1246
+ status: 'ok',
1247
+ key: 'cloudflare',
1248
+ value: connection.displayName,
1249
+ ...(connection.cloudflareAccountId
1250
+ ? { detail: `account ${connection.cloudflareAccountId}` }
1251
+ : {}),
1252
+ },
1253
+ { key: 'hs-x account', value: connection.accountId },
1254
+ ];
1141
1255
  }
1142
1256
  function writeDiagnostics(diagnostics) {
1143
1257
  for (const diagnostic of diagnostics) {
@@ -1230,7 +1344,7 @@ const connectCloudflareCmd = CommandNative.make('cloudflare', {
1230
1344
  argv.push('--local-control-plane');
1231
1345
  const defaultCwd = yield* Cwd;
1232
1346
  const cwd = Option.getOrElse(opts.cwd, () => defaultCwd);
1233
- yield* runQuarantined(opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1347
+ yield* runQuarantined('connect', opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1234
1348
  })));
1235
1349
  // connect hubspot
1236
1350
  const connectDeveloperAccountIdOption = OptionsNative.text('developer-account-id').pipe(OptionsNative.optional);
@@ -1285,7 +1399,7 @@ const connectHubspotCmd = CommandNative.make('hubspot', {
1285
1399
  argv.push('--yes');
1286
1400
  const defaultCwd = yield* Cwd;
1287
1401
  const cwd = Option.getOrElse(opts.cwd, () => defaultCwd);
1288
- yield* runQuarantined(opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1402
+ yield* runQuarantined('connect', opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1289
1403
  })));
1290
1404
  // Bare `hs-x connect` runs the interactive wizard (HubSpot, then optionally
1291
1405
  // Cloudflare); non-TTY/--json invocations fall back to the curated help text
@@ -1350,7 +1464,7 @@ export const connectCmd = CommandNative.make('connect', {
1350
1464
  argv.push('--yes');
1351
1465
  const defaultCwd = yield* Cwd;
1352
1466
  const cwd = Option.getOrElse(opts.cwd, () => defaultCwd);
1353
- yield* runQuarantined(opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1467
+ yield* runQuarantined('connect', opts.json, () => connectCommand({ argv, cwd, json: opts.json }));
1354
1468
  }))).pipe(CommandNative.withSubcommands([connectCloudflareCmd, connectHubspotCmd]));
1355
1469
  // Appended imports (ES module imports are hoisted; placement at file end keeps
1356
1470
  // this additive change off the existing import block to avoid merge conflicts).
@@ -1358,6 +1472,6 @@ import * as CommandNative from '@effect/cli/Command';
1358
1472
  import * as OptionsNative from '@effect/cli/Options';
1359
1473
  import { Option } from 'effect';
1360
1474
  import { exitWith } from '../cli-error.js';
1361
- import { jsonOption, cwdOption, accountIdOption, controlPlaneUrlOption, userIdOption, runHandler, runQuarantined, } from '../cli/kit.js';
1475
+ import { accountIdOption, controlPlaneUrlOption, cwdOption, jsonOption, runHandler, runQuarantined, userIdOption, } from '../cli/kit.js';
1362
1476
  import { Cwd } from '../services/cwd.js';
1363
1477
  //# sourceMappingURL=connect.js.map