@hs-x/cli 0.1.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 (302) hide show
  1. package/README.md +1001 -0
  2. package/dist/account-store.d.ts +51 -0
  3. package/dist/account-store.d.ts.map +1 -0
  4. package/dist/account-store.js +138 -0
  5. package/dist/account-store.js.map +1 -0
  6. package/dist/bin/hs-x.d.ts +3 -0
  7. package/dist/bin/hs-x.d.ts.map +1 -0
  8. package/dist/bin/hs-x.js +47 -0
  9. package/dist/bin/hs-x.js.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +595 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli-error.d.ts +36 -0
  15. package/dist/cli-error.d.ts.map +1 -0
  16. package/dist/cli-error.js +40 -0
  17. package/dist/cli-error.js.map +1 -0
  18. package/dist/cloudflare-auth.d.ts +25 -0
  19. package/dist/cloudflare-auth.d.ts.map +1 -0
  20. package/dist/cloudflare-auth.js +251 -0
  21. package/dist/cloudflare-auth.js.map +1 -0
  22. package/dist/cloudflare-kv.d.ts +23 -0
  23. package/dist/cloudflare-kv.d.ts.map +1 -0
  24. package/dist/cloudflare-kv.js +101 -0
  25. package/dist/cloudflare-kv.js.map +1 -0
  26. package/dist/cloudflare-oauth-store.d.ts +16 -0
  27. package/dist/cloudflare-oauth-store.d.ts.map +1 -0
  28. package/dist/cloudflare-oauth-store.js +80 -0
  29. package/dist/cloudflare-oauth-store.js.map +1 -0
  30. package/dist/cloudflare-oauth.d.ts +82 -0
  31. package/dist/cloudflare-oauth.d.ts.map +1 -0
  32. package/dist/cloudflare-oauth.js +336 -0
  33. package/dist/cloudflare-oauth.js.map +1 -0
  34. package/dist/cloudflare-pointer.d.ts +13 -0
  35. package/dist/cloudflare-pointer.d.ts.map +1 -0
  36. package/dist/cloudflare-pointer.js +46 -0
  37. package/dist/cloudflare-pointer.js.map +1 -0
  38. package/dist/command-history.d.ts +7 -0
  39. package/dist/command-history.d.ts.map +1 -0
  40. package/dist/command-history.js +34 -0
  41. package/dist/command-history.js.map +1 -0
  42. package/dist/commands/account.d.ts +7 -0
  43. package/dist/commands/account.d.ts.map +1 -0
  44. package/dist/commands/account.js +315 -0
  45. package/dist/commands/account.js.map +1 -0
  46. package/dist/commands/api.d.ts +36 -0
  47. package/dist/commands/api.d.ts.map +1 -0
  48. package/dist/commands/api.js +521 -0
  49. package/dist/commands/api.js.map +1 -0
  50. package/dist/commands/completion.d.ts +7 -0
  51. package/dist/commands/completion.d.ts.map +1 -0
  52. package/dist/commands/completion.js +121 -0
  53. package/dist/commands/completion.js.map +1 -0
  54. package/dist/commands/connect.d.ts +7 -0
  55. package/dist/commands/connect.d.ts.map +1 -0
  56. package/dist/commands/connect.js +1123 -0
  57. package/dist/commands/connect.js.map +1 -0
  58. package/dist/commands/control-plane-read.d.ts +22 -0
  59. package/dist/commands/control-plane-read.d.ts.map +1 -0
  60. package/dist/commands/control-plane-read.js +350 -0
  61. package/dist/commands/control-plane-read.js.map +1 -0
  62. package/dist/commands/deploy-promote.d.ts +14 -0
  63. package/dist/commands/deploy-promote.d.ts.map +1 -0
  64. package/dist/commands/deploy-promote.js +105 -0
  65. package/dist/commands/deploy-promote.js.map +1 -0
  66. package/dist/commands/deploy.d.ts +18 -0
  67. package/dist/commands/deploy.d.ts.map +1 -0
  68. package/dist/commands/deploy.js +2764 -0
  69. package/dist/commands/deploy.js.map +1 -0
  70. package/dist/commands/dev.d.ts +7 -0
  71. package/dist/commands/dev.d.ts.map +1 -0
  72. package/dist/commands/dev.js +913 -0
  73. package/dist/commands/dev.js.map +1 -0
  74. package/dist/commands/doctor.d.ts +8 -0
  75. package/dist/commands/doctor.d.ts.map +1 -0
  76. package/dist/commands/doctor.js +258 -0
  77. package/dist/commands/doctor.js.map +1 -0
  78. package/dist/commands/flags.d.ts +22 -0
  79. package/dist/commands/flags.d.ts.map +1 -0
  80. package/dist/commands/flags.js +185 -0
  81. package/dist/commands/flags.js.map +1 -0
  82. package/dist/commands/help-command.d.ts +13 -0
  83. package/dist/commands/help-command.d.ts.map +1 -0
  84. package/dist/commands/help-command.js +482 -0
  85. package/dist/commands/help-command.js.map +1 -0
  86. package/dist/commands/history.d.ts +6 -0
  87. package/dist/commands/history.d.ts.map +1 -0
  88. package/dist/commands/history.js +42 -0
  89. package/dist/commands/history.js.map +1 -0
  90. package/dist/commands/init.d.ts +8 -0
  91. package/dist/commands/init.d.ts.map +1 -0
  92. package/dist/commands/init.js +233 -0
  93. package/dist/commands/init.js.map +1 -0
  94. package/dist/commands/link.d.ts +26 -0
  95. package/dist/commands/link.d.ts.map +1 -0
  96. package/dist/commands/link.js +441 -0
  97. package/dist/commands/link.js.map +1 -0
  98. package/dist/commands/login.d.ts +8 -0
  99. package/dist/commands/login.d.ts.map +1 -0
  100. package/dist/commands/login.js +381 -0
  101. package/dist/commands/login.js.map +1 -0
  102. package/dist/commands/migrate.d.ts +8 -0
  103. package/dist/commands/migrate.d.ts.map +1 -0
  104. package/dist/commands/migrate.js +258 -0
  105. package/dist/commands/migrate.js.map +1 -0
  106. package/dist/commands/rollback.d.ts +21 -0
  107. package/dist/commands/rollback.d.ts.map +1 -0
  108. package/dist/commands/rollback.js +301 -0
  109. package/dist/commands/rollback.js.map +1 -0
  110. package/dist/commands/secrets.d.ts +7 -0
  111. package/dist/commands/secrets.d.ts.map +1 -0
  112. package/dist/commands/secrets.js +230 -0
  113. package/dist/commands/secrets.js.map +1 -0
  114. package/dist/commands/status.d.ts +7 -0
  115. package/dist/commands/status.d.ts.map +1 -0
  116. package/dist/commands/status.js +241 -0
  117. package/dist/commands/status.js.map +1 -0
  118. package/dist/commands/unlink.d.ts +21 -0
  119. package/dist/commands/unlink.d.ts.map +1 -0
  120. package/dist/commands/unlink.js +83 -0
  121. package/dist/commands/unlink.js.map +1 -0
  122. package/dist/commands/update.d.ts +11 -0
  123. package/dist/commands/update.d.ts.map +1 -0
  124. package/dist/commands/update.js +154 -0
  125. package/dist/commands/update.js.map +1 -0
  126. package/dist/commands/validate.d.ts +9 -0
  127. package/dist/commands/validate.d.ts.map +1 -0
  128. package/dist/commands/validate.js +39 -0
  129. package/dist/commands/validate.js.map +1 -0
  130. package/dist/config.d.ts +12 -0
  131. package/dist/config.d.ts.map +1 -0
  132. package/dist/config.js +64 -0
  133. package/dist/config.js.map +1 -0
  134. package/dist/constants.d.ts +4 -0
  135. package/dist/constants.d.ts.map +1 -0
  136. package/dist/constants.js +4 -0
  137. package/dist/constants.js.map +1 -0
  138. package/dist/control-plane-fetch.d.ts +34 -0
  139. package/dist/control-plane-fetch.d.ts.map +1 -0
  140. package/dist/control-plane-fetch.js +73 -0
  141. package/dist/control-plane-fetch.js.map +1 -0
  142. package/dist/control-plane-loader.d.ts +16 -0
  143. package/dist/control-plane-loader.d.ts.map +1 -0
  144. package/dist/control-plane-loader.js +24 -0
  145. package/dist/control-plane-loader.js.map +1 -0
  146. package/dist/dev/compat-shim.d.ts +40 -0
  147. package/dist/dev/compat-shim.d.ts.map +1 -0
  148. package/dist/dev/compat-shim.js +65 -0
  149. package/dist/dev/compat-shim.js.map +1 -0
  150. package/dist/dev/event-bus.d.ts +27 -0
  151. package/dist/dev/event-bus.d.ts.map +1 -0
  152. package/dist/dev/event-bus.js +32 -0
  153. package/dist/dev/event-bus.js.map +1 -0
  154. package/dist/dev/log-server.d.ts +52 -0
  155. package/dist/dev/log-server.d.ts.map +1 -0
  156. package/dist/dev/log-server.js +216 -0
  157. package/dist/dev/log-server.js.map +1 -0
  158. package/dist/dev/session-manager.d.ts +33 -0
  159. package/dist/dev/session-manager.d.ts.map +1 -0
  160. package/dist/dev/session-manager.js +132 -0
  161. package/dist/dev/session-manager.js.map +1 -0
  162. package/dist/dev/stream-renderer.d.ts +22 -0
  163. package/dist/dev/stream-renderer.d.ts.map +1 -0
  164. package/dist/dev/stream-renderer.js +65 -0
  165. package/dist/dev/stream-renderer.js.map +1 -0
  166. package/dist/dev/tunnel.d.ts +40 -0
  167. package/dist/dev/tunnel.d.ts.map +1 -0
  168. package/dist/dev/tunnel.js +139 -0
  169. package/dist/dev/tunnel.js.map +1 -0
  170. package/dist/effect-http.d.ts +10 -0
  171. package/dist/effect-http.d.ts.map +1 -0
  172. package/dist/effect-http.js +38 -0
  173. package/dist/effect-http.js.map +1 -0
  174. package/dist/errors-registry.d.ts +11 -0
  175. package/dist/errors-registry.d.ts.map +1 -0
  176. package/dist/errors-registry.js +554 -0
  177. package/dist/errors-registry.js.map +1 -0
  178. package/dist/errors.d.ts +58 -0
  179. package/dist/errors.d.ts.map +1 -0
  180. package/dist/errors.js +30 -0
  181. package/dist/errors.js.map +1 -0
  182. package/dist/help.d.ts +6 -0
  183. package/dist/help.d.ts.map +1 -0
  184. package/dist/help.js +100 -0
  185. package/dist/help.js.map +1 -0
  186. package/dist/history.d.ts +15 -0
  187. package/dist/history.d.ts.map +1 -0
  188. package/dist/history.js +69 -0
  189. package/dist/history.js.map +1 -0
  190. package/dist/hubspot-auth.d.ts +53 -0
  191. package/dist/hubspot-auth.d.ts.map +1 -0
  192. package/dist/hubspot-auth.js +301 -0
  193. package/dist/hubspot-auth.js.map +1 -0
  194. package/dist/hubspot-developer-client.d.ts +10 -0
  195. package/dist/hubspot-developer-client.d.ts.map +1 -0
  196. package/dist/hubspot-developer-client.js +212 -0
  197. package/dist/hubspot-developer-client.js.map +1 -0
  198. package/dist/index.d.ts +5 -0
  199. package/dist/index.d.ts.map +1 -0
  200. package/dist/index.js +4 -0
  201. package/dist/index.js.map +1 -0
  202. package/dist/init/templates.d.ts +18 -0
  203. package/dist/init/templates.d.ts.map +1 -0
  204. package/dist/init/templates.js +239 -0
  205. package/dist/init/templates.js.map +1 -0
  206. package/dist/load-env.d.ts +16 -0
  207. package/dist/load-env.d.ts.map +1 -0
  208. package/dist/load-env.js +69 -0
  209. package/dist/load-env.js.map +1 -0
  210. package/dist/machine-id.d.ts +3 -0
  211. package/dist/machine-id.d.ts.map +1 -0
  212. package/dist/machine-id.js +41 -0
  213. package/dist/machine-id.js.map +1 -0
  214. package/dist/paths.d.ts +4 -0
  215. package/dist/paths.d.ts.map +1 -0
  216. package/dist/paths.js +19 -0
  217. package/dist/paths.js.map +1 -0
  218. package/dist/prompt.d.ts +43 -0
  219. package/dist/prompt.d.ts.map +1 -0
  220. package/dist/prompt.js +379 -0
  221. package/dist/prompt.js.map +1 -0
  222. package/dist/reporter/human.d.ts +28 -0
  223. package/dist/reporter/human.d.ts.map +1 -0
  224. package/dist/reporter/human.js +126 -0
  225. package/dist/reporter/human.js.map +1 -0
  226. package/dist/reporter/index.d.ts +14 -0
  227. package/dist/reporter/index.d.ts.map +1 -0
  228. package/dist/reporter/index.js +37 -0
  229. package/dist/reporter/index.js.map +1 -0
  230. package/dist/reporter/json.d.ts +43 -0
  231. package/dist/reporter/json.d.ts.map +1 -0
  232. package/dist/reporter/json.js +146 -0
  233. package/dist/reporter/json.js.map +1 -0
  234. package/dist/reporter/style.d.ts +34 -0
  235. package/dist/reporter/style.d.ts.map +1 -0
  236. package/dist/reporter/style.js +97 -0
  237. package/dist/reporter/style.js.map +1 -0
  238. package/dist/reporter/types.d.ts +41 -0
  239. package/dist/reporter/types.d.ts.map +1 -0
  240. package/dist/reporter/types.js +2 -0
  241. package/dist/reporter/types.js.map +1 -0
  242. package/dist/result.d.ts +4 -0
  243. package/dist/result.d.ts.map +1 -0
  244. package/dist/result.js +2 -0
  245. package/dist/result.js.map +1 -0
  246. package/dist/services/account-store.d.ts +31 -0
  247. package/dist/services/account-store.d.ts.map +1 -0
  248. package/dist/services/account-store.js +135 -0
  249. package/dist/services/account-store.js.map +1 -0
  250. package/dist/services/app-paths.d.ts +25 -0
  251. package/dist/services/app-paths.d.ts.map +1 -0
  252. package/dist/services/app-paths.js +34 -0
  253. package/dist/services/app-paths.js.map +1 -0
  254. package/dist/services/cloudflare-auth.d.ts +83 -0
  255. package/dist/services/cloudflare-auth.d.ts.map +1 -0
  256. package/dist/services/cloudflare-auth.js +30 -0
  257. package/dist/services/cloudflare-auth.js.map +1 -0
  258. package/dist/services/cloudflare-kv.d.ts +45 -0
  259. package/dist/services/cloudflare-kv.d.ts.map +1 -0
  260. package/dist/services/cloudflare-kv.js +151 -0
  261. package/dist/services/cloudflare-kv.js.map +1 -0
  262. package/dist/services/command-history.d.ts +29 -0
  263. package/dist/services/command-history.d.ts.map +1 -0
  264. package/dist/services/command-history.js +62 -0
  265. package/dist/services/command-history.js.map +1 -0
  266. package/dist/services/control-plane.d.ts +32 -0
  267. package/dist/services/control-plane.d.ts.map +1 -0
  268. package/dist/services/control-plane.js +57 -0
  269. package/dist/services/control-plane.js.map +1 -0
  270. package/dist/services/env-loader.d.ts +18 -0
  271. package/dist/services/env-loader.d.ts.map +1 -0
  272. package/dist/services/env-loader.js +34 -0
  273. package/dist/services/env-loader.js.map +1 -0
  274. package/dist/services/http.d.ts +19 -0
  275. package/dist/services/http.d.ts.map +1 -0
  276. package/dist/services/http.js +9 -0
  277. package/dist/services/http.js.map +1 -0
  278. package/dist/services/live.d.ts +16 -0
  279. package/dist/services/live.d.ts.map +1 -0
  280. package/dist/services/live.js +26 -0
  281. package/dist/services/live.js.map +1 -0
  282. package/dist/services/machine-id.d.ts +18 -0
  283. package/dist/services/machine-id.d.ts.map +1 -0
  284. package/dist/services/machine-id.js +39 -0
  285. package/dist/services/machine-id.js.map +1 -0
  286. package/dist/services/reporter.d.ts +55 -0
  287. package/dist/services/reporter.d.ts.map +1 -0
  288. package/dist/services/reporter.js +49 -0
  289. package/dist/services/reporter.js.map +1 -0
  290. package/dist/state-store.d.ts +39 -0
  291. package/dist/state-store.d.ts.map +1 -0
  292. package/dist/state-store.js +89 -0
  293. package/dist/state-store.js.map +1 -0
  294. package/dist/telemetry.d.ts +13 -0
  295. package/dist/telemetry.d.ts.map +1 -0
  296. package/dist/telemetry.js +129 -0
  297. package/dist/telemetry.js.map +1 -0
  298. package/dist/tenant-state.d.ts +69 -0
  299. package/dist/tenant-state.d.ts.map +1 -0
  300. package/dist/tenant-state.js +161 -0
  301. package/dist/tenant-state.js.map +1 -0
  302. package/package.json +38 -0
@@ -0,0 +1,2764 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { cp, mkdir, mkdtemp, readFile, readdir, stat, writeFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import { tmpdir } from 'node:os';
6
+ import { basename, dirname, join, relative, resolve } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { cloudflareResourceName, generateHubSpotRuntimeProject, generateProjectArtifacts, planPortalSchemaManagement, renderAlchemyProgram, renderCloudflareWorkerEntrypoint, } from '@hs-x/codegen';
9
+ import { loadCreateControlPlane } from '../control-plane-loader.js';
10
+ import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
11
+ import { validateProject } from '@hs-x/validator';
12
+ import { Effect } from 'effect';
13
+ import { isLinked } from '../account-store.js';
14
+ import { resolveCloudflareCredentials } from '../cloudflare-kv.js';
15
+ import { DEFAULT_CONTROL_PLANE_URL, formatConfigUrl, loadCliConfig, resolveControlPlaneUrl, } from '../config.js';
16
+ import { CLI_VERSION } from '../constants.js';
17
+ import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
18
+ import { executeCliHttp } from '../effect-http.js';
19
+ import { createDeployHubSpotDeveloperClient } from '../hubspot-developer-client.js';
20
+ import { hydrateAncestorEnv } from '../load-env.js';
21
+ import { getMachineId } from '../machine-id.js';
22
+ import { cachePath } from '../paths.js';
23
+ import { isInteractive, promptConfirm, promptMultiSelect, promptSelect, promptText, } from '../prompt.js';
24
+ import { createReporter } from '../reporter/index.js';
25
+ import { ensureTenantStateStore, preflightTenantStateCredentials, } from '../tenant-state.js';
26
+ import { deployPromoteCommand, requestControlPlaneDeployPromotion } from './deploy-promote.js';
27
+ async function hostedHttp(input) {
28
+ const headers = { ...(input.headers ?? {}) };
29
+ let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
30
+ if (input.body !== undefined) {
31
+ headers['content-type'] = headers['content-type'] ?? 'application/json';
32
+ request = request.pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(typeof input.body === 'string' ? input.body : JSON.stringify(input.body), headers['content-type']));
33
+ }
34
+ return executeCliHttp(request);
35
+ }
36
+ async function resolveAccountIdOrPrompt(input) {
37
+ const fromFlag = resolveFlag(input.argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
38
+ if (fromFlag && fromFlag.length > 0)
39
+ return { accountId: fromFlag, fromPrompt: false };
40
+ const { loadStore } = await import('../account-store.js');
41
+ const store = await loadStore();
42
+ const ids = Object.keys(store.accounts);
43
+ if (ids.length === 0)
44
+ return { fromPrompt: false };
45
+ const defaultId = store.defaultAccountId;
46
+ // Non-interactive, --json, or --yes: accept the configured default rather than
47
+ // silently degrading to "no account picked" — that path led deploy to exit 0
48
+ // after building artifacts only, looking like success.
49
+ const yes = input.argv.includes('--yes') || input.argv.includes('-y');
50
+ if (input.json || !isInteractive() || yes) {
51
+ if (defaultId && ids.includes(defaultId)) {
52
+ return { accountId: defaultId, fromPrompt: false };
53
+ }
54
+ return { fromPrompt: false };
55
+ }
56
+ const picked = await promptSelect({
57
+ message: `Which HS-X account for ${input.purpose}?`,
58
+ ...(defaultId && ids.includes(defaultId) ? { default: defaultId } : {}),
59
+ options: ids.map((id) => {
60
+ const a = store.accounts[id];
61
+ return {
62
+ value: id,
63
+ label: id,
64
+ description: a
65
+ ? `portal ${a.hubspotPortalId} — ${a.displayName}${id === defaultId ? ' (default)' : ''}`
66
+ : '',
67
+ };
68
+ }),
69
+ });
70
+ return picked ? { accountId: picked, fromPrompt: true } : { fromPrompt: false };
71
+ }
72
+ function missingAccountIdError(input) {
73
+ const code = 'HSX_E_INPUT_MISSING_ACCOUNT_ID';
74
+ const message = 'Missing --account-id.';
75
+ const hint = isInteractive()
76
+ ? 'Run `hs-x accounts list` to see options, then pass --account-id or set HSX_ACCOUNT_ID.'
77
+ : 'Pass --account-id or set HSX_ACCOUNT_ID. (In a TTY you would be prompted.)';
78
+ if (input.json) {
79
+ write(`${JSON.stringify({
80
+ schema_version: 1,
81
+ command: input.command,
82
+ ok: false,
83
+ error: { code, message, hint },
84
+ }, null, 2)}\n`);
85
+ }
86
+ else {
87
+ const reporter = createReporter({ command: input.command, argv: input.argv });
88
+ reporter.error(code, message, { hint, docs_url: `https://hs-x.dev/errors/${code}` });
89
+ reporter.done(undefined, 10);
90
+ }
91
+ return { exitCode: 10 };
92
+ }
93
+ async function promptMissingText(message, current, validate) {
94
+ if (current && current.length > 0)
95
+ return current;
96
+ const answer = await promptText({
97
+ message,
98
+ ...(validate ? { validate } : {}),
99
+ });
100
+ return answer === undefined ? undefined : answer;
101
+ }
102
+ /**
103
+ * Unlinked OAuth installs need the HubSpot app's client id + client secret so
104
+ * the deployed Worker's `/oauth-start` can complete the token exchange. Linked
105
+ * deploys read these from the control plane; unlinked supplies them via
106
+ * flag/env. When interactive, PROMPT for anything missing (only on the first
107
+ * worker, to avoid contending for stdin across the concurrent worker deploys)
108
+ * and persist answers to the environment so sibling workers reuse them — rather
109
+ * than the human having to know to pre-set the env vars, then silently shipping
110
+ * a Worker that 500s on /oauth-start (the run-006 papercut).
111
+ *
112
+ * We deliberately do NOT hard-fail on missing creds: the client secret is
113
+ * grabbed from the app's Auth page, which only exists once the first deploy has
114
+ * uploaded the app, so a bootstrap deploy legitimately has no secret yet. The
115
+ * post-deploy output points the user at that Auth URL to finish and re-deploy.
116
+ */
117
+ async function resolveUnlinkedOAuthCredentials(input) {
118
+ // Only an interactive, unlinked OAuth, non-dry-run deploy prompts: linked
119
+ // reads the secret from the control plane, non-OAuth apps need none, a dry run
120
+ // never deploys, and non-interactive runs must supply creds via flag/env.
121
+ if (input.auth !== 'oauth' || input.linked || input.dryRun || !input.allowPrompt) {
122
+ return { clientId: input.hubSpotClientId, clientSecret: input.hubSpotClientSecret };
123
+ }
124
+ const clientId = await promptMissingText('HubSpot app client id (HSX_HUBSPOT_CLIENT_ID)', input.hubSpotClientId);
125
+ const clientSecret = await promptMissingText('HubSpot app client secret — copy it from the app’s Auth tab (it is not exposed via API)', input.hubSpotClientSecret);
126
+ if (clientId)
127
+ process.env.HSX_HUBSPOT_CLIENT_ID = clientId;
128
+ if (clientSecret)
129
+ process.env.HSX_HUBSPOT_CLIENT_SECRET = clientSecret;
130
+ return { clientId, clientSecret };
131
+ }
132
+ function missingProjectIdError(input) {
133
+ const code = 'HSX_E_INPUT_MISSING_PROJECT_ID';
134
+ const message = 'Missing --project-id.';
135
+ const hint = 'Pass --project-id, set HSX_PROJECT_ID, or add `name: "..."` to hsx.config.ts so we can derive it.';
136
+ if (input.json) {
137
+ write(`${JSON.stringify({
138
+ schema_version: 1,
139
+ command: input.command,
140
+ ok: false,
141
+ error: { code, message, hint },
142
+ }, null, 2)}\n`);
143
+ }
144
+ else {
145
+ const reporter = createReporter({ command: input.command, argv: input.argv });
146
+ reporter.error(code, message, { hint, docs_url: `https://hs-x.dev/errors/${code}` });
147
+ reporter.done(undefined, 10);
148
+ }
149
+ return { exitCode: 10 };
150
+ }
151
+ function inputValidationError(input) {
152
+ if (input.json) {
153
+ write(`${JSON.stringify({
154
+ schema_version: 1,
155
+ command: input.command,
156
+ ok: false,
157
+ error: {
158
+ code: input.code,
159
+ message: input.message,
160
+ hint: input.hint,
161
+ detail: input.detail,
162
+ },
163
+ }, null, 2)}\n`);
164
+ }
165
+ else {
166
+ const reporter = createReporter({ command: input.command, argv: input.argv });
167
+ reporter.error(input.code, input.message, {
168
+ hint: input.hint,
169
+ docs_url: `https://hs-x.dev/errors/${input.code}`,
170
+ });
171
+ reporter.done(undefined, 10);
172
+ }
173
+ return { exitCode: 10 };
174
+ }
175
+ async function readHsxConfigProjectId(root) {
176
+ try {
177
+ const contents = await readFile(join(root, 'hsx.config.ts'), 'utf8');
178
+ const appBody = callObjectBody(contents, 'defineApp') ?? contents;
179
+ const name = stringPropertyValue(appBody, 'name');
180
+ return name ? toProjectId(name) : undefined;
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ }
186
+ async function readHsxConfigHeartbeat(root) {
187
+ try {
188
+ const contents = await readFile(join(root, 'hsx.config.ts'), 'utf8');
189
+ const appBody = callObjectBody(contents, 'defineApp') ?? contents;
190
+ return booleanPropertyValue(appBody, 'heartbeat');
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
195
+ }
196
+ function toProjectId(value) {
197
+ return value
198
+ .trim()
199
+ .toLowerCase()
200
+ .replace(/[^a-z0-9_-]+/g, '-')
201
+ .replace(/^-+|-+$/g, '');
202
+ }
203
+ function cancelledResult(argv, command) {
204
+ const reporter = createReporter({ command, argv });
205
+ reporter.info('Cancelled.');
206
+ return { exitCode: 130 };
207
+ }
208
+ async function waitForHealthyControlPlaneDrift({ controlPlaneUrl, projectId, deployId, userId, timeoutMs, }) {
209
+ const startedAt = Date.now();
210
+ let lastState = 'unknown';
211
+ let lastReason;
212
+ do {
213
+ const response = await hostedHttp({
214
+ url: new URL(`/v1/projects/${encodeURIComponent(projectId)}/drift`, controlPlaneUrl),
215
+ headers: await controlPlaneAuthHeaders(userId),
216
+ });
217
+ const body = await response.json().catch(() => undefined);
218
+ if (response.ok) {
219
+ const drift = Schema.decodeUnknownSync(schemas.DriftRead)(body);
220
+ lastState = drift.state;
221
+ lastReason = drift.reason;
222
+ if (drift.deployId === deployId && drift.state === 'healthy') {
223
+ return drift;
224
+ }
225
+ if (drift.deployId === deployId &&
226
+ (drift.state === 'unknown_code' ||
227
+ drift.state === 'resource_missing' ||
228
+ drift.state === 'billing_untrusted')) {
229
+ throw new Error(`Deploy ${deployId} cannot be promoted; drift state is ${drift.state}${drift.reason ? `: ${drift.reason}` : ''}.`);
230
+ }
231
+ }
232
+ else if (response.status !== 404) {
233
+ throw new Error(isRecord(body) && typeof body.message === 'string'
234
+ ? body.message
235
+ : `Control-plane drift status failed with status ${response.status}`);
236
+ }
237
+ await sleep(1000);
238
+ } while (Date.now() - startedAt < timeoutMs);
239
+ throw new Error(`Timed out waiting for deploy ${deployId} to become healthy; latest drift state was ${lastState}${lastReason ? `: ${lastReason}` : ''}.`);
240
+ }
241
+ function isRecord(value) {
242
+ return typeof value === 'object' && value !== null;
243
+ }
244
+ export async function deployCommand({ argv, root, json, }) {
245
+ if (argv[1] === 'promote') {
246
+ return deployPromoteCommand({ argv, json });
247
+ }
248
+ // bun only auto-loads .env/.env.local from the CWD; when deploying from a
249
+ // nested app dir, pull in a repo-root .env.local (CLOUDFLARE_ACCOUNT_ID, etc.)
250
+ // so the deploy doesn't fail for vars that are set at the root.
251
+ hydrateAncestorEnv(root);
252
+ const planOnly = argv.includes('--plan') || argv.includes('--dry-run');
253
+ const buildOnly = argv.includes('--build-only');
254
+ const hubspotOnly = argv.includes('--hubspot-only');
255
+ const explicitHubspotUpload = argv.includes('--hubspot-upload') || argv.includes('--local-hubspot-upload');
256
+ const localHubspotUpload = argv.includes('--local-hubspot-upload');
257
+ const hubspotUploadOnly = argv.includes('--hubspot-upload-only') || argv.includes('--skip-deploy');
258
+ const cloudflareDeploy = argv.includes('--cloudflare-deploy');
259
+ const cloudflareDryRun = argv.includes('--cloudflare-dry-run');
260
+ const explicitCloudflareDeployRequested = cloudflareDeploy || cloudflareDryRun;
261
+ const noManageSchema = argv.includes('--no-manage-schema');
262
+ const localControlPlane = argv.includes('--local-control-plane');
263
+ const recordLocal = argv.includes('--record-local');
264
+ const promoteWhenHealthy = argv.includes('--promote-when-healthy');
265
+ const applySchema = argv.includes('--apply-schema');
266
+ const heartbeatFlag = argv.includes('--heartbeat');
267
+ const noHeartbeat = argv.includes('--no-heartbeat');
268
+ const shouldRecordControlPlane = recordLocal || promoteWhenHealthy;
269
+ const explicitControlPlaneUrl = resolveFlag(argv, '--control-plane-url');
270
+ const controlPlaneUrl = explicitControlPlaneUrl ?? (planOnly ? undefined : process.env.HSX_CONTROL_PLANE_URL);
271
+ const linked = await isLinked();
272
+ const machineId = await getMachineId();
273
+ const userId = resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user';
274
+ const resolvedAcct = await resolveAccountIdOrPrompt({
275
+ argv,
276
+ json,
277
+ purpose: 'this deploy',
278
+ });
279
+ const accountId = resolvedAcct.accountId;
280
+ const explicitProjectId = resolveFlag(argv, '--project-id') ?? process.env.HSX_PROJECT_ID;
281
+ let projectId = explicitProjectId;
282
+ const validation = await validateProject({ root });
283
+ const workers = await discoverWorkerManifests(root, { noManageSchema });
284
+ const app = await readHsxAppConfig(root);
285
+ const configHeartbeat = await readHsxConfigHeartbeat(root);
286
+ if (!projectId) {
287
+ projectId = await readHsxConfigProjectId(root);
288
+ }
289
+ const needsRuntime = workers.some((worker) => worker.capabilities.some((capability) => capability.runtimeNeeds.some((need) => need !== 'hubspot-only')));
290
+ const cloudflareDeployRequested = !buildOnly &&
291
+ (explicitCloudflareDeployRequested || (!planOnly && needsRuntime && !explicitHubspotUpload));
292
+ const hubspotUploadRequested = !buildOnly && (explicitHubspotUpload || (!planOnly && !cloudflareDryRun && Boolean(projectId)));
293
+ const portalSchemaRead = await readPortalSchema(argv, workers);
294
+ const portalSchemaPlan = portalSchemaRead
295
+ ? planPortalSchemaManagement({ workers, observed: portalSchemaRead.observed })
296
+ : undefined;
297
+ const willChangeAnything = !planOnly &&
298
+ (applySchema ||
299
+ shouldRecordControlPlane ||
300
+ hubspotUploadRequested ||
301
+ cloudflareDeployRequested ||
302
+ Boolean(controlPlaneUrl));
303
+ const flagYes = argv.includes('--yes') || argv.includes('-y');
304
+ if (!projectId && (controlPlaneUrl || shouldRecordControlPlane || promoteWhenHealthy)) {
305
+ if (!projectId && !json && isInteractive()) {
306
+ const answer = await promptText({
307
+ message: 'HS-X project id',
308
+ default: basename(root),
309
+ validate: (value) => (value.length > 0 ? undefined : 'Project id is required.'),
310
+ });
311
+ if (answer === undefined)
312
+ return cancelledResult(argv, 'deploy');
313
+ projectId = answer;
314
+ }
315
+ }
316
+ if (!json && willChangeAnything && !flagYes && isInteractive()) {
317
+ const summary = buildDeployPlanSummary({
318
+ accountId,
319
+ projectId,
320
+ workers,
321
+ portalSchemaPlan,
322
+ applySchema,
323
+ hubspotUpload: hubspotUploadRequested,
324
+ cloudflareDeployRequested,
325
+ cloudflareDryRun,
326
+ controlPlaneUrl,
327
+ shouldRecordControlPlane,
328
+ promoteWhenHealthy,
329
+ });
330
+ const reporter = createReporter({ command: 'deploy', argv });
331
+ reporter.header(basename(root));
332
+ reporter.block(summary);
333
+ const confirm = await promptConfirm({
334
+ message: 'Apply this plan?',
335
+ default: false,
336
+ });
337
+ if (confirm === undefined || confirm === false) {
338
+ return cancelledResult(argv, 'deploy');
339
+ }
340
+ }
341
+ const portalSchemaApply = applySchema
342
+ ? await applyPortalSchemaPlan({
343
+ argv,
344
+ plan: portalSchemaPlan,
345
+ source: portalSchemaRead?.source,
346
+ })
347
+ : undefined;
348
+ const manifest = await generateProjectArtifacts({
349
+ root,
350
+ workers,
351
+ appObjects: app.appObjects,
352
+ appObjectAssociations: app.appObjectAssociations,
353
+ appEvents: app.appEvents,
354
+ });
355
+ const shouldCreateControlPlaneRequest = !hubspotOnly && Boolean(accountId);
356
+ let controlPlaneRequest;
357
+ let resolvedProjectId = projectId;
358
+ if (shouldCreateControlPlaneRequest && !(explicitHubspotUpload && accountId && !projectId)) {
359
+ if (!accountId) {
360
+ return missingAccountIdError({ argv, command: 'deploy', json });
361
+ }
362
+ // projectId is only required when actually building a control-plane request.
363
+ // Fall back to `name` from hsx.config.ts so a user with `account set` +
364
+ // --yes doesn't need to repeat --project-id.
365
+ if (!resolvedProjectId) {
366
+ resolvedProjectId = await readHsxConfigProjectId(root);
367
+ }
368
+ if (!resolvedProjectId) {
369
+ return missingProjectIdError({ argv, command: 'deploy', json });
370
+ }
371
+ try {
372
+ const app = await readHsxAppConfig(root);
373
+ controlPlaneRequest = Schema.decodeUnknownSync(schemas.DeployPlanRequest)({
374
+ accountId,
375
+ projectId: resolvedProjectId,
376
+ manifest,
377
+ ...(app.billing ? { billing: app.billing } : {}),
378
+ });
379
+ }
380
+ catch (error) {
381
+ const detail = error instanceof Error ? error.message : String(error);
382
+ return inputValidationError({
383
+ argv,
384
+ command: 'deploy',
385
+ json,
386
+ code: 'HSX_E_INPUT_INVALID_DEPLOY_PLAN',
387
+ message: 'Deploy plan request failed validation.',
388
+ hint: 'Check --account-id and --project-id, and that `.hs-x/manifest.json` is well-formed.',
389
+ detail,
390
+ });
391
+ }
392
+ }
393
+ const controlPlanePlan = localControlPlane && controlPlaneRequest
394
+ ? await requestLocalControlPlaneDeployPlan(controlPlaneRequest)
395
+ : controlPlaneUrl && controlPlaneRequest
396
+ ? await requestHostedControlPlaneDeployPlan({
397
+ request: controlPlaneRequest,
398
+ controlPlaneUrl,
399
+ userId,
400
+ })
401
+ : !buildOnly && !hubspotOnly && projectId
402
+ ? createUnlinkedDeployPlan({
403
+ projectId,
404
+ machineId,
405
+ manifest,
406
+ workers,
407
+ })
408
+ : undefined;
409
+ const controlPlaneBackedPlan = Boolean(controlPlanePlan && controlPlaneRequest && (localControlPlane || controlPlaneUrl));
410
+ const unlinkedDeployPlan = Boolean(controlPlanePlan && !controlPlaneBackedPlan);
411
+ if (controlPlanePlan) {
412
+ await writeFile(join(root, '.hs-x', 'alchemy.run.ts'), renderAlchemyProgram({
413
+ hsXAccountId: controlPlanePlan.accountId,
414
+ projectId: controlPlanePlan.projectId,
415
+ deployId: controlPlanePlan.deployId,
416
+ workers,
417
+ }));
418
+ }
419
+ const cloudflareDeployResult = cloudflareDeployRequested && !planOnly && validation.ok && controlPlanePlan
420
+ ? await executeCloudflareDeploy({
421
+ argv,
422
+ root,
423
+ workers,
424
+ controlPlanePlan,
425
+ userId,
426
+ machineId,
427
+ heartbeatEnabled: noHeartbeat
428
+ ? false
429
+ : controlPlaneBackedPlan
430
+ ? configHeartbeat !== false
431
+ : heartbeatFlag || configHeartbeat === true,
432
+ ...(controlPlaneBackedPlan && controlPlaneUrl ? { controlPlaneUrl } : {}),
433
+ dryRun: cloudflareDryRun,
434
+ })
435
+ : undefined;
436
+ const localRecordResult = shouldRecordControlPlane && controlPlaneRequest && controlPlanePlan
437
+ ? localControlPlane
438
+ ? await requestLocalControlPlaneDeployRecordAndMaybePromote({
439
+ request: controlPlaneRequest,
440
+ plan: controlPlanePlan,
441
+ promoteWhenHealthy,
442
+ })
443
+ : controlPlaneUrl
444
+ ? await requestHostedControlPlaneDeployRecordAndMaybePromote({
445
+ request: controlPlaneRequest,
446
+ plan: controlPlanePlan,
447
+ controlPlaneUrl,
448
+ userId,
449
+ promoteWhenHealthy,
450
+ promotionTimeoutMs: Number(resolveFlag(argv, '--promotion-timeout-ms') ?? '60000'),
451
+ })
452
+ : undefined
453
+ : undefined;
454
+ const controlPlaneRecord = localRecordResult?.record;
455
+ const controlPlaneDrift = localRecordResult?.drift;
456
+ const controlPlanePromotion = localRecordResult?.promotion;
457
+ if (hubspotUploadRequested && validation.ok) {
458
+ await ensureHubSpotRuntimeProjectArtifacts({
459
+ argv,
460
+ root,
461
+ app,
462
+ workers,
463
+ cloudflareDeployResult,
464
+ needsRuntime,
465
+ ...(controlPlanePlan ? { controlPlanePlan } : {}),
466
+ });
467
+ }
468
+ const hubspotUploadResult = hubspotUploadRequested &&
469
+ !planOnly &&
470
+ validation.ok &&
471
+ !(hubspotOnly && needsRuntime) &&
472
+ (!needsRuntime ||
473
+ hubspotOnly ||
474
+ cloudflareDeployRequested ||
475
+ resolveFlag(argv, '--runtime-origin'))
476
+ ? await executeHubSpotOnlyUpload({
477
+ argv,
478
+ root,
479
+ local: localHubspotUpload,
480
+ uploadOnly: hubspotUploadOnly,
481
+ })
482
+ : undefined;
483
+ // Everything that gates a healthy deploy *except* the HubSpot upload, so we
484
+ // can tell a total failure apart from "the Cloudflare worker is live but the
485
+ // HubSpot upload failed". The CF deploy runs first and stands on its own, so
486
+ // a later upload failure must not erase that the worker is serving traffic.
487
+ const preHubSpotOk = validation.ok &&
488
+ !(hubspotOnly && needsRuntime) &&
489
+ !(portalSchemaPlan && portalSchemaPlan.errors.length > 0) &&
490
+ !((localControlPlane || Boolean(controlPlaneUrl && controlPlaneRequest)) &&
491
+ !controlPlanePlan) &&
492
+ !(!planOnly && cloudflareDeployRequested && !cloudflareDeployResult);
493
+ const hubspotUploadFailed = !planOnly && hubspotUploadRequested && !hubspotUploadResult;
494
+ const cloudflareDeployed = !planOnly && cloudflareDeployRequested && Boolean(cloudflareDeployResult);
495
+ // A live worker with only the HubSpot upload failing: not a clean success,
496
+ // but distinct from total failure so callers don't tear down a good worker.
497
+ const partial = preHubSpotOk && hubspotUploadFailed && cloudflareDeployed;
498
+ const plan = {
499
+ ok: preHubSpotOk && !hubspotUploadFailed,
500
+ partial,
501
+ command: 'deploy',
502
+ linkState: linked ? 'linked' : 'unlinked',
503
+ machineId,
504
+ mode: hubspotUploadResult
505
+ ? localHubspotUpload
506
+ ? 'local-hubspot-upload'
507
+ : 'hubspot-upload'
508
+ : cloudflareDeployResult
509
+ ? cloudflareDryRun
510
+ ? 'cloudflare-dry-run'
511
+ : 'cloudflare-deploy'
512
+ : buildOnly
513
+ ? 'build-only'
514
+ : localControlPlane
515
+ ? 'local-control-plane-plan'
516
+ : controlPlaneBackedPlan && controlPlaneUrl
517
+ ? 'control-plane-plan'
518
+ : planOnly
519
+ ? 'plan'
520
+ : 'local-artifacts',
521
+ root,
522
+ hubspotOnly,
523
+ needsRuntime,
524
+ manifest,
525
+ controlPlaneRequest,
526
+ controlPlanePlan,
527
+ unlinkedDeployPlan,
528
+ controlPlaneRecord,
529
+ controlPlaneDrift,
530
+ controlPlanePromotion,
531
+ cloudflareDeploy: cloudflareDeployResult,
532
+ hubspotUpload: hubspotUploadResult,
533
+ portalSchemaSource: portalSchemaRead?.source,
534
+ portalSchemaPlan,
535
+ portalSchemaApply,
536
+ diagnostics: validation.diagnostics,
537
+ };
538
+ if (json) {
539
+ write(`${JSON.stringify(plan, null, 2)}\n`);
540
+ }
541
+ else {
542
+ renderDeployHuman({
543
+ argv,
544
+ plan,
545
+ root,
546
+ planOnly,
547
+ workers,
548
+ portalSchemaPlan,
549
+ portalSchemaApply,
550
+ needsRuntime,
551
+ hubspotOnly,
552
+ hubspotUpload: hubspotUploadRequested,
553
+ cloudflareDeployRequested,
554
+ cloudflareDryRun,
555
+ validation,
556
+ controlPlaneRequest,
557
+ controlPlanePlan,
558
+ unlinkedDeployPlan,
559
+ controlPlaneRecord,
560
+ controlPlanePromotion,
561
+ cloudflareDeployResult,
562
+ hubspotUploadResult,
563
+ linked,
564
+ machineId,
565
+ });
566
+ }
567
+ // Record tenant state + emit the deploy event whenever the worker is actually
568
+ // live — including the partial case (HubSpot upload failed) — so a successful
569
+ // Cloudflare deploy is never silently dropped.
570
+ if ((plan.ok || plan.partial) && !linked && cloudflareDeployResult && !cloudflareDryRun) {
571
+ await recordTenantDeployState({
572
+ argv,
573
+ root,
574
+ machineId,
575
+ deploy: cloudflareDeployResult,
576
+ plan: controlPlanePlan,
577
+ environment: resolveFlag(argv, '--env') ?? resolveFlag(argv, '--environment') ?? 'production',
578
+ });
579
+ await emitAnonymousDeployEvent({
580
+ argv,
581
+ machineId,
582
+ projectId: controlPlanePlan?.projectId ?? projectId,
583
+ deployId: cloudflareDeployResult.deployId,
584
+ workerUrl: cloudflareDeployResult.workers.find((worker) => worker.url)?.url,
585
+ environment: resolveFlag(argv, '--env') ?? resolveFlag(argv, '--environment') ?? 'production',
586
+ });
587
+ }
588
+ // 0 = clean, 20 = partial (worker live, HubSpot upload failed — distinct so
589
+ // automation doesn't treat a serving worker as a total failure), 1 = failure.
590
+ return { exitCode: plan.ok ? 0 : plan.partial ? 20 : 1 };
591
+ }
592
+ function buildDeployPlanSummary(input) {
593
+ const lines = ['', 'Planned changes:'];
594
+ const capCount = input.workers.reduce((c, w) => c + w.capabilities.length, 0);
595
+ if (input.accountId)
596
+ lines.push(` account: ${input.accountId}`);
597
+ if (input.projectId)
598
+ lines.push(` project: ${input.projectId}`);
599
+ lines.push(` workers: ${input.workers.length} (${capCount} capabilities)`);
600
+ if (input.applySchema && input.portalSchemaPlan) {
601
+ const adds = input.portalSchemaPlan.actions.filter((item) => item.kind === 'will-create-object' || item.kind === 'will-create-property').length;
602
+ const updates = input.portalSchemaPlan.actions.filter((item) => item.kind === 'will-alter-property').length;
603
+ lines.push(` portal schema: apply (+${adds}, ~${updates})`);
604
+ }
605
+ else if (input.portalSchemaPlan && input.portalSchemaPlan.actions.length > 0) {
606
+ lines.push(` portal schema: ${input.portalSchemaPlan.actions.length} pending (preview only)`);
607
+ }
608
+ if (input.controlPlaneUrl) {
609
+ lines.push(` control plane: ${input.controlPlaneUrl}`);
610
+ }
611
+ if (input.shouldRecordControlPlane) {
612
+ lines.push(` record: yes${input.promoteWhenHealthy ? ' + promote-when-healthy' : ''}`);
613
+ }
614
+ if (input.cloudflareDeployRequested) {
615
+ lines.push(` cloudflare: ${input.cloudflareDryRun ? 'dry-run' : 'deploy'}`);
616
+ }
617
+ if (input.hubspotUpload)
618
+ lines.push(' hubspot upload: yes');
619
+ return lines.join('\n');
620
+ }
621
+ function createUnlinkedDeployPlan(input) {
622
+ const deployId = `deploy_${toProjectId(input.projectId)}_${Math.floor(Date.now() / 1000)}_${randomBytes(3).toString('hex').slice(0, 4)}`;
623
+ const ownerId = `local_${input.machineId.slice(2, 10)}`;
624
+ const manifestHash = `sha256:${createHash('sha256')
625
+ .update(JSON.stringify(input.manifest))
626
+ .digest('hex')
627
+ .slice(0, 16)}`;
628
+ return Schema.decodeUnknownSync(schemas.DeployPlanResponse)({
629
+ deployId,
630
+ accountId: ownerId,
631
+ projectId: input.projectId,
632
+ plannedAt: new Date().toISOString(),
633
+ manifestHash,
634
+ compatibility: 'first-deploy',
635
+ resources: {
636
+ hsXAccountId: ownerId,
637
+ workerNames: input.workers.map((worker) => cloudflareResourceName({
638
+ hsXAccountId: Schema.decodeSync(schemas.AccountId)(ownerId),
639
+ projectId: Schema.decodeSync(schemas.ProjectId)(input.projectId),
640
+ deployId: Schema.decodeSync(schemas.DeployId)(deployId),
641
+ }, `${input.projectId}-${worker.name}`)),
642
+ },
643
+ runtimeBindings: {
644
+ oauthCallbackPath: '/oauth-callback',
645
+ hubSpotOAuthClientIdBinding: 'HSX_HUBSPOT_CLIENT_ID',
646
+ hubSpotOAuthClientSecretBinding: 'HSX_HUBSPOT_CLIENT_SECRET',
647
+ },
648
+ leases: [],
649
+ });
650
+ }
651
+ async function emitAnonymousDeployEvent(input) {
652
+ const endpoint = new URL(`/v1/machines/${encodeURIComponent(input.machineId)}/events`, resolveControlPlaneUrl(input.argv));
653
+ const controller = new AbortController();
654
+ const timeout = setTimeout(() => controller.abort(), 350);
655
+ try {
656
+ await fetch(endpoint, {
657
+ method: 'POST',
658
+ signal: controller.signal,
659
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
660
+ body: JSON.stringify({
661
+ schema_version: 1,
662
+ event: 'deploy.succeeded',
663
+ cli_version: CLI_VERSION,
664
+ ...(input.projectId ? { project_id: input.projectId } : {}),
665
+ deploy_id: input.deployId,
666
+ environment: input.environment,
667
+ ...(input.workerUrl ? { worker_url: input.workerUrl } : {}),
668
+ timestamp: new Date().toISOString(),
669
+ }),
670
+ });
671
+ }
672
+ catch {
673
+ // Anonymous events are best-effort and must never affect deploy success.
674
+ }
675
+ finally {
676
+ clearTimeout(timeout);
677
+ }
678
+ }
679
+ async function recordTenantDeployState(input) {
680
+ if (!input.plan)
681
+ return;
682
+ const { pointer, stateStore } = await ensureTenantStateStore({
683
+ argv: input.argv,
684
+ root: input.root,
685
+ });
686
+ const now = new Date().toISOString();
687
+ const existingOwnerMeta = await stateStore.getOwnerMeta();
688
+ await stateStore.putOwnerMeta(Schema.decodeUnknownSync(schemas.LocalOwnerMeta)({
689
+ ownerId: pointer.ownerId,
690
+ createdAt: existingOwnerMeta?.createdAt ?? now,
691
+ updatedAt: now,
692
+ machineId: existingOwnerMeta?.machineId ?? input.machineId,
693
+ }));
694
+ await stateStore.recordDeploy(Schema.decodeUnknownSync(schemas.LocalDeployEntry)({
695
+ ownerId: pointer.ownerId,
696
+ projectId: input.plan.projectId,
697
+ deployId: input.deploy.deployId,
698
+ environment: input.environment,
699
+ machineId: input.machineId,
700
+ manifestHash: input.plan.manifestHash,
701
+ workerNames: input.deploy.workers.map((worker) => worker.workerName),
702
+ workerUrls: input.deploy.workers.flatMap((worker) => (worker.url ? [worker.url] : [])),
703
+ status: 'deployed',
704
+ createdAt: now,
705
+ updatedAt: now,
706
+ }));
707
+ await stateStore.setActive(Schema.decodeUnknownSync(schemas.LocalActiveDeploy)({
708
+ ownerId: pointer.ownerId,
709
+ projectId: input.plan.projectId,
710
+ deployId: input.deploy.deployId,
711
+ environment: input.environment,
712
+ updatedAt: now,
713
+ }));
714
+ }
715
+ function renderDeployHuman(input) {
716
+ const reporter = createReporter({
717
+ command: 'deploy',
718
+ argv: input.argv,
719
+ });
720
+ const subject = basename(input.root);
721
+ reporter.header(subject);
722
+ const capCount = input.workers.reduce((c, w) => c + w.capabilities.length, 0);
723
+ reporter
724
+ .step('Validating project')
725
+ .ok(`${input.workers.length} workers, ${capCount} capabilities`);
726
+ const sources = input.workers.flatMap((worker) => worker.capabilities.flatMap((capability) => capability.kind === 'sync' && capability.source
727
+ ? [
728
+ `${capability.source.name} (${capability.source.kind}${capability.source.kind === 'push'
729
+ ? `, ${capability.source.webhookPath}`
730
+ : `, ${capability.schedule ?? 'manual'}`})`,
731
+ ]
732
+ : []));
733
+ if (sources.length > 0)
734
+ reporter.info(`Sources: ${sources.join(', ')}`);
735
+ const syncSchemaModes = input.workers.flatMap((worker) => worker.capabilities.flatMap((capability) => capability.kind === 'sync' ? [`${capability.id}: ${capability.manageSchema ?? false}`] : []));
736
+ if (syncSchemaModes.length > 0) {
737
+ reporter.info(`Portal schema management: ${syncSchemaModes.join(', ')}`);
738
+ }
739
+ if (input.portalSchemaPlan) {
740
+ reporter.info(renderPortalSchemaPlan(input.portalSchemaPlan).trimEnd());
741
+ }
742
+ if (input.portalSchemaApply) {
743
+ reporter.info(renderPortalSchemaApply(input.portalSchemaApply).trimEnd());
744
+ }
745
+ reporter.info(`Runtime: ${input.needsRuntime ? 'Cloudflare Worker required' : 'HubSpot-only compatible'}`);
746
+ if (input.hubspotOnly && input.needsRuntime) {
747
+ reporter.warn('HSX_W_DEPLOY_HUBSPOT_ONLY_INCOMPATIBLE', 'Cannot use --hubspot-only because at least one capability has a runtime handler.');
748
+ }
749
+ if (input.hubspotUpload && !input.hubspotOnly && !input.cloudflareDeployRequested) {
750
+ reporter.warn('HSX_W_DEPLOY_HUBSPOT_UPLOAD_WITHOUT_RUNTIME_DEPLOY', 'HubSpot upload requested without --cloudflare-deploy; runtime endpoints may still point at an older Worker.');
751
+ }
752
+ if (input.validation.diagnostics.length > 0) {
753
+ for (const d of input.validation.diagnostics)
754
+ reporter.info(formatDiagnostic(d));
755
+ }
756
+ if (input.controlPlaneRequest) {
757
+ const projectId = input.controlPlaneRequest.projectId;
758
+ reporter.info(`Control plane: ready to POST /v1/deploys/plan for ${projectId}`);
759
+ }
760
+ if (input.controlPlanePlan) {
761
+ reporter.info(input.unlinkedDeployPlan
762
+ ? `Unlinked deploy id: ${input.controlPlanePlan.deployId}`
763
+ : `Control plane deploy id: ${input.controlPlanePlan.deployId}`);
764
+ if (input.controlPlanePlan.sharedCloudflareQuota) {
765
+ reporter.info(`Shared Cloudflare quota: ${input.controlPlanePlan.sharedCloudflareQuota.visibleSiblingAccountIds.join(', ')}`);
766
+ }
767
+ reporter.info(`Leases: ${input.controlPlanePlan.leases.length}`);
768
+ }
769
+ if (input.controlPlaneRecord) {
770
+ reporter.info(`Local control plane record status: ${input.controlPlaneRecord.status}`);
771
+ }
772
+ if (input.controlPlanePromotion) {
773
+ reporter.info(`Promoted deploy: ${input.controlPlanePromotion.promoted.deployId}`);
774
+ }
775
+ if (input.cloudflareDeployResult) {
776
+ const deployed = input.cloudflareDeployResult.workers
777
+ .map((worker) => `${worker.workerName}${worker.dryRun ? ' (dry-run)' : ''}`)
778
+ .join(', ');
779
+ reporter.info(`Cloudflare deploy: ${deployed}`);
780
+ }
781
+ if (!input.linked && input.cloudflareDeployResult) {
782
+ reporter.info(`Machine dashboard: ${machineDashboardUrl(input.machineId)}`);
783
+ }
784
+ if (input.hubspotUploadResult) {
785
+ reporter.info(`HubSpot upload build id: ${input.hubspotUploadResult.buildId}`);
786
+ reporter.info(`HubSpot deploy id: ${input.hubspotUploadResult.deployId}`);
787
+ const authUrl = hubSpotAppAuthUrl(input.hubspotUploadResult);
788
+ if (!input.linked && authUrl) {
789
+ reporter.info(`App auth (grab client secret / configure install OAuth): ${authUrl}`);
790
+ }
791
+ }
792
+ reporter.info('Generated .hs-x/manifest.json and refs stubs.');
793
+ const firstWorkerUrl = input.cloudflareDeployResult?.workers.find((worker) => worker.url)?.url;
794
+ if (input.plan.partial) {
795
+ // The worker deployed; only the HubSpot upload failed. Say so loudly so the
796
+ // live worker isn't mistaken for a total failure despite the non-zero exit.
797
+ reporter.warn('HSX_W_DEPLOY_PARTIAL', `Cloudflare worker is live${firstWorkerUrl ? ` at ${firstWorkerUrl}` : ''}, but the HubSpot upload failed (see above). Exit code 20.`);
798
+ }
799
+ const doneMessage = input.planOnly
800
+ ? 'Planned'
801
+ : input.plan.partial
802
+ ? `Deployed to Cloudflare${firstWorkerUrl ? ` (${firstWorkerUrl})` : ''}; HubSpot upload failed`
803
+ : input.cloudflareDeployResult
804
+ ? input.linked
805
+ ? `Deployed${firstWorkerUrl ? ` to ${firstWorkerUrl}` : ''}`
806
+ : `Deployed (unlinked)${firstWorkerUrl ? ` to ${firstWorkerUrl}` : ''}; machine_id ${input.machineId}. Run \`hs-x link\` to claim this history.`
807
+ : input.hubspotUploadResult
808
+ ? input.linked
809
+ ? 'Deployed to HubSpot'
810
+ : `Deployed (unlinked); machine_id ${input.machineId}. Run \`hs-x link\` to claim this history.`
811
+ : 'Built artifacts';
812
+ reporter.done(doneMessage, input.plan.ok ? 0 : input.plan.partial ? 20 : 1);
813
+ }
814
+ function machineDashboardUrl(machineId) {
815
+ const base = process.env.HSX_DASHBOARD_URL ?? 'https://app.hs-x.dev';
816
+ return `${base.replace(/\/$/, '')}/m/${encodeURIComponent(machineId)}`;
817
+ }
818
+ /**
819
+ * The HubSpot developer-console URL for an uploaded app's auth tab, where a
820
+ * human grabs the client secret and configures install OAuth (HubSpot does not
821
+ * expose the secret via API). Requires the developer account id + the app uid;
822
+ * returns undefined when either is unknown (e.g. a control-plane lease deploy).
823
+ */
824
+ function hubSpotAppAuthUrl(result) {
825
+ if (!result.developerAccountId || !result.appUid)
826
+ return undefined;
827
+ const base = process.env.HSX_HUBSPOT_APP_BASE_URL ?? 'https://app.hubspot.com';
828
+ return `${base.replace(/\/$/, '')}/developer-projects/${encodeURIComponent(result.developerAccountId)}/project/${encodeURIComponent(result.projectName)}/component/${encodeURIComponent(result.appUid)}/auth`;
829
+ }
830
+ function formatDiagnostic(d) {
831
+ if (d && typeof d === 'object') {
832
+ const obj = d;
833
+ return `${obj.code ?? 'diag'}: ${obj.message ?? JSON.stringify(d)}`;
834
+ }
835
+ return String(d);
836
+ }
837
+ async function readPortalSchema(argv, workers) {
838
+ const schemaPath = resolveFlag(argv, '--portal-schema-fixture');
839
+ const live = argv.includes('--portal-schema-live');
840
+ const local = argv.includes('--local-hubspot-schema');
841
+ if (schemaPath && (live || local)) {
842
+ throw new Error('Use only one of --portal-schema-fixture, --portal-schema-live, or --local-hubspot-schema.');
843
+ }
844
+ if (schemaPath) {
845
+ const value = JSON.parse(await readFile(resolve(schemaPath), 'utf8'));
846
+ if (!isRecord(value) || !Array.isArray(value.objects)) {
847
+ throw new Error('--portal-schema-fixture must point to JSON with an objects array.');
848
+ }
849
+ return { observed: value, source: 'fixture' };
850
+ }
851
+ if (!live && !local) {
852
+ return undefined;
853
+ }
854
+ return {
855
+ observed: await readLivePortalSchema(argv, workers, { local }),
856
+ source: local ? 'hubspot-local' : 'hubspot-live',
857
+ };
858
+ }
859
+ async function readLivePortalSchema(argv, workers, options) {
860
+ const client = await createDeployHubSpotDeveloperClient({
861
+ argv,
862
+ local: options.local,
863
+ allowedOperations: ['hubspot-schema-management'],
864
+ });
865
+ const targets = portalSchemaTargets(workers);
866
+ const schemas = await client.schemaManagement.listSchemas().catch(() => ({ results: [] }));
867
+ const schemaByName = new Map(schemas.results.flatMap((schema) => {
868
+ const name = hubSpotSchemaName(schema);
869
+ return name ? [[name, schema]] : [];
870
+ }));
871
+ const objects = [];
872
+ for (const objectType of targets) {
873
+ try {
874
+ const properties = await client.schemaManagement.listProperties({ objectType });
875
+ objects.push({
876
+ name: objectType,
877
+ properties: properties.results.map(toObservedPortalProperty),
878
+ });
879
+ }
880
+ catch {
881
+ const schema = schemaByName.get(objectType);
882
+ if (schema?.properties) {
883
+ objects.push({
884
+ name: objectType,
885
+ properties: schema.properties.map(toObservedPortalProperty),
886
+ });
887
+ }
888
+ }
889
+ }
890
+ return { objects };
891
+ }
892
+ function portalSchemaTargets(workers) {
893
+ const targets = new Set();
894
+ for (const worker of workers) {
895
+ for (const capability of worker.capabilities) {
896
+ if (capability.kind === 'sync' && capability.into && capability.schema) {
897
+ targets.add(capability.into);
898
+ }
899
+ }
900
+ }
901
+ return [...targets].sort();
902
+ }
903
+ function hubSpotSchemaName(schema) {
904
+ if (schema.name) {
905
+ return schema.name;
906
+ }
907
+ return schema.objectTypeId;
908
+ }
909
+ function toObservedPortalProperty(property) {
910
+ return {
911
+ name: property.name,
912
+ type: property.type,
913
+ ...(property.fieldType ? { fieldType: property.fieldType } : {}),
914
+ };
915
+ }
916
+ async function applyPortalSchemaPlan({ argv, plan, source, }) {
917
+ if (!plan) {
918
+ throw new Error('--apply-schema requires --portal-schema-live or --local-hubspot-schema.');
919
+ }
920
+ if (source === 'fixture') {
921
+ throw new Error('--apply-schema cannot apply a fixture schema plan. Use --portal-schema-live.');
922
+ }
923
+ if (plan.errors.length > 0) {
924
+ throw new Error('--apply-schema refused to run because the portal schema plan has errors.');
925
+ }
926
+ const local = source === 'hubspot-local';
927
+ const client = await createDeployHubSpotDeveloperClient({
928
+ argv,
929
+ local,
930
+ allowedOperations: ['hubspot-schema-management'],
931
+ });
932
+ const applied = [];
933
+ const skipped = [];
934
+ for (const action of plan.actions) {
935
+ if (action.kind === 'will-create-object') {
936
+ await client.schemaManagement.createSchema({
937
+ name: action.objectType,
938
+ labels: portalObjectLabels(action.objectType),
939
+ });
940
+ applied.push(`create-object:${action.objectType}`);
941
+ continue;
942
+ }
943
+ if (!action.propertyName || !action.declaredType) {
944
+ skipped.push(action.message);
945
+ continue;
946
+ }
947
+ if (action.kind === 'will-create-property') {
948
+ const fieldType = defaultHubSpotFieldType(action.declaredType);
949
+ await client.schemaManagement.createProperty({
950
+ objectType: action.objectType,
951
+ name: action.propertyName,
952
+ label: propertyLabel(action.propertyName),
953
+ type: action.declaredType,
954
+ ...(fieldType ? { fieldType } : {}),
955
+ });
956
+ applied.push(`create-property:${action.objectType}.${action.propertyName}`);
957
+ continue;
958
+ }
959
+ if (action.kind === 'will-alter-property') {
960
+ const fieldType = defaultHubSpotFieldType(action.declaredType);
961
+ await client.schemaManagement.updateProperty({
962
+ objectType: action.objectType,
963
+ propertyName: action.propertyName,
964
+ type: action.declaredType,
965
+ ...(fieldType ? { fieldType } : {}),
966
+ });
967
+ applied.push(`alter-property:${action.objectType}.${action.propertyName}`);
968
+ }
969
+ }
970
+ return {
971
+ source: local ? 'hubspot-local' : 'hubspot-live',
972
+ applied,
973
+ skipped,
974
+ };
975
+ }
976
+ function renderPortalSchemaPlan(plan) {
977
+ if (plan.items.length === 0) {
978
+ return 'Portal schema: no changes or warnings.\n';
979
+ }
980
+ const lines = ['Portal schema:'];
981
+ for (const item of plan.items) {
982
+ const prefix = item.kind === 'warning' ? 'WARN' : item.kind === 'error' ? 'ERROR' : 'PLAN';
983
+ lines.push(`${prefix} ${item.message}`);
984
+ }
985
+ return `${lines.join('\n')}\n`;
986
+ }
987
+ function renderPortalSchemaApply(result) {
988
+ const lines = [`Portal schema apply (${result.source}):`];
989
+ for (const applied of result.applied) {
990
+ lines.push(`APPLIED ${applied}`);
991
+ }
992
+ for (const skipped of result.skipped) {
993
+ lines.push(`SKIPPED ${skipped}`);
994
+ }
995
+ if (result.applied.length === 0 && result.skipped.length === 0) {
996
+ lines.push('APPLIED no changes');
997
+ }
998
+ return `${lines.join('\n')}\n`;
999
+ }
1000
+ function propertyLabel(propertyName) {
1001
+ return propertyName
1002
+ .split(/[_-]+/)
1003
+ .filter(Boolean)
1004
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
1005
+ .join(' ');
1006
+ }
1007
+ function portalObjectLabels(objectType) {
1008
+ const singular = propertyLabel(objectType.replace(/s$/, '')) || objectType;
1009
+ return { singular, plural: propertyLabel(objectType) || objectType };
1010
+ }
1011
+ function defaultHubSpotFieldType(type) {
1012
+ switch (type) {
1013
+ case 'number':
1014
+ case 'currency':
1015
+ return 'number';
1016
+ case 'date':
1017
+ case 'datetime':
1018
+ return 'date';
1019
+ case 'enumeration':
1020
+ return 'select';
1021
+ case 'bool':
1022
+ case 'boolean':
1023
+ return 'booleancheckbox';
1024
+ default:
1025
+ return 'text';
1026
+ }
1027
+ }
1028
+ async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan, controlPlaneUrl, userId, machineId, heartbeatEnabled, dryRun, }) {
1029
+ // Fail fast on missing Cloudflare credentials BEFORE deploying any Worker, so
1030
+ // an unlinked deploy can't leave a Worker live and then error on the
1031
+ // tenant-state write for a missing account id.
1032
+ if (!controlPlaneUrl && !dryRun) {
1033
+ await preflightTenantStateCredentials({ argv, root });
1034
+ }
1035
+ const sourcePaths = await discoverWorkerSourcePaths(root);
1036
+ const concurrency = Math.max(1, Math.min(4, workers.length));
1037
+ const results = await Effect.runPromise(Effect.forEach(workers.map((worker, index) => ({ worker, index })), ({ worker, index }) => Effect.promise(() => executeCloudflareWorkerDeploy({
1038
+ argv,
1039
+ root,
1040
+ worker,
1041
+ index,
1042
+ sourcePaths,
1043
+ controlPlanePlan,
1044
+ userId,
1045
+ machineId,
1046
+ heartbeatEnabled,
1047
+ dryRun,
1048
+ ...(controlPlaneUrl ? { controlPlaneUrl } : {}),
1049
+ })), { concurrency }));
1050
+ return {
1051
+ deployId: controlPlanePlan.deployId,
1052
+ workers: results,
1053
+ };
1054
+ }
1055
+ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, sourcePaths, controlPlanePlan, controlPlaneUrl, userId, machineId, heartbeatEnabled, dryRun, }) {
1056
+ const workerSourcePath = sourcePaths.get(worker.name);
1057
+ if (!workerSourcePath) {
1058
+ throw new Error(`Could not find source file for worker ${worker.name}.`);
1059
+ }
1060
+ const workerName = controlPlanePlan.resources.workerNames[index];
1061
+ if (!workerName) {
1062
+ throw new Error(`Control-plane deploy plan did not include a Cloudflare name for ${worker.name}.`);
1063
+ }
1064
+ const entrypointPath = join(root, '.hs-x', 'cloudflare', `${fileSlug(worker.name)}.entry.ts`);
1065
+ await mkdir(dirname(entrypointPath), { recursive: true });
1066
+ const appConfig = await readHsxAppConfig(root);
1067
+ const environment = resolveFlag(argv, '--env') ?? resolveFlag(argv, '--environment') ?? 'production';
1068
+ const deployCliConfig = loadCliConfig(argv);
1069
+ const runtimeControlPlaneUrl = formatConfigUrl(deployCliConfig.runtimeControlPlaneUrl ?? new URL(DEFAULT_CONTROL_PLANE_URL));
1070
+ await writeFile(entrypointPath, renderCloudflareWorkerEntrypoint({
1071
+ workerImportPath: relativeImportPath(dirname(entrypointPath), workerSourcePath),
1072
+ ...(controlPlaneUrl && appConfig.billing
1073
+ ? {
1074
+ billing: {
1075
+ controlPlaneUrl: runtimeControlPlaneUrl,
1076
+ accountId: controlPlanePlan.accountId,
1077
+ projectId: controlPlanePlan.projectId,
1078
+ environment,
1079
+ },
1080
+ }
1081
+ : {}),
1082
+ ...(heartbeatEnabled
1083
+ ? {
1084
+ heartbeat: controlPlaneUrl
1085
+ ? {
1086
+ mode: 'linked',
1087
+ controlPlaneUrl: runtimeControlPlaneUrl,
1088
+ accountId: controlPlanePlan.accountId,
1089
+ hsXAccountId: controlPlanePlan.accountId,
1090
+ projectId: controlPlanePlan.projectId,
1091
+ deployId: controlPlanePlan.deployId,
1092
+ environment,
1093
+ manifestHash: controlPlanePlan.manifestHash,
1094
+ }
1095
+ : {
1096
+ mode: 'anonymous',
1097
+ controlPlaneUrl: formatConfigUrl(loadCliConfig(argv).runtimeControlPlaneUrl ??
1098
+ new URL(DEFAULT_CONTROL_PLANE_URL)),
1099
+ machineId,
1100
+ projectId: controlPlanePlan.projectId,
1101
+ deployId: controlPlanePlan.deployId,
1102
+ environment,
1103
+ manifestHash: controlPlanePlan.manifestHash,
1104
+ },
1105
+ }
1106
+ : {}),
1107
+ }));
1108
+ const compatibilityDate = resolveFlag(argv, '--compatibility-date') ?? new Date().toISOString().slice(0, 10);
1109
+ const hubspotAppId = readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID);
1110
+ const installRuntimeBinding = hubspotAppId
1111
+ ? controlPlaneUrl
1112
+ ? await ensureDeployInstallRuntimeBinding({
1113
+ argv,
1114
+ root,
1115
+ controlPlaneUrl,
1116
+ userId,
1117
+ accountId: controlPlanePlan.accountId,
1118
+ projectId: controlPlanePlan.projectId,
1119
+ environment,
1120
+ hubSpotAppId: hubspotAppId,
1121
+ dryRun,
1122
+ })
1123
+ : await ensureTenantDeployInstallRuntimeBinding({
1124
+ argv,
1125
+ root,
1126
+ ownerId: controlPlanePlan.accountId,
1127
+ projectId: controlPlanePlan.projectId,
1128
+ environment,
1129
+ hubSpotAppId: hubspotAppId,
1130
+ dryRun,
1131
+ })
1132
+ : undefined;
1133
+ // ADR-015/ADR-014 tenant data plane — linked deploys only (codegen emits the
1134
+ // options solely for heartbeat.mode==='linked'). Provisions the project D1 +
1135
+ // flags KV + grant secret, persisting the scope + secret to local state so the
1136
+ // leaveable `hs-x flags` CLI can sign without the control plane.
1137
+ const tenantDataPlane = installRuntimeBinding && controlPlaneUrl && hubspotAppId && !dryRun
1138
+ ? await ensureTenantDataPlane({
1139
+ argv,
1140
+ root,
1141
+ accountId: controlPlanePlan.accountId,
1142
+ projectId: controlPlanePlan.projectId,
1143
+ environment,
1144
+ hubSpotAppId: hubspotAppId,
1145
+ install: installRuntimeBinding,
1146
+ controlPlaneUrl,
1147
+ userId,
1148
+ })
1149
+ : undefined;
1150
+ const hubSpotOAuthSecret = hubspotAppId && controlPlaneUrl
1151
+ ? await readDeployHubSpotOAuthSecret({
1152
+ controlPlaneUrl,
1153
+ userId,
1154
+ accountId: controlPlanePlan.accountId,
1155
+ projectId: controlPlanePlan.projectId,
1156
+ environment,
1157
+ hubSpotAppId: hubspotAppId,
1158
+ })
1159
+ : undefined;
1160
+ const hubSpotClientId = hubSpotOAuthSecret?.clientId ??
1161
+ resolveFlag(argv, '--hubspot-client-id') ??
1162
+ process.env.HSX_HUBSPOT_CLIENT_ID;
1163
+ // The client secret comes from the control plane when linked; unlinked it can
1164
+ // be supplied directly so the deployed Worker can complete the install OAuth
1165
+ // token exchange (otherwise /oauth-start 500s with "missing HSX_HUBSPOT_CLIENT_ID").
1166
+ const hubSpotClientSecret = hubSpotOAuthSecret?.clientSecret ??
1167
+ resolveFlag(argv, '--hubspot-client-secret') ??
1168
+ process.env.HSX_HUBSPOT_CLIENT_SECRET;
1169
+ // Interactively fill the unlinked OAuth client id/secret the deployed Worker
1170
+ // needs for /oauth-start, instead of silently shipping a Worker that 500s.
1171
+ const { clientId: resolvedHubSpotClientId, clientSecret: resolvedHubSpotClientSecret } = await resolveUnlinkedOAuthCredentials({
1172
+ auth: appConfig.auth,
1173
+ linked: Boolean(controlPlaneUrl),
1174
+ dryRun,
1175
+ allowPrompt: index === 0,
1176
+ hubSpotClientId,
1177
+ hubSpotClientSecret,
1178
+ });
1179
+ const hubSpotScopesForRuntime = await readHsxAppScopesForDeploy(root);
1180
+ const billingRuntimeToken = controlPlaneUrl && appConfig.billing && !dryRun
1181
+ ? await requestDeployBillingRuntimeToken({
1182
+ controlPlaneUrl,
1183
+ userId,
1184
+ accountId: controlPlanePlan.accountId,
1185
+ projectId: controlPlanePlan.projectId,
1186
+ environment,
1187
+ })
1188
+ : undefined;
1189
+ const configPath = join(root, '.hs-x', 'cloudflare', `${fileSlug(worker.name)}.wrangler.toml`);
1190
+ await writeFile(configPath, renderWranglerConfig({
1191
+ workerName,
1192
+ entrypointPath: relativeImportPath(dirname(configPath), entrypointPath),
1193
+ compatibilityDate,
1194
+ ...(installRuntimeBinding
1195
+ ? { installKvNamespaceId: installRuntimeBinding.installKvNamespaceId }
1196
+ : {}),
1197
+ ...(tenantDataPlane
1198
+ ? {
1199
+ flagsKvNamespaceId: tenantDataPlane.flagsKvNamespaceId,
1200
+ tenantD1DatabaseId: tenantDataPlane.tenantD1DatabaseId,
1201
+ tenantD1DatabaseName: tenantDataPlane.tenantD1DatabaseName,
1202
+ tenantMigrationsDir: relative(dirname(configPath), join(root, 'node_modules', '@hs-x', 'runtime', 'migrations')),
1203
+ }
1204
+ : {}),
1205
+ }));
1206
+ const command = [
1207
+ 'bun',
1208
+ 'x',
1209
+ 'wrangler',
1210
+ 'deploy',
1211
+ '--config',
1212
+ relative(root, configPath),
1213
+ ...(controlPlaneUrl ? ['--var', `HSX_CONTROL_PLANE_URL:${runtimeControlPlaneUrl}`] : []),
1214
+ ...(hubspotAppId ? ['--var', `HSX_APP_ID:${hubspotAppId}`] : []),
1215
+ ...(resolvedHubSpotClientId
1216
+ ? ['--var', `HSX_HUBSPOT_CLIENT_ID:${resolvedHubSpotClientId}`]
1217
+ : []),
1218
+ ...(hubSpotScopesForRuntime.length > 0
1219
+ ? ['--var', `HSX_HUBSPOT_SCOPES:${hubSpotScopesForRuntime.join(' ')}`]
1220
+ : []),
1221
+ ...(dryRun ? ['--dry-run'] : []),
1222
+ ];
1223
+ const deployEnv = cloudflareDeployEnv(argv);
1224
+ const output = await runCloudflareCommand(command, {
1225
+ cwd: root,
1226
+ env: deployEnv,
1227
+ });
1228
+ if (installRuntimeBinding?.tokenKeySecretValue && !dryRun) {
1229
+ await runCloudflareCommand([
1230
+ 'bun',
1231
+ 'x',
1232
+ 'wrangler',
1233
+ 'secret',
1234
+ 'put',
1235
+ installRuntimeBinding.tokenKeySecretName,
1236
+ '--name',
1237
+ workerName,
1238
+ ], {
1239
+ cwd: root,
1240
+ env: deployEnv,
1241
+ stdin: installRuntimeBinding.tokenKeySecretValue,
1242
+ });
1243
+ }
1244
+ if (resolvedHubSpotClientSecret && !dryRun) {
1245
+ await runCloudflareCommand([
1246
+ 'bun',
1247
+ 'x',
1248
+ 'wrangler',
1249
+ 'secret',
1250
+ 'put',
1251
+ controlPlanePlan.runtimeBindings.hubSpotOAuthClientSecretBinding,
1252
+ '--name',
1253
+ workerName,
1254
+ ], {
1255
+ cwd: root,
1256
+ env: deployEnv,
1257
+ stdin: resolvedHubSpotClientSecret,
1258
+ });
1259
+ }
1260
+ if (billingRuntimeToken && !dryRun) {
1261
+ await runCloudflareCommand(['bun', 'x', 'wrangler', 'secret', 'put', billingRuntimeToken.binding, '--name', workerName], {
1262
+ cwd: root,
1263
+ env: deployEnv,
1264
+ stdin: billingRuntimeToken.runtimeToken,
1265
+ });
1266
+ }
1267
+ // ADR-015 grant-verifier secret — only set when freshly generated (a re-deploy
1268
+ // reuses the value already on the Worker + in local state).
1269
+ if (tenantDataPlane?.syncGrantSecretValue && !dryRun) {
1270
+ await runCloudflareCommand([
1271
+ 'bun',
1272
+ 'x',
1273
+ 'wrangler',
1274
+ 'secret',
1275
+ 'put',
1276
+ tenantDataPlane.syncGrantSecretName,
1277
+ '--name',
1278
+ workerName,
1279
+ ], {
1280
+ cwd: root,
1281
+ env: deployEnv,
1282
+ stdin: tenantDataPlane.syncGrantSecretValue,
1283
+ });
1284
+ }
1285
+ // ADR-015 §5: register the deployed Worker URL control-plane-side so the
1286
+ // CP-mediated flag-authoring conduit (dashboard/agent → CP → tenant) can reach
1287
+ // it. The URL is only known after the deploy (the install-runtime PUT in
1288
+ // ensureDeployInstallRuntimeBinding runs pre-deploy), so this is a best-effort
1289
+ // post-deploy re-registration — a failure never breaks the deploy.
1290
+ const resolvedRuntimeUrl = resolveFlag(argv, '--runtime-origin') ??
1291
+ extractWorkerUrl(output.stdout) ??
1292
+ extractWorkerUrl(output.stderr);
1293
+ if (controlPlaneUrl && installRuntimeBinding && hubspotAppId && resolvedRuntimeUrl && !dryRun) {
1294
+ try {
1295
+ await putDeployInstallRuntimeBinding({
1296
+ controlPlaneUrl,
1297
+ userId,
1298
+ accountId: controlPlanePlan.accountId,
1299
+ projectId: controlPlanePlan.projectId,
1300
+ environment,
1301
+ hubSpotAppId: hubspotAppId,
1302
+ installKvNamespaceId: installRuntimeBinding.installKvNamespaceId,
1303
+ installKvNamespaceName: installRuntimeBinding.installKvNamespaceName,
1304
+ tokenKeySecretName: installRuntimeBinding.tokenKeySecretName,
1305
+ runtimeBaseUrl: resolvedRuntimeUrl,
1306
+ ...(tenantDataPlane ? { syncGrantSecretName: tenantDataPlane.syncGrantSecretName } : {}),
1307
+ });
1308
+ }
1309
+ catch (error) {
1310
+ process.stderr.write(`warning: could not register the tenant Worker URL with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
1311
+ }
1312
+ }
1313
+ return {
1314
+ workerName,
1315
+ workerSourcePath: relative(root, workerSourcePath),
1316
+ entrypointPath: relative(root, entrypointPath),
1317
+ configPath: relative(root, configPath),
1318
+ url: extractWorkerUrl(output.stdout) ?? extractWorkerUrl(output.stderr),
1319
+ command,
1320
+ dryRun,
1321
+ stdout: output.stdout,
1322
+ stderr: output.stderr,
1323
+ };
1324
+ }
1325
+ async function discoverWorkerSourcePaths(root) {
1326
+ const files = await collectFiles(join(root, 'src'));
1327
+ const sources = new Map();
1328
+ for (const file of files) {
1329
+ const source = await readFile(file, 'utf8');
1330
+ const workerName = /defineWorker\s*\(\s*["'`]([^"'`]+)["'`]/.exec(source)?.[1];
1331
+ if (workerName) {
1332
+ sources.set(workerName, file);
1333
+ }
1334
+ }
1335
+ return sources;
1336
+ }
1337
+ function relativeImportPath(fromDir, toFile) {
1338
+ const path = relative(fromDir, toFile).replaceAll('\\', '/');
1339
+ return path.startsWith('.') ? path : `./${path}`;
1340
+ }
1341
+ function fileSlug(value) {
1342
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'worker';
1343
+ }
1344
+ function cloudflareDeployEnv(argv) {
1345
+ const { apiToken, accountId } = resolveCloudflareCredentials(argv);
1346
+ return {
1347
+ ...(apiToken ? { CLOUDFLARE_API_TOKEN: apiToken } : {}),
1348
+ ...(accountId ? { CLOUDFLARE_ACCOUNT_ID: accountId } : {}),
1349
+ };
1350
+ }
1351
+ async function ensureTenantDeployInstallRuntimeBinding(input) {
1352
+ const { pointer, stateStore } = await ensureTenantStateStore({
1353
+ argv: input.argv,
1354
+ root: input.root,
1355
+ });
1356
+ const existing = await stateStore.getBinding({
1357
+ projectId: input.projectId,
1358
+ environment: input.environment,
1359
+ hubSpotAppId: input.hubSpotAppId,
1360
+ });
1361
+ if (existing) {
1362
+ return {
1363
+ installKvNamespaceId: existing.installKvNamespaceId,
1364
+ installKvNamespaceName: existing.installKvNamespaceName,
1365
+ tokenKeySecretName: existing.tokenKeySecretName,
1366
+ };
1367
+ }
1368
+ if (input.dryRun)
1369
+ return undefined;
1370
+ const installKvNamespaceName = installKvNamespaceNameFor({
1371
+ accountId: input.ownerId,
1372
+ projectId: input.projectId,
1373
+ environment: input.environment,
1374
+ hubSpotAppId: input.hubSpotAppId,
1375
+ });
1376
+ const namespaceOutput = await runCloudflareCommand([
1377
+ 'bun',
1378
+ 'x',
1379
+ 'wrangler',
1380
+ 'kv',
1381
+ 'namespace',
1382
+ 'create',
1383
+ installKvNamespaceName,
1384
+ '--binding',
1385
+ 'INSTALL_KV',
1386
+ ], {
1387
+ cwd: input.root,
1388
+ env: cloudflareDeployEnv(input.argv),
1389
+ });
1390
+ const installKvNamespaceId = extractKvNamespaceId(`${namespaceOutput.stdout}\n${namespaceOutput.stderr}`);
1391
+ if (!installKvNamespaceId) {
1392
+ throw new Error('Could not read Cloudflare KV namespace id from wrangler output.');
1393
+ }
1394
+ const tokenKeySecretName = 'HSX_TOKEN_KEY';
1395
+ await stateStore.putBinding(Schema.decodeUnknownSync(schemas.LocalAppRuntimeBinding)({
1396
+ ownerId: pointer.ownerId,
1397
+ projectId: input.projectId,
1398
+ hubSpotAppId: input.hubSpotAppId,
1399
+ environment: input.environment,
1400
+ installKvNamespaceId,
1401
+ installKvNamespaceName,
1402
+ tokenKeySecretName,
1403
+ updatedAt: new Date().toISOString(),
1404
+ }));
1405
+ return {
1406
+ installKvNamespaceId,
1407
+ installKvNamespaceName,
1408
+ tokenKeySecretName,
1409
+ tokenKeySecretValue: randomBytes(32).toString('base64url'),
1410
+ };
1411
+ }
1412
+ async function ensureDeployInstallRuntimeBinding(input) {
1413
+ const existing = await readDeployInstallRuntimeBinding(input);
1414
+ if (existing) {
1415
+ return {
1416
+ installKvNamespaceId: existing.installKvNamespaceId,
1417
+ installKvNamespaceName: existing.installKvNamespaceName,
1418
+ tokenKeySecretName: existing.tokenKeySecretName,
1419
+ };
1420
+ }
1421
+ if (input.dryRun) {
1422
+ return undefined;
1423
+ }
1424
+ const installKvNamespaceName = installKvNamespaceNameFor({
1425
+ accountId: input.accountId,
1426
+ projectId: input.projectId,
1427
+ environment: input.environment,
1428
+ hubSpotAppId: input.hubSpotAppId,
1429
+ });
1430
+ const namespaceOutput = await runCloudflareCommand([
1431
+ 'bun',
1432
+ 'x',
1433
+ 'wrangler',
1434
+ 'kv',
1435
+ 'namespace',
1436
+ 'create',
1437
+ installKvNamespaceName,
1438
+ '--binding',
1439
+ 'INSTALL_KV',
1440
+ ], {
1441
+ cwd: input.root,
1442
+ env: cloudflareDeployEnv(input.argv),
1443
+ });
1444
+ const installKvNamespaceId = extractKvNamespaceId(`${namespaceOutput.stdout}\n${namespaceOutput.stderr}`);
1445
+ if (!installKvNamespaceId) {
1446
+ throw new Error('Could not read Cloudflare KV namespace id from wrangler output.');
1447
+ }
1448
+ const tokenKeySecretName = 'HSX_TOKEN_KEY';
1449
+ await putDeployInstallRuntimeBinding({
1450
+ ...input,
1451
+ installKvNamespaceId,
1452
+ installKvNamespaceName,
1453
+ tokenKeySecretName,
1454
+ });
1455
+ return {
1456
+ installKvNamespaceId,
1457
+ installKvNamespaceName,
1458
+ tokenKeySecretName,
1459
+ tokenKeySecretValue: randomBytes(32).toString('base64url'),
1460
+ };
1461
+ }
1462
+ async function readDeployInstallRuntimeBinding(input) {
1463
+ const response = await hostedHttp({
1464
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/install-runtime?environment=${encodeURIComponent(input.environment)}`, input.controlPlaneUrl),
1465
+ headers: await controlPlaneAuthHeaders(input.userId),
1466
+ });
1467
+ if (response.status === 404) {
1468
+ return undefined;
1469
+ }
1470
+ const body = await response.json();
1471
+ if (!response.ok) {
1472
+ const message = isRecord(body) && typeof body.message === 'string'
1473
+ ? body.message
1474
+ : `Control plane returned ${response.status}.`;
1475
+ throw new Error(`Could not read scoped install runtime binding: ${message}`);
1476
+ }
1477
+ return Schema.decodeUnknownSync(schemas.HubSpotAppInstallRuntimeMetadata)(body);
1478
+ }
1479
+ async function putDeployInstallRuntimeBinding(input) {
1480
+ const response = await hostedHttp({
1481
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/install-runtime`, input.controlPlaneUrl),
1482
+ method: 'PUT',
1483
+ headers: await controlPlaneAuthHeaders(input.userId),
1484
+ body: {
1485
+ environment: input.environment,
1486
+ installKvNamespaceId: input.installKvNamespaceId,
1487
+ installKvNamespaceName: input.installKvNamespaceName,
1488
+ tokenKeySecretName: input.tokenKeySecretName,
1489
+ ...(input.runtimeBaseUrl ? { runtimeBaseUrl: input.runtimeBaseUrl } : {}),
1490
+ ...(input.syncGrantSecretName ? { syncGrantSecretName: input.syncGrantSecretName } : {}),
1491
+ },
1492
+ });
1493
+ const body = await response.json();
1494
+ if (!response.ok) {
1495
+ const message = isRecord(body) && typeof body.message === 'string'
1496
+ ? body.message
1497
+ : `Control plane returned ${response.status}.`;
1498
+ throw new Error(`Could not store scoped install runtime binding: ${message}`);
1499
+ }
1500
+ }
1501
+ async function requestDeployBillingRuntimeToken(input) {
1502
+ const response = await hostedHttp({
1503
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/billing/runtime-token`, input.controlPlaneUrl),
1504
+ method: 'POST',
1505
+ headers: await controlPlaneAuthHeaders(input.userId),
1506
+ body: {
1507
+ environment: input.environment,
1508
+ },
1509
+ });
1510
+ const body = await response.json();
1511
+ if (!response.ok) {
1512
+ const message = isRecord(body) && typeof body.message === 'string'
1513
+ ? body.message
1514
+ : `Control plane returned ${response.status}.`;
1515
+ throw new Error(`Could not mint billing runtime token: ${message}`);
1516
+ }
1517
+ if (!isRecord(body) ||
1518
+ typeof body.runtimeToken !== 'string' ||
1519
+ body.binding !== 'HSX_RUNTIME_TOKEN') {
1520
+ throw new Error('Control plane returned a malformed billing runtime token response.');
1521
+ }
1522
+ return {
1523
+ runtimeToken: body.runtimeToken,
1524
+ binding: body.binding,
1525
+ };
1526
+ }
1527
+ function installKvNamespaceNameFor(input) {
1528
+ return truncateCloudflareResourceName(`hsx-${cloudflareNameSlug(input.accountId)}-${cloudflareNameSlug(input.projectId)}-${cloudflareNameSlug(input.environment)}-${cloudflareNameSlug(String(input.hubSpotAppId))}-install`);
1529
+ }
1530
+ function cloudflareNameSlug(value) {
1531
+ return (value
1532
+ .toLowerCase()
1533
+ .replace(/[^a-z0-9]+/g, '-')
1534
+ .replace(/^-+|-+$/g, '') || 'x');
1535
+ }
1536
+ function truncateCloudflareResourceName(value) {
1537
+ return value.length <= 63 ? value : value.slice(0, 63).replace(/-+$/g, '');
1538
+ }
1539
+ function extractKvNamespaceId(output) {
1540
+ return (/"id"\s*:\s*"([^"]+)"/.exec(output)?.[1] ??
1541
+ /id\s*=\s*"([^"]+)"/.exec(output)?.[1] ??
1542
+ /id[:\s]+([a-f0-9]{16,})/i.exec(output)?.[1]);
1543
+ }
1544
+ // ADR-015/ADR-014 tenant data plane: one project-scoped D1 (flag config +
1545
+ // installer PII + platform events) and one flags-snapshot KV per install.
1546
+ function tenantD1DatabaseNameFor(input) {
1547
+ return truncateCloudflareResourceName(`hsx-${cloudflareNameSlug(input.accountId)}-${cloudflareNameSlug(input.projectId)}-${cloudflareNameSlug(input.environment)}-${cloudflareNameSlug(String(input.hubSpotAppId))}-tenant`);
1548
+ }
1549
+ function flagsKvNamespaceNameFor(input) {
1550
+ return truncateCloudflareResourceName(`hsx-${cloudflareNameSlug(input.accountId)}-${cloudflareNameSlug(input.projectId)}-${cloudflareNameSlug(input.environment)}-${cloudflareNameSlug(String(input.hubSpotAppId))}-flags`);
1551
+ }
1552
+ function extractD1DatabaseId(output) {
1553
+ return (/"database_id"\s*:\s*"([^"]+)"/.exec(output)?.[1] ??
1554
+ /database_id\s*=\s*"([^"]+)"/.exec(output)?.[1] ??
1555
+ /database_id[:\s]+([a-f0-9-]{16,})/i.exec(output)?.[1]);
1556
+ }
1557
+ /**
1558
+ * ADR-015/ADR-014 §9 + §5: provision the project-scoped tenant D1 + the
1559
+ * flags-snapshot KV for a LINKED deploy, generate the per-install
1560
+ * HSX_SYNC_GRANT_KEY, and persist everything (including the InstallationKey
1561
+ * scope accountId + the secret VALUE) to local state so the leaveable
1562
+ * `hs-x flags` CLI can sign flags:write grants without the control plane.
1563
+ *
1564
+ * Idempotent: re-uses already-provisioned ids + an already-generated secret from
1565
+ * local state (so a re-deploy keeps the same secret the Worker already holds).
1566
+ * Returns `syncGrantSecretValue` only when freshly generated — the caller does
1567
+ * a `wrangler secret put` only then. Tenant-data-plane activation is
1568
+ * linked-only (codegen emits the options solely for heartbeat.mode==='linked'),
1569
+ * so this is never called for unlinked direct-to-HubSpot deploys.
1570
+ */
1571
+ async function ensureTenantDataPlane(input) {
1572
+ const { pointer, stateStore } = await ensureTenantStateStore({
1573
+ argv: input.argv,
1574
+ root: input.root,
1575
+ });
1576
+ const existing = await stateStore.getBinding({
1577
+ projectId: input.projectId,
1578
+ environment: input.environment,
1579
+ hubSpotAppId: input.hubSpotAppId,
1580
+ });
1581
+ const env = cloudflareDeployEnv(input.argv);
1582
+ const tenantD1DatabaseName = existing?.tenantD1DatabaseName ?? tenantD1DatabaseNameFor(input);
1583
+ let tenantD1DatabaseId = existing?.tenantD1DatabaseId;
1584
+ if (!tenantD1DatabaseId) {
1585
+ const output = await runCloudflareCommand(['bun', 'x', 'wrangler', 'd1', 'create', tenantD1DatabaseName], { cwd: input.root, env });
1586
+ tenantD1DatabaseId = extractD1DatabaseId(`${output.stdout}\n${output.stderr}`);
1587
+ if (!tenantD1DatabaseId) {
1588
+ throw new Error('Could not read tenant D1 database id from wrangler output.');
1589
+ }
1590
+ }
1591
+ const flagsKvNamespaceName = existing?.flagsKvNamespaceName ?? flagsKvNamespaceNameFor(input);
1592
+ let flagsKvNamespaceId = existing?.flagsKvNamespaceId;
1593
+ if (!flagsKvNamespaceId) {
1594
+ const output = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'create', flagsKvNamespaceName, '--binding', 'FLAGS_KV'], { cwd: input.root, env });
1595
+ flagsKvNamespaceId = extractKvNamespaceId(`${output.stdout}\n${output.stderr}`);
1596
+ if (!flagsKvNamespaceId) {
1597
+ throw new Error('Could not read flags KV namespace id from wrangler output.');
1598
+ }
1599
+ }
1600
+ const syncGrantSecretName = 'HSX_SYNC_GRANT_KEY';
1601
+ const reusedSecret = existing?.syncGrantSecretValue;
1602
+ const syncGrantSecretValue = reusedSecret ?? randomBytes(32).toString('base64url');
1603
+ await stateStore.putBinding(Schema.decodeUnknownSync(schemas.LocalAppRuntimeBinding)({
1604
+ ownerId: pointer.ownerId,
1605
+ tenantScopeAccountId: input.accountId,
1606
+ projectId: input.projectId,
1607
+ hubSpotAppId: input.hubSpotAppId,
1608
+ environment: input.environment,
1609
+ installKvNamespaceId: input.install.installKvNamespaceId,
1610
+ installKvNamespaceName: input.install.installKvNamespaceName,
1611
+ tokenKeySecretName: input.install.tokenKeySecretName,
1612
+ tenantD1DatabaseId,
1613
+ tenantD1DatabaseName,
1614
+ flagsKvNamespaceId,
1615
+ flagsKvNamespaceName,
1616
+ syncGrantSecretName,
1617
+ syncGrantSecretValue,
1618
+ updatedAt: new Date().toISOString(),
1619
+ }));
1620
+ // ADR-015 §5 "Both": register the secret control-plane-side for the (gated)
1621
+ // dashboard/agent/sync consumers. Best-effort — the leaveable CLI-direct path
1622
+ // reads the value from local state and needs no control plane.
1623
+ if (input.controlPlaneUrl && input.userId) {
1624
+ try {
1625
+ await registerDeploySyncGrantSecret({
1626
+ controlPlaneUrl: input.controlPlaneUrl,
1627
+ userId: input.userId,
1628
+ accountId: input.accountId,
1629
+ projectId: input.projectId,
1630
+ environment: input.environment,
1631
+ hubSpotAppId: input.hubSpotAppId,
1632
+ syncGrantSecret: syncGrantSecretValue,
1633
+ });
1634
+ }
1635
+ catch (error) {
1636
+ process.stderr.write(`warning: could not register the sync grant secret with the control plane: ${error instanceof Error ? error.message : String(error)}\n`);
1637
+ }
1638
+ }
1639
+ return {
1640
+ tenantD1DatabaseId,
1641
+ tenantD1DatabaseName,
1642
+ flagsKvNamespaceId,
1643
+ flagsKvNamespaceName,
1644
+ syncGrantSecretName,
1645
+ ...(reusedSecret ? {} : { syncGrantSecretValue }),
1646
+ };
1647
+ }
1648
+ async function registerDeploySyncGrantSecret(input) {
1649
+ const response = await hostedHttp({
1650
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/sync-grant-secret`, input.controlPlaneUrl),
1651
+ method: 'PUT',
1652
+ headers: await controlPlaneAuthHeaders(input.userId),
1653
+ body: {
1654
+ environment: input.environment,
1655
+ syncGrantSecret: input.syncGrantSecret,
1656
+ },
1657
+ });
1658
+ const body = await response.json();
1659
+ if (!response.ok) {
1660
+ const message = isRecord(body) && typeof body.message === 'string'
1661
+ ? body.message
1662
+ : `Control plane returned ${response.status}.`;
1663
+ throw new Error(`Could not register the sync grant secret: ${message}`);
1664
+ }
1665
+ }
1666
+ export function renderWranglerConfig(input) {
1667
+ const lines = [
1668
+ '# Generated by hs-x. Do not edit by hand.',
1669
+ `name = "${tomlString(input.workerName)}"`,
1670
+ `main = "${tomlString(input.entrypointPath)}"`,
1671
+ `compatibility_date = "${tomlString(input.compatibilityDate)}"`,
1672
+ ];
1673
+ if (input.installKvNamespaceId) {
1674
+ lines.push('', '[[kv_namespaces]]', 'binding = "INSTALL_KV"', `id = "${tomlString(input.installKvNamespaceId)}"`);
1675
+ }
1676
+ // ADR-015 flag-snapshot KV (the eval-path hot replica).
1677
+ if (input.flagsKvNamespaceId) {
1678
+ lines.push('', '[[kv_namespaces]]', 'binding = "FLAGS_KV"', `id = "${tomlString(input.flagsKvNamespaceId)}"`);
1679
+ }
1680
+ // ADR-015/ADR-014 project-scoped tenant D1 (flag config + installer PII +
1681
+ // platform events). migrations_dir lets wrangler auto-apply the tenant
1682
+ // migrations on deploy.
1683
+ if (input.tenantD1DatabaseId) {
1684
+ lines.push('', '[[d1_databases]]', 'binding = "TENANT_DB"');
1685
+ if (input.tenantD1DatabaseName) {
1686
+ lines.push(`database_name = "${tomlString(input.tenantD1DatabaseName)}"`);
1687
+ }
1688
+ lines.push(`database_id = "${tomlString(input.tenantD1DatabaseId)}"`);
1689
+ if (input.tenantMigrationsDir) {
1690
+ lines.push(`migrations_dir = "${tomlString(input.tenantMigrationsDir)}"`);
1691
+ }
1692
+ }
1693
+ return `${lines.join('\n')}\n`;
1694
+ }
1695
+ function tomlString(value) {
1696
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
1697
+ }
1698
+ async function readDeployHubSpotOAuthSecret(input) {
1699
+ const response = await hostedHttp({
1700
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/oauth-secret?environment=${encodeURIComponent(input.environment)}`, input.controlPlaneUrl),
1701
+ headers: await controlPlaneAuthHeaders(input.userId),
1702
+ });
1703
+ if (response.status === 404) {
1704
+ return undefined;
1705
+ }
1706
+ const body = await response.json();
1707
+ if (!response.ok) {
1708
+ const message = isRecord(body) && typeof body.message === 'string'
1709
+ ? body.message
1710
+ : `Control plane returned ${response.status}.`;
1711
+ throw new Error(`Could not read scoped HubSpot OAuth secret: ${message}`);
1712
+ }
1713
+ return Schema.decodeUnknownSync(schemas.HubSpotAppOAuthSecretDeployValue)(body);
1714
+ }
1715
+ function extractWorkerUrl(output) {
1716
+ return /https:\/\/[a-zA-Z0-9.-]+\.workers\.dev\b/.exec(output)?.[0];
1717
+ }
1718
+ async function ensureHubSpotRuntimeProjectArtifacts({ argv, root, app, workers, cloudflareDeployResult, needsRuntime, controlPlanePlan, }) {
1719
+ if (!needsRuntime) {
1720
+ return;
1721
+ }
1722
+ const runtimeBaseUrl = resolveFlag(argv, '--runtime-origin') ??
1723
+ cloudflareDeployResult?.workers.find((worker) => worker.url)?.url;
1724
+ if (!runtimeBaseUrl) {
1725
+ throw new Error('Runtime HubSpot metadata requires a Worker URL. Pass --runtime-origin or run a non-dry-run Cloudflare deploy that reports a workers.dev URL.');
1726
+ }
1727
+ const oauthCallbackPath = controlPlanePlan?.runtimeBindings?.oauthCallbackPath ?? '/oauth-callback';
1728
+ const redirectUrls = app.auth === 'oauth' && oauthCallbackPath
1729
+ ? [`${runtimeBaseUrl.replace(/\/$/, '')}${oauthCallbackPath}`]
1730
+ : [];
1731
+ const generated = generateHubSpotRuntimeProject({
1732
+ ...app,
1733
+ workers,
1734
+ runtimeBaseUrl,
1735
+ redirectUrls,
1736
+ });
1737
+ for (const [file, contents] of Object.entries(generated.files)) {
1738
+ const path = join(root, file);
1739
+ await mkdir(dirname(path), { recursive: true });
1740
+ await writeFile(path, contents);
1741
+ }
1742
+ }
1743
+ async function readHsxAppScopesForDeploy(root) {
1744
+ try {
1745
+ const app = await readHsxAppConfig(root);
1746
+ return app.scopes;
1747
+ }
1748
+ catch {
1749
+ return [];
1750
+ }
1751
+ }
1752
+ async function readHsxAppConfig(root) {
1753
+ const raw = await readFile(join(root, 'hsx.config.ts'), 'utf8');
1754
+ const loaded = parseHsxAppConfigLiteral(raw) ?? (await loadHsxAppConfigModule(root).catch(() => undefined));
1755
+ return {
1756
+ appName: loaded?.name ?? /name\s*:\s*["'`]([^"'`]+)["'`]/.exec(raw)?.[1] ?? basename(root),
1757
+ distribution: loaded?.distribution ?? /distribution\s*:\s*["'`]([^"'`]+)["'`]/.exec(raw)?.[1] ?? 'private',
1758
+ auth: loaded?.auth ?? /auth\s*:\s*["'`]([^"'`]+)["'`]/.exec(raw)?.[1] ?? 'oauth',
1759
+ platformVersion: loaded?.platformVersion ??
1760
+ /platformVersion\s*:\s*["'`]([^"'`]+)["'`]/.exec(raw)?.[1] ??
1761
+ '2026.03',
1762
+ scopes: loaded?.scopes ?? readStringArrayProperty(raw, 'scopes'),
1763
+ appObjects: discoverAppObjectDeclarations(raw),
1764
+ appObjectAssociations: discoverAppObjectAssociationDeclarations(raw),
1765
+ appEvents: discoverAppEventDeclarations(raw),
1766
+ ...(loaded?.billing ? { billing: loaded.billing } : {}),
1767
+ };
1768
+ }
1769
+ function parseHsxAppConfigLiteral(source) {
1770
+ const start = source.indexOf('defineApp(');
1771
+ if (start < 0)
1772
+ return undefined;
1773
+ const objectStart = source.indexOf('{', start);
1774
+ if (objectStart < 0)
1775
+ return undefined;
1776
+ let depth = 0;
1777
+ let quote;
1778
+ let escaped = false;
1779
+ for (let i = objectStart; i < source.length; i += 1) {
1780
+ const char = source[i];
1781
+ if (!char)
1782
+ continue;
1783
+ if (quote) {
1784
+ if (escaped) {
1785
+ escaped = false;
1786
+ }
1787
+ else if (char === '\\') {
1788
+ escaped = true;
1789
+ }
1790
+ else if (char === quote) {
1791
+ quote = undefined;
1792
+ }
1793
+ continue;
1794
+ }
1795
+ if (char === '"' || char === "'" || char === '`') {
1796
+ quote = char;
1797
+ continue;
1798
+ }
1799
+ if (char === '{')
1800
+ depth += 1;
1801
+ if (char === '}') {
1802
+ depth -= 1;
1803
+ if (depth === 0) {
1804
+ try {
1805
+ return Function(`"use strict"; return (${source.slice(objectStart, i + 1)});`)();
1806
+ }
1807
+ catch {
1808
+ return undefined;
1809
+ }
1810
+ }
1811
+ }
1812
+ }
1813
+ return undefined;
1814
+ }
1815
+ async function loadHsxAppConfigModule(root) {
1816
+ const url = pathToFileURL(join(root, 'hsx.config.ts'));
1817
+ url.searchParams.set('hsxConfigLoad', String(Date.now()));
1818
+ const mod = (await import(url.href));
1819
+ const app = mod.default;
1820
+ return app && typeof app === 'object'
1821
+ ? app
1822
+ : undefined;
1823
+ }
1824
+ function readStringArrayProperty(source, propertyName) {
1825
+ const body = new RegExp(`${propertyName}\\s*:\\s*\\[([\\s\\S]*?)\\]`).exec(source)?.[1];
1826
+ if (!body) {
1827
+ return [];
1828
+ }
1829
+ return [...body.matchAll(/["'`]([^"'`]+)["'`]/g)].map((match) => match[1] ?? '');
1830
+ }
1831
+ function callObjectBody(source, functionName) {
1832
+ const match = new RegExp(`\\b${functionName}\\s*\\(`).exec(source);
1833
+ if (!match)
1834
+ return undefined;
1835
+ const start = source.indexOf('{', match.index + match[0].length);
1836
+ if (start === -1)
1837
+ return undefined;
1838
+ const end = findMatching(source, start, '{', '}');
1839
+ return end === -1 ? undefined : source.slice(start + 1, end);
1840
+ }
1841
+ function discoverAppObjectDeclarations(source) {
1842
+ return declarationBodies(source, 'appObject').map(({ id, body }) => {
1843
+ const properties = discoverDeclarationProperties(body);
1844
+ const firstPropertyName = Object.keys(properties)[0] ?? 'name';
1845
+ return {
1846
+ kind: 'app-object',
1847
+ id,
1848
+ uid: stringPropertyValue(body, 'uid') ?? defaultUid(id),
1849
+ name: stringPropertyValue(body, 'name') ?? defaultUid(id),
1850
+ label: stringPropertyValue(body, 'label') ?? id,
1851
+ singularForm: stringPropertyValue(body, 'singularForm') ?? stringPropertyValue(body, 'label') ?? id,
1852
+ pluralForm: stringPropertyValue(body, 'pluralForm') ?? `${stringPropertyValue(body, 'label') ?? id}s`,
1853
+ ...(stringPropertyValue(body, 'description')
1854
+ ? { description: stringPropertyValue(body, 'description') }
1855
+ : {}),
1856
+ ...(stringPropertyValue(body, 'appPrefix')
1857
+ ? { appPrefix: stringPropertyValue(body, 'appPrefix') }
1858
+ : {}),
1859
+ primaryDisplayLabelPropertyName: stringPropertyValue(body, 'primaryDisplayLabelPropertyName') ?? firstPropertyName,
1860
+ ...(readStringArrayProperty(body, 'secondaryDisplayLabelPropertyNames').length
1861
+ ? {
1862
+ secondaryDisplayLabelPropertyNames: readStringArrayProperty(body, 'secondaryDisplayLabelPropertyNames'),
1863
+ }
1864
+ : {}),
1865
+ ...(readStringArrayProperty(body, 'requiredProperties').length
1866
+ ? { requiredProperties: readStringArrayProperty(body, 'requiredProperties') }
1867
+ : {}),
1868
+ ...(readStringArrayProperty(body, 'searchableProperties').length
1869
+ ? { searchableProperties: readStringArrayProperty(body, 'searchableProperties') }
1870
+ : {}),
1871
+ ...(readStringArrayProperty(body, 'defaultCreateFormFields').length
1872
+ ? { defaultCreateFormFields: readStringArrayProperty(body, 'defaultCreateFormFields') }
1873
+ : {}),
1874
+ properties,
1875
+ };
1876
+ });
1877
+ }
1878
+ function discoverAppObjectAssociationDeclarations(source) {
1879
+ return declarationBodies(source, 'appObjectAssociation').map(({ id, body }) => ({
1880
+ kind: 'app-object-association',
1881
+ id,
1882
+ uid: stringPropertyValue(body, 'uid') ?? defaultUid(id),
1883
+ fromObjectType: stringPropertyValue(body, 'fromObjectType') ?? 'CONTACT',
1884
+ toObjectType: stringPropertyValue(body, 'toObjectType') ?? 'CONTACT',
1885
+ ...(stringPropertyValue(body, 'name') ? { name: stringPropertyValue(body, 'name') } : {}),
1886
+ ...(stringPropertyValue(body, 'label') ? { label: stringPropertyValue(body, 'label') } : {}),
1887
+ ...(stringPropertyValue(body, 'inverseLabel')
1888
+ ? { inverseLabel: stringPropertyValue(body, 'inverseLabel') }
1889
+ : {}),
1890
+ }));
1891
+ }
1892
+ function discoverAppEventDeclarations(source) {
1893
+ return declarationBodies(source, 'appEvent').map(({ id, body }) => ({
1894
+ kind: 'app-event',
1895
+ id,
1896
+ uid: stringPropertyValue(body, 'uid') ?? defaultUid(id),
1897
+ name: stringPropertyValue(body, 'name') ?? defaultUid(id),
1898
+ label: stringPropertyValue(body, 'label') ?? id,
1899
+ ...(stringPropertyValue(body, 'description')
1900
+ ? { description: stringPropertyValue(body, 'description') }
1901
+ : {}),
1902
+ objectType: stringPropertyValue(body, 'objectType') ?? 'CONTACT',
1903
+ ...(booleanPropertyValue(body, 'supportsCustomObject') === undefined
1904
+ ? {}
1905
+ : { supportsCustomObject: booleanPropertyValue(body, 'supportsCustomObject') }),
1906
+ ...(stringPropertyValue(body, 'headerTemplate')
1907
+ ? { headerTemplate: stringPropertyValue(body, 'headerTemplate') }
1908
+ : {}),
1909
+ ...(stringPropertyValue(body, 'detailTemplate')
1910
+ ? { detailTemplate: stringPropertyValue(body, 'detailTemplate') }
1911
+ : {}),
1912
+ properties: discoverDeclarationProperties(body),
1913
+ }));
1914
+ }
1915
+ function declarationBodies(source, functionName) {
1916
+ const declarations = [];
1917
+ const pattern = new RegExp(`\\b${functionName}\\s*\\(\\s*["'\`]([^"'\`]+)["'\`]`, 'g');
1918
+ for (const match of source.matchAll(pattern)) {
1919
+ const start = source.indexOf('{', (match.index ?? 0) + match[0].length);
1920
+ if (start === -1)
1921
+ continue;
1922
+ const end = findMatching(source, start, '{', '}');
1923
+ if (end === -1)
1924
+ continue;
1925
+ declarations.push({ id: match[1] ?? 'unknown', body: source.slice(start + 1, end) });
1926
+ }
1927
+ return declarations;
1928
+ }
1929
+ function discoverDeclarationProperties(body) {
1930
+ const propertiesBody = objectPropertyBody(body, 'properties');
1931
+ if (!propertiesBody) {
1932
+ return {};
1933
+ }
1934
+ const properties = {};
1935
+ for (const { name, body: propertyBody } of topLevelObjectEntries(propertiesBody)) {
1936
+ const options = readStringArrayProperty(propertyBody, 'options').map((value) => ({
1937
+ value,
1938
+ label: value,
1939
+ }));
1940
+ properties[name] = {
1941
+ type: stringPropertyValue(propertyBody, 'type') ?? 'string',
1942
+ label: stringPropertyValue(propertyBody, 'label') ?? name,
1943
+ ...(options.length ? { options } : {}),
1944
+ };
1945
+ }
1946
+ return properties;
1947
+ }
1948
+ function topLevelObjectEntries(source) {
1949
+ const entries = [];
1950
+ let index = 0;
1951
+ let depth = 0;
1952
+ while (index < source.length) {
1953
+ const char = source[index];
1954
+ if (char === '{' || char === '[' || char === '(') {
1955
+ depth += 1;
1956
+ index += 1;
1957
+ continue;
1958
+ }
1959
+ if (char === '}' || char === ']' || char === ')') {
1960
+ depth = Math.max(0, depth - 1);
1961
+ index += 1;
1962
+ continue;
1963
+ }
1964
+ if (depth === 0) {
1965
+ const match = /^[\s,]*([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(source.slice(index));
1966
+ if (match) {
1967
+ const name = match[1] ?? 'unknown';
1968
+ const valueStart = index + match[0].length;
1969
+ const braceStart = source.indexOf('{', valueStart);
1970
+ if (braceStart !== -1 && source.slice(valueStart, braceStart).trim() === '') {
1971
+ const braceEnd = findMatching(source, braceStart, '{', '}');
1972
+ if (braceEnd !== -1) {
1973
+ entries.push({ name, body: source.slice(braceStart + 1, braceEnd) });
1974
+ index = braceEnd + 1;
1975
+ continue;
1976
+ }
1977
+ }
1978
+ }
1979
+ }
1980
+ index += 1;
1981
+ }
1982
+ return entries;
1983
+ }
1984
+ function objectPropertyBody(source, propertyName) {
1985
+ const match = new RegExp(`\\b${propertyName}\\s*:`).exec(source);
1986
+ if (!match)
1987
+ return undefined;
1988
+ const start = source.indexOf('{', match.index + match[0].length);
1989
+ if (start === -1)
1990
+ return undefined;
1991
+ const end = findMatching(source, start, '{', '}');
1992
+ return end === -1 ? undefined : source.slice(start + 1, end);
1993
+ }
1994
+ function findMatching(source, start, open, close) {
1995
+ let depth = 0;
1996
+ for (let index = start; index < source.length; index += 1) {
1997
+ const char = source[index];
1998
+ if (char === open)
1999
+ depth += 1;
2000
+ if (char === close) {
2001
+ depth -= 1;
2002
+ if (depth === 0)
2003
+ return index;
2004
+ }
2005
+ }
2006
+ return -1;
2007
+ }
2008
+ function stringPropertyValue(source, propertyName) {
2009
+ return new RegExp(`\\b${propertyName}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`).exec(source)?.[1];
2010
+ }
2011
+ function booleanPropertyValue(source, propertyName) {
2012
+ const value = new RegExp(`\\b${propertyName}\\s*:\\s*(true|false)\\b`).exec(source)?.[1];
2013
+ return value === undefined ? undefined : value === 'true';
2014
+ }
2015
+ function defaultUid(id) {
2016
+ return id
2017
+ .trim()
2018
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
2019
+ .replace(/[^A-Za-z0-9]+/g, '_')
2020
+ .replace(/^_+|_+$/g, '')
2021
+ .toUpperCase();
2022
+ }
2023
+ function runCloudflareCommand(command, options) {
2024
+ return Effect.runPromise(Effect.promise(() => new Promise((resolvePromise, rejectPromise) => {
2025
+ const child = spawn(command[0] ?? 'bun', command.slice(1), {
2026
+ cwd: options.cwd,
2027
+ env: { ...process.env, ...options.env },
2028
+ stdio: [options.stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
2029
+ });
2030
+ let stdout = '';
2031
+ let stderr = '';
2032
+ child.stdout?.setEncoding('utf8');
2033
+ child.stderr?.setEncoding('utf8');
2034
+ child.stdout?.on('data', (chunk) => {
2035
+ stdout += chunk;
2036
+ });
2037
+ child.stderr?.on('data', (chunk) => {
2038
+ stderr += chunk;
2039
+ });
2040
+ if (options.stdin !== undefined) {
2041
+ child.stdin?.end(`${options.stdin}\n`);
2042
+ }
2043
+ child.on('error', rejectPromise);
2044
+ child.on('exit', (code) => {
2045
+ if (code === 0) {
2046
+ resolvePromise({ stdout, stderr });
2047
+ }
2048
+ else {
2049
+ rejectPromise(new Error(`Cloudflare deploy failed with exit code ${code ?? 'unknown'}.\n${stderr || stdout}`));
2050
+ }
2051
+ });
2052
+ })).pipe(Effect.withSpan('cli.deploy.cloudflare_command', {
2053
+ attributes: {
2054
+ executable: command[0] ?? 'bun',
2055
+ args_count: Math.max(0, command.length - 1),
2056
+ },
2057
+ })));
2058
+ }
2059
+ async function executeHubSpotOnlyUpload({ argv, root, local, uploadOnly, }) {
2060
+ const hsprojectConfig = await readHubSpotProjectConfig(root);
2061
+ const configuredProjectName = typeof hsprojectConfig.name === 'string' && hsprojectConfig.name.length > 0
2062
+ ? hsprojectConfig.name
2063
+ : undefined;
2064
+ // The HubSpot project name must match the name baked into the generated
2065
+ // bundle (hsproject.json + app-hsmeta uid), not the on-disk directory name.
2066
+ // Using basename(root) breaks when the project lives in a generic dir (e.g.
2067
+ // `app/`): the upload targets a project named "app" while the bundle declares
2068
+ // the real project id, and HubSpot rejects the mismatch with an opaque 400.
2069
+ const projectName = resolveFlag(argv, '--hubspot-project-name') ?? configuredProjectName ?? basename(root);
2070
+ const intermediateRepresentation = await readHubSpotProjectIntermediateRepresentation(root);
2071
+ const appUid = readAppUidFromIntermediateRepresentation(intermediateRepresentation);
2072
+ // For the post-deploy app Auth URL (unlinked installs). Resolved from
2073
+ // flag/env here so the URL is correct on the unlinked path; on a
2074
+ // control-plane lease path it stays undefined and the URL is skipped.
2075
+ const developerAccountId = resolveFlag(argv, '--developer-account-id') ?? process.env.HSX_HUBSPOT_DEVELOPER_ACCOUNT_ID;
2076
+ const archivePath = await writeLocalHubSpotProjectArchive(root, projectName, {
2077
+ sourceOnly: intermediateRepresentation !== undefined,
2078
+ });
2079
+ const client = await createDeployHubSpotDeveloperClient({
2080
+ argv,
2081
+ local,
2082
+ allowedOperations: ['hubspot-project-upload'],
2083
+ cwd: root,
2084
+ });
2085
+ await client.projects.ensureProject({ projectName });
2086
+ const upload = await client.projects.upload({
2087
+ projectName,
2088
+ archivePath,
2089
+ message: resolveFlag(argv, '--message') ?? 'HS-X HubSpot-only deploy',
2090
+ platformVersion: resolveFlag(argv, '--platform-version') ?? '2026.03',
2091
+ intermediateRepresentation,
2092
+ });
2093
+ const buildStatus = await waitForHubSpotBuildStatus({
2094
+ client,
2095
+ projectName,
2096
+ buildId: upload.buildId,
2097
+ timeoutMs: Number(resolveFlag(argv, '--hubspot-build-timeout-ms') ?? '60000'),
2098
+ intervalMs: 1000,
2099
+ });
2100
+ const autoDeployEnabled = readBooleanProperty(buildStatus, 'isAutoDeployEnabled');
2101
+ if (uploadOnly || readStatus(buildStatus) !== 'SUCCESS' || autoDeployEnabled) {
2102
+ const appId = readStatus(buildStatus) === 'SUCCESS'
2103
+ ? await optionalHubSpotAppId(client, projectName, appUid)
2104
+ : 0;
2105
+ return {
2106
+ projectName,
2107
+ archivePath,
2108
+ appId,
2109
+ appUid,
2110
+ developerAccountId,
2111
+ buildId: upload.buildId,
2112
+ deployId: undefined,
2113
+ buildStatus,
2114
+ deployStatus: undefined,
2115
+ deploySkipped: Boolean(uploadOnly || autoDeployEnabled),
2116
+ local,
2117
+ };
2118
+ }
2119
+ const deploy = await client.projects.deployBuild({
2120
+ projectName,
2121
+ buildId: upload.buildId,
2122
+ force: argv.includes('--force'),
2123
+ });
2124
+ const deployId = readNumericProperty(deploy, 'deployId');
2125
+ const deployStatus = deployId === undefined
2126
+ ? deploy
2127
+ : await client.projects.getDeployStatus({
2128
+ projectName,
2129
+ deployId,
2130
+ });
2131
+ const appId = await optionalHubSpotAppId(client, projectName, appUid);
2132
+ return {
2133
+ projectName,
2134
+ archivePath,
2135
+ appId,
2136
+ appUid,
2137
+ developerAccountId,
2138
+ buildId: upload.buildId,
2139
+ deployId,
2140
+ buildStatus,
2141
+ deployStatus,
2142
+ deploySkipped: false,
2143
+ local,
2144
+ };
2145
+ }
2146
+ async function optionalHubSpotAppId(client, projectName, appUid) {
2147
+ try {
2148
+ return await client.projects.resolveAppId(appUid === undefined ? { projectName } : { projectName, appUid });
2149
+ }
2150
+ catch {
2151
+ return 0;
2152
+ }
2153
+ }
2154
+ async function waitForHubSpotBuildStatus({ client, projectName, buildId, timeoutMs, intervalMs, }) {
2155
+ const startedAt = Date.now();
2156
+ let latest;
2157
+ do {
2158
+ latest = await client.projects.getBuildStatus({ projectName, buildId });
2159
+ const status = readStatus(latest);
2160
+ if (status === 'SUCCESS' || status === 'FAILURE') {
2161
+ return latest;
2162
+ }
2163
+ await sleep(intervalMs);
2164
+ } while (Date.now() - startedAt < timeoutMs);
2165
+ return latest;
2166
+ }
2167
+ function sleep(ms) {
2168
+ return new Promise((resolve) => setTimeout(resolve, ms));
2169
+ }
2170
+ async function writeLocalHubSpotProjectArchive(root, projectName, options = { sourceOnly: false }) {
2171
+ const outDir = await mkdtemp(join(tmpdir(), 'hs-x-hubspot-project-'));
2172
+ const archivePath = join(outDir, `${projectName.replace(/[^a-zA-Z0-9._-]+/g, '-')}-hubspot-project.zip`);
2173
+ const stageDir = join(outDir, 'project');
2174
+ await cp(root, stageDir, {
2175
+ recursive: true,
2176
+ filter(source) {
2177
+ return (!source.includes(`${join(root, 'node_modules')}/`) &&
2178
+ !source.includes(`${join(root, '.hs-x')}/`));
2179
+ },
2180
+ });
2181
+ await patchHubSpotProjectName(stageDir, projectName);
2182
+ await runZip(options.sourceOnly ? await hubSpotProjectSourceRoot(stageDir) : stageDir, archivePath);
2183
+ return archivePath;
2184
+ }
2185
+ async function hubSpotProjectSourceRoot(root) {
2186
+ const hsproject = await readHubSpotProjectConfig(root);
2187
+ const srcDir = typeof hsproject.srcDir === 'string' ? hsproject.srcDir : 'src';
2188
+ return join(root, srcDir);
2189
+ }
2190
+ async function patchHubSpotProjectName(root, projectName) {
2191
+ const path = join(root, 'hsproject.json');
2192
+ const raw = await readFile(path, 'utf8').catch(() => undefined);
2193
+ if (!raw) {
2194
+ return;
2195
+ }
2196
+ const parsed = JSON.parse(raw);
2197
+ if (!isRecord(parsed)) {
2198
+ return;
2199
+ }
2200
+ await writeFile(path, `${JSON.stringify({ ...parsed, name: projectName }, null, 2)}\n`);
2201
+ }
2202
+ async function readHubSpotProjectIntermediateRepresentation(root) {
2203
+ const hsproject = await readHubSpotProjectConfig(root);
2204
+ const srcDir = typeof hsproject.srcDir === 'string' ? hsproject.srcDir : 'src';
2205
+ const sourceRoot = join(root, srcDir);
2206
+ const metaFiles = await collectHubSpotMetaFiles(sourceRoot);
2207
+ const nodes = {};
2208
+ let appUid;
2209
+ for (const file of metaFiles) {
2210
+ const raw = await readFile(file, 'utf8');
2211
+ const parsed = JSON.parse(raw);
2212
+ if (!isRecord(parsed) || typeof parsed.uid !== 'string' || typeof parsed.type !== 'string') {
2213
+ continue;
2214
+ }
2215
+ if (parsed.type === 'app') {
2216
+ appUid = parsed.uid;
2217
+ }
2218
+ }
2219
+ for (const file of metaFiles) {
2220
+ const raw = await readFile(file, 'utf8');
2221
+ const parsed = JSON.parse(raw);
2222
+ if (!isRecord(parsed) ||
2223
+ typeof parsed.uid !== 'string' ||
2224
+ typeof parsed.type !== 'string' ||
2225
+ !isRecord(parsed.config)) {
2226
+ continue;
2227
+ }
2228
+ // Map the user-facing hsmeta `type` to HubSpot's internal component type
2229
+ // exactly as @hubspot/project-parsing-lib's mapToInternalType does:
2230
+ // `app` -> APPLICATION, otherwise upper-snake-case (card -> CARD,
2231
+ // workflow-action -> WORKFLOW_ACTION). The previous app/card-only switch
2232
+ // silently dropped workflow actions, so the IR sent to upload/new-api had
2233
+ // no child components and HubSpot rejected the build with NO_COMPONENTS.
2234
+ const componentType = parsed.type === 'app' ? 'APPLICATION' : parsed.type.toUpperCase().replace(/-/g, '_');
2235
+ nodes[parsed.uid] = {
2236
+ uid: parsed.uid,
2237
+ config: parsed.config,
2238
+ componentType,
2239
+ componentDeps: componentType !== 'APPLICATION' && appUid ? { app: appUid, allAppObjects: [] } : {},
2240
+ metaFilePath: file.slice(sourceRoot.length + 1),
2241
+ files: {},
2242
+ };
2243
+ }
2244
+ if (Object.keys(nodes).length === 0) {
2245
+ return undefined;
2246
+ }
2247
+ return {
2248
+ intermediateNodesIndexedByUid: nodes,
2249
+ profileData: { vars: { profileVariables: {} } },
2250
+ };
2251
+ }
2252
+ function readAppUidFromIntermediateRepresentation(value) {
2253
+ if (!isRecord(value) || !isRecord(value.intermediateNodesIndexedByUid)) {
2254
+ return undefined;
2255
+ }
2256
+ for (const node of Object.values(value.intermediateNodesIndexedByUid)) {
2257
+ if (!isRecord(node) || node.componentType !== 'APPLICATION' || typeof node.uid !== 'string') {
2258
+ continue;
2259
+ }
2260
+ return node.uid;
2261
+ }
2262
+ return undefined;
2263
+ }
2264
+ async function readHubSpotProjectConfig(root) {
2265
+ const raw = await readFile(join(root, 'hsproject.json'), 'utf8').catch(() => '{}');
2266
+ const parsed = JSON.parse(raw);
2267
+ return isRecord(parsed) ? parsed : {};
2268
+ }
2269
+ async function collectHubSpotMetaFiles(root) {
2270
+ const files = [];
2271
+ async function walk(dir) {
2272
+ let entries;
2273
+ try {
2274
+ entries = await readdir(dir, { withFileTypes: true });
2275
+ }
2276
+ catch {
2277
+ return;
2278
+ }
2279
+ for (const entry of entries) {
2280
+ const path = join(dir, entry.name);
2281
+ if (entry.isDirectory()) {
2282
+ await walk(path);
2283
+ }
2284
+ else if (entry.isFile() && entry.name.endsWith('-hsmeta.json')) {
2285
+ files.push(path);
2286
+ }
2287
+ }
2288
+ }
2289
+ await walk(root);
2290
+ return files.sort();
2291
+ }
2292
+ function runZip(root, archivePath) {
2293
+ return new Promise((resolve, reject) => {
2294
+ const child = spawn('zip', ['-qr', archivePath, '.', '-x', 'node_modules/*', '.hs-x/*'], {
2295
+ cwd: root,
2296
+ stdio: ['ignore', 'ignore', 'pipe'],
2297
+ });
2298
+ let stderr = '';
2299
+ child.stderr?.setEncoding('utf8');
2300
+ child.stderr?.on('data', (chunk) => {
2301
+ stderr += chunk;
2302
+ });
2303
+ child.on('error', reject);
2304
+ child.on('close', (code) => {
2305
+ if (code === 0) {
2306
+ resolve();
2307
+ }
2308
+ else {
2309
+ reject(new Error(`Unable to create HubSpot project zip archive: ${stderr.trim()}`));
2310
+ }
2311
+ });
2312
+ });
2313
+ }
2314
+ function readNumericProperty(value, key) {
2315
+ return isRecord(value) && typeof value[key] === 'number' ? value[key] : undefined;
2316
+ }
2317
+ function readBooleanProperty(value, key) {
2318
+ return isRecord(value) && typeof value[key] === 'boolean' ? value[key] : undefined;
2319
+ }
2320
+ function readStatus(value) {
2321
+ return isRecord(value) && typeof value.status === 'string' ? value.status : undefined;
2322
+ }
2323
+ function hasFlag(argv, flag) {
2324
+ return argv.includes(flag);
2325
+ }
2326
+ function readPositiveInteger(value, label) {
2327
+ const parsed = Number.parseInt(value, 10);
2328
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2329
+ throw new Error(`${label} must be a positive integer.`);
2330
+ }
2331
+ return parsed;
2332
+ }
2333
+ function readOptionalHubSpotAppId(value) {
2334
+ if (!value)
2335
+ return undefined;
2336
+ const parsed = readPositiveInteger(value, '--hubspot-app-id');
2337
+ return Schema.decodeSync(schemas.HubSpotAppId)(parsed);
2338
+ }
2339
+ function deployBundleKey(input) {
2340
+ return Schema.decodeSync(schemas.BundleKey)(`r2://bundles/${input.projectId}/${input.deployId}.tar.gz`);
2341
+ }
2342
+ async function requestLocalControlPlaneDeployPlan(request) {
2343
+ const controlPlane = (await loadCreateControlPlane())({
2344
+ now: () => new Date('2026-05-18T14:00:00.000Z'),
2345
+ });
2346
+ const headers = {
2347
+ 'content-type': 'application/json',
2348
+ ...(await controlPlaneAuthHeaders('local-cli-user')),
2349
+ };
2350
+ await seedLocalControlPlaneCredentials(controlPlane, request.accountId, headers);
2351
+ const response = await controlPlane.fetch(new Request('https://api.hs-x.dev/v1/deploys/plan', {
2352
+ method: 'POST',
2353
+ headers,
2354
+ body: JSON.stringify(request),
2355
+ }));
2356
+ const body = await response.json();
2357
+ if (!response.ok) {
2358
+ throw new Error(isRecord(body) && typeof body.message === 'string'
2359
+ ? body.message
2360
+ : `Local control-plane deploy plan failed with status ${response.status}`);
2361
+ }
2362
+ return Schema.decodeUnknownSync(schemas.DeployPlanResponse)(body);
2363
+ }
2364
+ async function requestHostedControlPlaneDeployPlan({ request, controlPlaneUrl, userId, }) {
2365
+ const response = await hostedHttp({
2366
+ url: new URL('/v1/deploys/plan', controlPlaneUrl),
2367
+ method: 'POST',
2368
+ headers: await controlPlaneAuthHeaders(userId),
2369
+ body: request,
2370
+ });
2371
+ const body = await response.json();
2372
+ if (!response.ok) {
2373
+ throw new Error(isRecord(body) && typeof body.message === 'string'
2374
+ ? body.message
2375
+ : `Control-plane deploy plan failed with status ${response.status}`);
2376
+ }
2377
+ return Schema.decodeUnknownSync(schemas.DeployPlanResponse)(body);
2378
+ }
2379
+ async function requestLocalControlPlaneDeployRecordAndMaybePromote({ request, plan, promoteWhenHealthy, }) {
2380
+ const controlPlane = (await loadCreateControlPlane())({
2381
+ now: () => new Date('2026-05-18T14:00:00.000Z'),
2382
+ });
2383
+ const headers = {
2384
+ 'content-type': 'application/json',
2385
+ ...(await controlPlaneAuthHeaders('local-cli-user')),
2386
+ };
2387
+ await seedLocalControlPlaneCredentials(controlPlane, request.accountId, headers);
2388
+ await controlPlane.fetch(new Request('https://api.hs-x.dev/v1/deploys/plan', {
2389
+ method: 'POST',
2390
+ headers,
2391
+ body: JSON.stringify(request),
2392
+ }));
2393
+ const response = await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/deploys/${plan.deployId}/record`, {
2394
+ method: 'POST',
2395
+ headers,
2396
+ body: JSON.stringify({
2397
+ accountId: request.accountId,
2398
+ projectId: request.projectId,
2399
+ manifestHash: plan.manifestHash,
2400
+ signedManifest: request.manifest,
2401
+ signature: `sig_${plan.deployId}`,
2402
+ bundleKey: deployBundleKey({ projectId: request.projectId, deployId: plan.deployId }),
2403
+ }),
2404
+ }));
2405
+ const body = await response.json();
2406
+ if (!response.ok) {
2407
+ throw new Error(isRecord(body) && typeof body.message === 'string'
2408
+ ? body.message
2409
+ : `Local control-plane deploy record failed with status ${response.status}`);
2410
+ }
2411
+ const record = Schema.decodeUnknownSync(schemas.DeployRecord)(body);
2412
+ if (!promoteWhenHealthy) {
2413
+ return { record };
2414
+ }
2415
+ const driftResponse = await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/projects/${request.projectId}/attestation`, {
2416
+ method: 'POST',
2417
+ headers,
2418
+ body: JSON.stringify({
2419
+ manifestHash: plan.manifestHash,
2420
+ bindingFingerprint: `bindings:${plan.deployId}`,
2421
+ sdkVersion: CLI_VERSION,
2422
+ taggedResourceCount: Math.max(1, plan.resources.workerNames.length),
2423
+ hsXAccountId: request.accountId,
2424
+ projectId: request.projectId,
2425
+ environment: 'dev',
2426
+ deployId: plan.deployId,
2427
+ timestamp: '2026-05-18T14:00:00.000Z',
2428
+ }),
2429
+ }));
2430
+ const driftBody = await driftResponse.json();
2431
+ if (!driftResponse.ok) {
2432
+ throw new Error(isRecord(driftBody) && typeof driftBody.message === 'string'
2433
+ ? driftBody.message
2434
+ : `Local control-plane attestation failed with status ${driftResponse.status}`);
2435
+ }
2436
+ const drift = Schema.decodeUnknownSync(schemas.DriftRead)(driftBody);
2437
+ if (drift.state !== 'healthy' || drift.deployId !== plan.deployId) {
2438
+ throw new Error(`Deploy ${plan.deployId} is not healthy enough to promote; drift state is ${drift.state}.`);
2439
+ }
2440
+ const promotionResponse = await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/deploys/${plan.deployId}/promote`, {
2441
+ method: 'POST',
2442
+ headers,
2443
+ body: JSON.stringify({ accountId: request.accountId, projectId: request.projectId }),
2444
+ }));
2445
+ const promotionBody = await promotionResponse.json();
2446
+ if (!promotionResponse.ok) {
2447
+ throw new Error(isRecord(promotionBody) && typeof promotionBody.message === 'string'
2448
+ ? promotionBody.message
2449
+ : `Local control-plane promotion failed with status ${promotionResponse.status}`);
2450
+ }
2451
+ return {
2452
+ record,
2453
+ drift,
2454
+ promotion: Schema.decodeUnknownSync(schemas.DeployPromotionResult)(promotionBody),
2455
+ };
2456
+ }
2457
+ async function requestHostedControlPlaneDeployRecordAndMaybePromote({ request, plan, controlPlaneUrl, userId, promoteWhenHealthy, promotionTimeoutMs, }) {
2458
+ const response = await hostedHttp({
2459
+ url: new URL(`/v1/deploys/${encodeURIComponent(plan.deployId)}/record`, controlPlaneUrl),
2460
+ method: 'POST',
2461
+ headers: await controlPlaneAuthHeaders(userId),
2462
+ body: {
2463
+ accountId: request.accountId,
2464
+ projectId: request.projectId,
2465
+ manifestHash: plan.manifestHash,
2466
+ signedManifest: request.manifest,
2467
+ signature: `sig_${plan.deployId}`,
2468
+ bundleKey: deployBundleKey({ projectId: request.projectId, deployId: plan.deployId }),
2469
+ },
2470
+ });
2471
+ const body = await response.json();
2472
+ if (!response.ok) {
2473
+ throw new Error(isRecord(body) && typeof body.message === 'string'
2474
+ ? body.message
2475
+ : `Control-plane deploy record failed with status ${response.status}`);
2476
+ }
2477
+ const record = Schema.decodeUnknownSync(schemas.DeployRecord)(body);
2478
+ if (!promoteWhenHealthy) {
2479
+ return { record };
2480
+ }
2481
+ const drift = await waitForHealthyControlPlaneDrift({
2482
+ controlPlaneUrl,
2483
+ projectId: request.projectId,
2484
+ deployId: plan.deployId,
2485
+ userId,
2486
+ timeoutMs: promotionTimeoutMs,
2487
+ });
2488
+ const promotion = await requestControlPlaneDeployPromotion({
2489
+ controlPlaneUrl,
2490
+ deployId: plan.deployId,
2491
+ accountId: request.accountId,
2492
+ projectId: request.projectId,
2493
+ userId,
2494
+ });
2495
+ return { record, drift, promotion };
2496
+ }
2497
+ async function seedLocalControlPlaneCredentials(controlPlane, accountId, headers) {
2498
+ await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/accounts/${accountId}/hubspot/connect`, {
2499
+ method: 'POST',
2500
+ headers,
2501
+ body: JSON.stringify({
2502
+ developerAccountId: `dev-${accountId}`,
2503
+ displayName: 'Local HubSpot Dev Account',
2504
+ authMethod: 'pak',
2505
+ personalAccessKey: 'pat-local-control-plane-redacted',
2506
+ }),
2507
+ }));
2508
+ await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/accounts/${accountId}/cloudflare/connect`, {
2509
+ method: 'POST',
2510
+ headers,
2511
+ body: JSON.stringify({
2512
+ cloudflareAccountId: `cf-${accountId}`,
2513
+ displayName: 'Local Cloudflare Account',
2514
+ authMethod: 'api-token',
2515
+ apiToken: 'cf-local-control-plane-redacted',
2516
+ }),
2517
+ }));
2518
+ }
2519
+ async function discoverWorkerManifests(root, options = {}) {
2520
+ const files = await collectFiles(join(root, 'src'));
2521
+ const workers = [];
2522
+ for (const file of files) {
2523
+ const source = await readFile(file, 'utf8');
2524
+ const workerName = /defineWorker\s*\(\s*["'`]([^"'`]+)["'`]/.exec(source)?.[1];
2525
+ if (!workerName) {
2526
+ continue;
2527
+ }
2528
+ const toolCapabilities = [
2529
+ ...source.matchAll(/(?:worker\.)?(?:tool|action)\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*\{([\s\S]*?)(?:async\s+)?handler\s*(?:[:(])/g),
2530
+ ].map((match) => ({
2531
+ kind: 'tool',
2532
+ id: match[1] ?? 'unknown',
2533
+ label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? match[1] ?? 'unknown',
2534
+ objectType: /objectType\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? 'unknown',
2535
+ ...objectFieldsProperty(match[2] ?? '', 'input'),
2536
+ ...objectFieldsProperty(match[2] ?? '', 'output'),
2537
+ runtimeNeeds: ['worker'],
2538
+ }));
2539
+ const cardBackendCapabilities = [
2540
+ ...source.matchAll(/(?:worker\.)?cardBackend\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*\{([\s\S]*?)(?:async\s+)?handler\s*(?:[:(])/g),
2541
+ ].map((match) => ({
2542
+ kind: 'card-backend',
2543
+ id: match[1] ?? 'unknown',
2544
+ label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? match[1] ?? 'unknown',
2545
+ runtimeNeeds: ['worker'],
2546
+ }));
2547
+ const sourceDefinitions = discoverSourceDefinitions(source);
2548
+ const syncCapabilities = [
2549
+ ...source.matchAll(/worker\.sync\s*\(\s*([^,]+)\s*,\s*\{([\s\S]*?)\}\s*\)/g),
2550
+ ].map((match) => {
2551
+ const sourceOrId = (match[1] ?? '').trim();
2552
+ const definition = match[2] ?? '';
2553
+ const id = /^["'`]([^"'`]+)["'`]$/.exec(sourceOrId)?.[1];
2554
+ const sourceDefinition = sourceDefinitions.get(sourceOrId);
2555
+ const sourceManifest = sourceDefinition?.kind === 'push'
2556
+ ? {
2557
+ kind: 'push',
2558
+ name: sourceDefinition.name,
2559
+ webhookPath: `/webhooks/${sourceDefinition.name}`,
2560
+ ...(sourceDefinition.auth ? { auth: { type: sourceDefinition.auth } } : {}),
2561
+ }
2562
+ : sourceDefinition?.kind === 'pull'
2563
+ ? {
2564
+ kind: 'pull',
2565
+ name: sourceDefinition.name,
2566
+ ...(sourceDefinition.auth ? { auth: { type: sourceDefinition.auth } } : {}),
2567
+ }
2568
+ : undefined;
2569
+ return {
2570
+ kind: 'sync',
2571
+ id: id ?? sourceManifest?.name ?? 'unknown',
2572
+ ...(/label\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1]
2573
+ ? { label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1] }
2574
+ : {}),
2575
+ schedule: /schedule\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1] ?? 'manual',
2576
+ ...stringProperty(definition, 'into'),
2577
+ ...schemaProperty(definition),
2578
+ manageSchema: options.noManageSchema ? false : discoverManageSchemaMode(definition),
2579
+ ...(sourceManifest ? { source: sourceManifest } : {}),
2580
+ runtimeNeeds: sourceManifest?.kind === 'push'
2581
+ ? ['worker', 'durable-object', 'queue']
2582
+ : ['worker', 'durable-object'],
2583
+ };
2584
+ });
2585
+ workers.push({
2586
+ name: workerName,
2587
+ capabilities: [...toolCapabilities, ...cardBackendCapabilities, ...syncCapabilities],
2588
+ });
2589
+ }
2590
+ return workers;
2591
+ }
2592
+ function stringProperty(source, propertyName) {
2593
+ const match = new RegExp(`${propertyName}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`).exec(source)?.[1];
2594
+ return match ? { [propertyName]: match } : {};
2595
+ }
2596
+ function schemaProperty(source) {
2597
+ const body = /schema\s*:\s*\{([\s\S]*?)\}\s*(?:,|$)/.exec(source)?.[1];
2598
+ if (!body) {
2599
+ return {};
2600
+ }
2601
+ const schema = {};
2602
+ for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*["'`]([^"'`]+)["'`]/g)) {
2603
+ if (match[1] && match[2]) {
2604
+ schema[match[1]] = match[2];
2605
+ }
2606
+ }
2607
+ return Object.keys(schema).length > 0 ? { schema } : {};
2608
+ }
2609
+ function objectFieldsProperty(source, propertyName) {
2610
+ const body = objectLiteralBody(source, propertyName);
2611
+ if (!body) {
2612
+ return {};
2613
+ }
2614
+ const fields = {};
2615
+ for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*\{([\s\S]*?)\}\s*,?/g)) {
2616
+ const fieldName = match[1];
2617
+ const definition = match[2] ?? '';
2618
+ if (!fieldName) {
2619
+ continue;
2620
+ }
2621
+ fields[fieldName] = {
2622
+ ...stringProperty(definition, 'type'),
2623
+ ...stringProperty(definition, 'label'),
2624
+ ...literalProperty(definition, 'default'),
2625
+ };
2626
+ }
2627
+ return Object.keys(fields).length > 0 ? { [propertyName]: fields } : {};
2628
+ }
2629
+ function objectLiteralBody(source, propertyName) {
2630
+ const start = new RegExp(`${propertyName}\\s*:\\s*\\{`).exec(source);
2631
+ if (!start) {
2632
+ return undefined;
2633
+ }
2634
+ let depth = 0;
2635
+ const bodyStart = start.index + start[0].length;
2636
+ for (let index = bodyStart; index < source.length; index += 1) {
2637
+ const char = source[index];
2638
+ if (char === '{')
2639
+ depth += 1;
2640
+ if (char === '}') {
2641
+ if (depth === 0) {
2642
+ return source.slice(bodyStart, index);
2643
+ }
2644
+ depth -= 1;
2645
+ }
2646
+ }
2647
+ return undefined;
2648
+ }
2649
+ function literalProperty(source, propertyName) {
2650
+ const match = new RegExp(`${propertyName}\\s*:\\s*([^,}\\n]+)`).exec(source)?.[1]?.trim();
2651
+ if (!match) {
2652
+ return {};
2653
+ }
2654
+ if (/^["'`]/.test(match)) {
2655
+ return stringProperty(source, propertyName);
2656
+ }
2657
+ const numeric = Number(match);
2658
+ if (Number.isFinite(numeric)) {
2659
+ return { [propertyName]: numeric };
2660
+ }
2661
+ if (match === 'true')
2662
+ return { [propertyName]: true };
2663
+ if (match === 'false')
2664
+ return { [propertyName]: false };
2665
+ return {};
2666
+ }
2667
+ function discoverManageSchemaMode(definition) {
2668
+ const literal = /manageSchema\s*:\s*["'`](properties|full)["'`]/.exec(definition)?.[1];
2669
+ if (literal === 'properties' || literal === 'full') {
2670
+ return literal;
2671
+ }
2672
+ return false;
2673
+ }
2674
+ function discoverSourceDefinitions(source) {
2675
+ const definitions = new Map();
2676
+ for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*defineSource(\.push)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
2677
+ const variableName = match[1];
2678
+ const body = match[3] ?? '';
2679
+ if (!variableName) {
2680
+ continue;
2681
+ }
2682
+ const name = /name\s*:\s*["'`]([^"'`]+)["'`]/.exec(body)?.[1] ?? variableName;
2683
+ const auth = /auth\s*:\s*\{\s*type\s*:\s*["'`](bearer|oauth2|basic|hmac)["'`]/.exec(body)?.[1];
2684
+ definitions.set(variableName, {
2685
+ kind: match[2] ? 'push' : 'pull',
2686
+ name,
2687
+ ...(auth ? { auth } : {}),
2688
+ });
2689
+ }
2690
+ return definitions;
2691
+ }
2692
+ async function collectFiles(root) {
2693
+ const files = [];
2694
+ async function walk(dir) {
2695
+ let entries;
2696
+ try {
2697
+ entries = await readdir(dir, { withFileTypes: true });
2698
+ }
2699
+ catch {
2700
+ return;
2701
+ }
2702
+ for (const entry of entries) {
2703
+ const path = join(dir, entry.name);
2704
+ if (entry.isDirectory()) {
2705
+ await walk(path);
2706
+ }
2707
+ else if (entry.isFile() && /\.(?:ts|tsx|js|jsx)$/.test(entry.name)) {
2708
+ files.push(path);
2709
+ }
2710
+ }
2711
+ }
2712
+ try {
2713
+ if ((await stat(root)).isDirectory()) {
2714
+ await walk(root);
2715
+ }
2716
+ }
2717
+ catch {
2718
+ return [];
2719
+ }
2720
+ return files.sort();
2721
+ }
2722
+ // as "flag not provided" so env-var fallbacks still apply.
2723
+ function isFlagValue(value) {
2724
+ return value !== undefined && value.length > 0 && !value.startsWith('-');
2725
+ }
2726
+ function resolveFlag(argv, flag) {
2727
+ const index = argv.indexOf(flag);
2728
+ if (index !== -1) {
2729
+ const next = argv[index + 1];
2730
+ if (isFlagValue(next))
2731
+ return next;
2732
+ }
2733
+ const prefix = `${flag}=`;
2734
+ const found = argv.find((arg) => arg.startsWith(prefix));
2735
+ if (!found)
2736
+ return undefined;
2737
+ const value = found.slice(prefix.length);
2738
+ return value.length > 0 ? value : undefined;
2739
+ }
2740
+ function resolveFlags(argv, flag) {
2741
+ const values = [];
2742
+ const prefix = `${flag}=`;
2743
+ for (let index = 0; index < argv.length; index += 1) {
2744
+ const arg = argv[index];
2745
+ if (arg === flag) {
2746
+ const next = argv[index + 1];
2747
+ if (isFlagValue(next)) {
2748
+ values.push(next);
2749
+ index += 1;
2750
+ }
2751
+ continue;
2752
+ }
2753
+ if (arg?.startsWith(prefix)) {
2754
+ const value = arg.slice(prefix.length);
2755
+ if (value.length > 0)
2756
+ values.push(value);
2757
+ }
2758
+ }
2759
+ return values;
2760
+ }
2761
+ function write(message) {
2762
+ process.stdout.write(message);
2763
+ }
2764
+ //# sourceMappingURL=deploy.js.map