@supatype/cli 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +208 -1
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/proxy-dev-app.d.ts +13 -0
  5. package/dist/app/proxy-dev-app.d.ts.map +1 -0
  6. package/dist/app/proxy-dev-app.js +53 -0
  7. package/dist/app/proxy-dev-app.js.map +1 -0
  8. package/dist/app-config.d.ts +7 -0
  9. package/dist/app-config.d.ts.map +1 -0
  10. package/dist/app-config.js +113 -0
  11. package/dist/app-config.js.map +1 -0
  12. package/dist/augmentation-generator.d.ts +2 -0
  13. package/dist/augmentation-generator.d.ts.map +1 -0
  14. package/dist/augmentation-generator.js +111 -0
  15. package/dist/augmentation-generator.js.map +1 -0
  16. package/dist/binary-cache.d.ts +94 -0
  17. package/dist/binary-cache.d.ts.map +1 -0
  18. package/dist/binary-cache.js +669 -0
  19. package/dist/binary-cache.js.map +1 -0
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +13 -7
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands/admin.d.ts.map +1 -1
  24. package/dist/commands/admin.js +4 -3
  25. package/dist/commands/admin.js.map +1 -1
  26. package/dist/commands/app.d.ts.map +1 -1
  27. package/dist/commands/app.js +56 -209
  28. package/dist/commands/app.js.map +1 -1
  29. package/dist/commands/cache.d.ts +6 -0
  30. package/dist/commands/cache.d.ts.map +1 -0
  31. package/dist/commands/cache.js +105 -0
  32. package/dist/commands/cache.js.map +1 -0
  33. package/dist/commands/cloud.d.ts +20 -0
  34. package/dist/commands/cloud.d.ts.map +1 -1
  35. package/dist/commands/cloud.js +50 -52
  36. package/dist/commands/cloud.js.map +1 -1
  37. package/dist/commands/db.d.ts.map +1 -1
  38. package/dist/commands/db.js +47 -54
  39. package/dist/commands/db.js.map +1 -1
  40. package/dist/commands/deploy.d.ts +2 -1
  41. package/dist/commands/deploy.d.ts.map +1 -1
  42. package/dist/commands/deploy.js +79 -52
  43. package/dist/commands/deploy.js.map +1 -1
  44. package/dist/commands/dev.d.ts +11 -0
  45. package/dist/commands/dev.d.ts.map +1 -1
  46. package/dist/commands/dev.js +759 -385
  47. package/dist/commands/dev.js.map +1 -1
  48. package/dist/commands/diff.d.ts.map +1 -1
  49. package/dist/commands/diff.js +30 -15
  50. package/dist/commands/diff.js.map +1 -1
  51. package/dist/commands/engine.d.ts +1 -3
  52. package/dist/commands/engine.d.ts.map +1 -1
  53. package/dist/commands/engine.js +13 -85
  54. package/dist/commands/engine.js.map +1 -1
  55. package/dist/commands/functions.d.ts.map +1 -1
  56. package/dist/commands/functions.js +92 -105
  57. package/dist/commands/functions.js.map +1 -1
  58. package/dist/commands/generate.d.ts.map +1 -1
  59. package/dist/commands/generate.js +22 -12
  60. package/dist/commands/generate.js.map +1 -1
  61. package/dist/commands/init.d.ts +1 -1
  62. package/dist/commands/init.d.ts.map +1 -1
  63. package/dist/commands/init.js +137 -410
  64. package/dist/commands/init.js.map +1 -1
  65. package/dist/commands/migrate-from-v1.d.ts +5 -0
  66. package/dist/commands/migrate-from-v1.d.ts.map +1 -0
  67. package/dist/commands/migrate-from-v1.js +125 -0
  68. package/dist/commands/migrate-from-v1.js.map +1 -0
  69. package/dist/commands/migrate.d.ts.map +1 -1
  70. package/dist/commands/migrate.js +27 -23
  71. package/dist/commands/migrate.js.map +1 -1
  72. package/dist/commands/pg.d.ts +8 -0
  73. package/dist/commands/pg.d.ts.map +1 -0
  74. package/dist/commands/pg.js +102 -0
  75. package/dist/commands/pg.js.map +1 -0
  76. package/dist/commands/pull.d.ts.map +1 -1
  77. package/dist/commands/pull.js +5 -66
  78. package/dist/commands/pull.js.map +1 -1
  79. package/dist/commands/push.d.ts.map +1 -1
  80. package/dist/commands/push.js +128 -38
  81. package/dist/commands/push.js.map +1 -1
  82. package/dist/commands/seed.d.ts +2 -0
  83. package/dist/commands/seed.d.ts.map +1 -1
  84. package/dist/commands/seed.js +44 -11
  85. package/dist/commands/seed.js.map +1 -1
  86. package/dist/commands/self-host.d.ts +7 -1
  87. package/dist/commands/self-host.d.ts.map +1 -1
  88. package/dist/commands/self-host.js +272 -758
  89. package/dist/commands/self-host.js.map +1 -1
  90. package/dist/commands/self-update.d.ts +9 -0
  91. package/dist/commands/self-update.d.ts.map +1 -0
  92. package/dist/commands/self-update.js +33 -0
  93. package/dist/commands/self-update.js.map +1 -0
  94. package/dist/commands/status.d.ts.map +1 -1
  95. package/dist/commands/status.js +4 -3
  96. package/dist/commands/status.js.map +1 -1
  97. package/dist/commands/types.d.ts +3 -0
  98. package/dist/commands/types.d.ts.map +1 -0
  99. package/dist/commands/types.js +62 -0
  100. package/dist/commands/types.js.map +1 -0
  101. package/dist/commands/update.d.ts +7 -0
  102. package/dist/commands/update.d.ts.map +1 -0
  103. package/dist/commands/update.js +93 -0
  104. package/dist/commands/update.js.map +1 -0
  105. package/dist/components.d.ts +5 -0
  106. package/dist/components.d.ts.map +1 -0
  107. package/dist/components.js +3 -0
  108. package/dist/components.js.map +1 -0
  109. package/dist/config.d.ts +10 -51
  110. package/dist/config.d.ts.map +1 -1
  111. package/dist/config.js +101 -33
  112. package/dist/config.js.map +1 -1
  113. package/dist/dev-compose.d.ts +17 -0
  114. package/dist/dev-compose.d.ts.map +1 -0
  115. package/dist/dev-compose.js +374 -0
  116. package/dist/dev-compose.js.map +1 -0
  117. package/dist/diff-output.d.ts +4 -0
  118. package/dist/diff-output.d.ts.map +1 -0
  119. package/dist/diff-output.js +12 -0
  120. package/dist/diff-output.js.map +1 -0
  121. package/dist/docker-postgres.d.ts +57 -0
  122. package/dist/docker-postgres.d.ts.map +1 -0
  123. package/dist/docker-postgres.js +208 -0
  124. package/dist/docker-postgres.js.map +1 -0
  125. package/dist/engine-client.d.ts +69 -0
  126. package/dist/engine-client.d.ts.map +1 -0
  127. package/dist/engine-client.js +157 -0
  128. package/dist/engine-client.js.map +1 -0
  129. package/dist/ensure-binary.d.ts +7 -0
  130. package/dist/ensure-binary.d.ts.map +1 -0
  131. package/dist/ensure-binary.js +17 -0
  132. package/dist/ensure-binary.js.map +1 -0
  133. package/dist/functions-router-gen.d.ts +14 -0
  134. package/dist/functions-router-gen.d.ts.map +1 -0
  135. package/dist/functions-router-gen.js +199 -0
  136. package/dist/functions-router-gen.js.map +1 -0
  137. package/dist/index.d.ts +4 -5
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +2 -3
  140. package/dist/index.js.map +1 -1
  141. package/dist/kong-config.d.ts +25 -0
  142. package/dist/kong-config.d.ts.map +1 -0
  143. package/dist/kong-config.js +71 -0
  144. package/dist/kong-config.js.map +1 -0
  145. package/dist/local-gateway.d.ts +7 -0
  146. package/dist/local-gateway.d.ts.map +1 -0
  147. package/dist/local-gateway.js +9 -0
  148. package/dist/local-gateway.js.map +1 -0
  149. package/dist/local-storage.d.ts +8 -0
  150. package/dist/local-storage.d.ts.map +1 -0
  151. package/dist/local-storage.js +14 -0
  152. package/dist/local-storage.js.map +1 -0
  153. package/dist/pgbouncer-userlist.d.ts +5 -0
  154. package/dist/pgbouncer-userlist.d.ts.map +1 -0
  155. package/dist/pgbouncer-userlist.js +14 -0
  156. package/dist/pgbouncer-userlist.js.map +1 -0
  157. package/dist/postgres-ctl.d.ts +44 -0
  158. package/dist/postgres-ctl.d.ts.map +1 -0
  159. package/dist/postgres-ctl.js +137 -0
  160. package/dist/postgres-ctl.js.map +1 -0
  161. package/dist/process-manager.d.ts +43 -0
  162. package/dist/process-manager.d.ts.map +1 -0
  163. package/dist/process-manager.js +135 -0
  164. package/dist/process-manager.js.map +1 -0
  165. package/dist/project-config.d.ts +235 -0
  166. package/dist/project-config.d.ts.map +1 -0
  167. package/dist/project-config.js +160 -0
  168. package/dist/project-config.js.map +1 -0
  169. package/dist/pull-utils.d.ts +15 -0
  170. package/dist/pull-utils.d.ts.map +1 -1
  171. package/dist/pull-utils.js +12 -0
  172. package/dist/pull-utils.js.map +1 -1
  173. package/dist/release-pins.d.ts +7 -0
  174. package/dist/release-pins.d.ts.map +1 -0
  175. package/dist/release-pins.js +27 -0
  176. package/dist/release-pins.js.map +1 -0
  177. package/dist/release-public-key.d.ts +8 -0
  178. package/dist/release-public-key.d.ts.map +1 -0
  179. package/dist/release-public-key.js +13 -0
  180. package/dist/release-public-key.js.map +1 -0
  181. package/dist/runtime-routes.d.ts +34 -0
  182. package/dist/runtime-routes.d.ts.map +1 -0
  183. package/dist/runtime-routes.js +252 -0
  184. package/dist/runtime-routes.js.map +1 -0
  185. package/dist/schema-ast-v2.d.ts +127 -0
  186. package/dist/schema-ast-v2.d.ts.map +1 -0
  187. package/dist/schema-ast-v2.js +226 -0
  188. package/dist/schema-ast-v2.js.map +1 -0
  189. package/dist/scripts/postinstall.d.ts +5 -6
  190. package/dist/scripts/postinstall.d.ts.map +1 -1
  191. package/dist/scripts/postinstall.js +36 -20
  192. package/dist/scripts/postinstall.js.map +1 -1
  193. package/dist/self-host-compose.d.ts +22 -0
  194. package/dist/self-host-compose.d.ts.map +1 -0
  195. package/dist/self-host-compose.js +347 -0
  196. package/dist/self-host-compose.js.map +1 -0
  197. package/dist/storage-provision.d.ts +24 -0
  198. package/dist/storage-provision.d.ts.map +1 -0
  199. package/dist/storage-provision.js +44 -0
  200. package/dist/storage-provision.js.map +1 -0
  201. package/dist/studio-admin-roles.d.ts +7 -0
  202. package/dist/studio-admin-roles.d.ts.map +1 -0
  203. package/dist/studio-admin-roles.js +14 -0
  204. package/dist/studio-admin-roles.js.map +1 -0
  205. package/dist/studio-dev-server.d.ts +22 -0
  206. package/dist/studio-dev-server.d.ts.map +1 -0
  207. package/dist/studio-dev-server.js +28 -0
  208. package/dist/studio-dev-server.js.map +1 -0
  209. package/dist/systemd.d.ts +26 -0
  210. package/dist/systemd.d.ts.map +1 -0
  211. package/dist/systemd.js +102 -0
  212. package/dist/systemd.js.map +1 -0
  213. package/dist/tsx-runner.d.ts.map +1 -1
  214. package/dist/tsx-runner.js +9 -2
  215. package/dist/tsx-runner.js.map +1 -1
  216. package/dist/type-extractor.d.ts +4 -0
  217. package/dist/type-extractor.d.ts.map +1 -0
  218. package/dist/type-extractor.js +1213 -0
  219. package/dist/type-extractor.js.map +1 -0
  220. package/dist/type-resolver.d.ts +33 -0
  221. package/dist/type-resolver.d.ts.map +1 -0
  222. package/dist/type-resolver.js +338 -0
  223. package/dist/type-resolver.js.map +1 -0
  224. package/package.json +4 -3
  225. package/releases/deno/VERSION +1 -0
  226. package/scripts/mirror-deno-release.sh +76 -0
  227. package/src/TYPE-RESOLUTION.md +294 -0
  228. package/src/app/proxy-dev-app.ts +67 -0
  229. package/src/app-config.ts +128 -0
  230. package/src/augmentation-generator.ts +126 -0
  231. package/src/binary-cache.ts +822 -0
  232. package/src/cli.ts +13 -8
  233. package/src/commands/admin.ts +4 -3
  234. package/src/commands/app.ts +67 -231
  235. package/src/commands/cache.ts +117 -0
  236. package/src/commands/cloud.ts +63 -64
  237. package/src/commands/db.ts +54 -63
  238. package/src/commands/deploy.ts +96 -62
  239. package/src/commands/dev.ts +933 -405
  240. package/src/commands/diff.ts +31 -29
  241. package/src/commands/engine.ts +13 -116
  242. package/src/commands/functions.ts +97 -115
  243. package/src/commands/generate.ts +23 -10
  244. package/src/commands/init.ts +149 -414
  245. package/src/commands/migrate-from-v1.ts +131 -0
  246. package/src/commands/migrate.ts +27 -23
  247. package/src/commands/pg.ts +133 -0
  248. package/src/commands/pull.ts +6 -85
  249. package/src/commands/push.ts +161 -56
  250. package/src/commands/seed.ts +54 -12
  251. package/src/commands/self-host.ts +312 -880
  252. package/src/commands/self-update.ts +45 -0
  253. package/src/commands/status.ts +4 -3
  254. package/src/commands/types.ts +76 -0
  255. package/src/commands/update.ts +109 -0
  256. package/src/components.ts +6 -0
  257. package/src/config.ts +127 -94
  258. package/src/dev-compose.ts +455 -0
  259. package/src/diff-output.ts +12 -0
  260. package/src/docker-postgres.ts +295 -0
  261. package/src/engine-client.ts +236 -0
  262. package/src/ensure-binary.ts +28 -0
  263. package/src/functions-router-gen.ts +224 -0
  264. package/src/index.ts +4 -12
  265. package/src/kong-config.ts +93 -0
  266. package/src/local-gateway.ts +9 -0
  267. package/src/local-storage.ts +14 -0
  268. package/src/pgbouncer-userlist.ts +15 -0
  269. package/src/postgres-ctl.ts +171 -0
  270. package/src/process-manager.ts +168 -0
  271. package/src/project-config.ts +386 -0
  272. package/src/pull-utils.ts +24 -0
  273. package/src/release-pins.ts +31 -0
  274. package/src/release-public-key.ts +12 -0
  275. package/src/runtime-routes.ts +291 -0
  276. package/src/schema-ast-v2.ts +324 -0
  277. package/src/scripts/postinstall.ts +36 -25
  278. package/src/self-host-compose.ts +389 -0
  279. package/src/storage-provision.ts +58 -0
  280. package/src/studio-admin-roles.ts +16 -0
  281. package/src/studio-dev-server.ts +53 -0
  282. package/src/systemd.ts +137 -0
  283. package/src/tsx-runner.ts +11 -1
  284. package/src/type-extractor.ts +1479 -0
  285. package/src/type-resolver.ts +457 -0
  286. package/tests/app-command.test.ts +54 -0
  287. package/tests/augmentation-generator.test.ts +59 -0
  288. package/tests/binary-cache-cloud-overrides.test.ts +123 -0
  289. package/tests/cached-artifact-format.test.ts +84 -0
  290. package/tests/cli-help.test.ts +40 -14
  291. package/tests/config.test.ts +171 -37
  292. package/tests/docker-postgres.test.ts +39 -0
  293. package/tests/engine-distribution.test.ts +3 -3
  294. package/tests/ensure-binary.test.ts +59 -0
  295. package/tests/init.test.ts +28 -86
  296. package/tests/migrate-from-v1.test.ts +29 -0
  297. package/tests/normalize-admin-config.test.ts +48 -0
  298. package/tests/pg-spawn-env.test.ts +18 -0
  299. package/tests/postgres-archive-tag.test.ts +9 -0
  300. package/tests/proxy-dev-app.test.ts +33 -0
  301. package/tests/pull-utils.test.ts +36 -1
  302. package/tests/release-pins.test.ts +28 -0
  303. package/tests/runtime-contract.test.ts +351 -0
  304. package/tests/seed-discover.test.ts +31 -0
  305. package/tests/studio-admin-roles.test.ts +27 -0
  306. package/tests/tsconfig.json +9 -0
  307. package/tests/type-extractor.test.ts +985 -0
  308. package/tests/type-resolver.test.ts +59 -0
  309. package/tsconfig.tsbuildinfo +1 -1
  310. package/vitest.config.ts +12 -0
  311. package/dist/engine/cache.d.ts +0 -37
  312. package/dist/engine/cache.d.ts.map +0 -1
  313. package/dist/engine/cache.js +0 -121
  314. package/dist/engine/cache.js.map +0 -1
  315. package/dist/engine/download.d.ts +0 -19
  316. package/dist/engine/download.d.ts.map +0 -1
  317. package/dist/engine/download.js +0 -108
  318. package/dist/engine/download.js.map +0 -1
  319. package/dist/engine/platform.d.ts +0 -24
  320. package/dist/engine/platform.d.ts.map +0 -1
  321. package/dist/engine/platform.js +0 -50
  322. package/dist/engine/platform.js.map +0 -1
  323. package/dist/engine/resolve.d.ts +0 -37
  324. package/dist/engine/resolve.d.ts.map +0 -1
  325. package/dist/engine/resolve.js +0 -133
  326. package/dist/engine/resolve.js.map +0 -1
  327. package/dist/engine/update-notify.d.ts +0 -11
  328. package/dist/engine/update-notify.d.ts.map +0 -1
  329. package/dist/engine/update-notify.js +0 -43
  330. package/dist/engine/update-notify.js.map +0 -1
  331. package/dist/engine/verify.d.ts +0 -50
  332. package/dist/engine/verify.d.ts.map +0 -1
  333. package/dist/engine/verify.js +0 -161
  334. package/dist/engine/verify.js.map +0 -1
  335. package/dist/engine-version.d.ts +0 -35
  336. package/dist/engine-version.d.ts.map +0 -1
  337. package/dist/engine-version.js +0 -35
  338. package/dist/engine-version.js.map +0 -1
  339. package/dist/engine.d.ts +0 -34
  340. package/dist/engine.d.ts.map +0 -1
  341. package/dist/engine.js +0 -76
  342. package/dist/engine.js.map +0 -1
  343. package/src/engine/cache.ts +0 -135
  344. package/src/engine/download.ts +0 -143
  345. package/src/engine/platform.ts +0 -66
  346. package/src/engine/resolve.ts +0 -197
  347. package/src/engine/update-notify.ts +0 -50
  348. package/src/engine/verify.ts +0 -206
  349. package/src/engine-version.ts +0 -39
  350. package/src/engine.ts +0 -99
@@ -1,477 +1,1005 @@
1
+ /**
2
+ * supatype dev — start local Postgres, apply schema, run supatype-server.
3
+ *
4
+ * Runtime provider (top-level `provider` or legacy `database.provider`):
5
+ * native — host Postgres + host server + host engine (default)
6
+ * docker — full self-host Compose stack (Kong :18473); see dev-compose.ts
7
+ *
8
+ * Edge functions (when a functions/ dir exists): Deno is resolved from the CDN cache
9
+ * (auto-download on miss). Self-host/cloud Docker stacks use supatype-server in-container;
10
+ * Deno is not provisioned by the CLI on those paths.
11
+ */
12
+
1
13
  import type { Command } from "commander"
2
- import { spawnSync, spawn, type ChildProcess } from "node:child_process"
14
+ import { spawnSync } from "node:child_process"
3
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
4
- import { resolve } from "node:path"
16
+ import { homedir } from "node:os"
17
+ import { isAbsolute, join, relative, resolve } from "node:path"
5
18
  import { loadConfig } from "../config.js"
6
- import { ensureEngine, invokeEngine } from "../engine.js"
7
-
8
- const POSTGREST_URL = "http://localhost:3000"
9
- const HEALTH_TIMEOUT_MS = 60_000
10
- const HEALTH_POLL_MS = 2_000
19
+ import {
20
+ functionsPathCandidatesFromProject,
21
+ resolveRuntimeProvider,
22
+ schemaPathFromProject,
23
+ type SupatypeProjectConfig,
24
+ } from "../project-config.js"
25
+ import { discoverTsFunctionsInDir, writeDevFunctionsRouter } from "../functions-router-gen.js"
26
+ import { signJwt } from "../jwt.js"
27
+ import {
28
+ normalisePlatformPath,
29
+ cachePath,
30
+ currentPlatform,
31
+ hasMeaningfulOverrides,
32
+ describeActiveOverrides,
33
+ postgresArchiveTag,
34
+ } from "../binary-cache.js"
35
+ import { ensureBinary } from "../ensure-binary.js"
36
+ import { startProxyDevApp } from "../app/proxy-dev-app.js"
37
+ import { ProcessManager } from "../process-manager.js"
38
+ import { startStudioViteDevServer } from "../studio-dev-server.js"
39
+ import { localStorageEnv } from "../local-storage.js"
40
+ import {
41
+ initdb,
42
+ start as pgStart,
43
+ stop as pgStop,
44
+ waitReady as pgWaitReady,
45
+ isPortInUse,
46
+ pgSpawnEnv,
47
+ } from "../postgres-ctl.js"
48
+ /** Map `email.smtp` from supatype.config.ts into GOTRUE_SMTP_* for the embedded GoTrue process. */
49
+ function gotrueSMTPFromEmailConfig(email: SupatypeProjectConfig["email"] | undefined): Record<string, string> {
50
+ const s = email?.smtp
51
+ if (!s) return {}
52
+ const out: Record<string, string> = {}
53
+ const host = s.host?.trim()
54
+ if (host) out.GOTRUE_SMTP_HOST = host
55
+ if (s.port !== undefined) out.GOTRUE_SMTP_PORT = String(s.port)
56
+ const user = s.user?.trim()
57
+ if (user) out.GOTRUE_SMTP_USER = user
58
+ if (s.pass !== undefined && s.pass !== "") out.GOTRUE_SMTP_PASS = s.pass
59
+ const admin = s.admin_email?.trim()
60
+ if (admin) out.GOTRUE_SMTP_ADMIN_EMAIL = admin
61
+ const sender = s.sender_name?.trim()
62
+ if (sender) out.GOTRUE_SMTP_SENDER_NAME = sender
63
+ return out
64
+ }
11
65
 
12
66
  export function registerDev(program: Command): void {
13
67
  program
14
68
  .command("dev")
15
- .description(
16
- "Start local Postgres, PostgREST, and Kong via Docker Compose, then watch for schema changes",
17
- )
69
+ .description("Start local Postgres, apply schema, and run supatype-server")
18
70
  .option("--no-watch", "Start services but do not watch for schema changes")
19
- .option("--local", "Run storage, realtime, and studio from source (monorepo dev)")
20
- .action(async (opts: { watch: boolean; local: boolean }) => {
71
+ .option("--port <port>", "Port for supatype-server (overrides config)", String)
72
+ .action(async (opts: { watch: boolean; port?: string }) => {
21
73
  const cwd = process.cwd()
22
74
 
23
- // Generate .env with local defaults if missing
24
- ensureDevEnv(cwd)
25
-
26
- if (opts.local) {
27
- // Monorepo dev generate an infra-only compose if needed, then start
28
- const composePath = resolve(cwd, "docker-compose.yml")
29
- if (!existsSync(composePath)) {
30
- ensureInfraCompose(cwd)
75
+ // ── 1. Load project config ─────────────────────────────────────────────
76
+ const config = loadConfig(cwd)
77
+ if (hasMeaningfulOverrides(config)) {
78
+ console.warn("[supatype] Local binary overrides active:")
79
+ for (const line of describeActiveOverrides(config)) {
80
+ console.warn(line)
31
81
  }
32
- console.log("Starting infra services...")
33
- const up = spawnSync(
34
- "docker",
35
- ["compose", "up", "-d", "--wait", "db", "pgbouncer", "gotrue", "postgrest", "minio", "kong"],
36
- { cwd, stdio: "inherit" },
82
+ console.warn("")
83
+ }
84
+ const projectName = config.project.name
85
+ const serverPort = opts.port ?? String(config.server.port ?? 54321)
86
+ const postgrestPort = String(config.server.postgrestPort ?? 3001)
87
+ const provider = resolveRuntimeProvider(config)
88
+
89
+ if (provider === "docker") {
90
+ const { runDevCompose } = await import("../dev-compose.js")
91
+ await runDevCompose(cwd, config, { watch: opts.watch !== false })
92
+ return
93
+ }
94
+
95
+ // ── 2. Resolve engine + server binaries ──────────────────────────────
96
+ console.log(`[supatype] Resolving component binaries for "${projectName}"...`)
97
+ const [engineBin, serverBin] = await Promise.all([
98
+ ensureBinary("engine", config),
99
+ ensureBinary("server", config),
100
+ ])
101
+
102
+ // ── 3. Per-project state directories ─────────────────────────────────
103
+ const stateRoot = join(homedir(), ".supatype", "projects", projectName)
104
+ const pidDir = join(stateRoot, "pid")
105
+ const logsDir = join(stateRoot, "logs")
106
+ const tmpDir = join(stateRoot, "tmp")
107
+
108
+ for (const d of [pidDir, logsDir, tmpDir]) {
109
+ mkdirSync(d, { recursive: true })
110
+ }
111
+
112
+ // ── 4. Port collision check ───────────────────────────────────────────
113
+ const pgPort = 5432
114
+ if (await isPortInUse(pgPort)) {
115
+ console.error(
116
+ `[supatype] Port ${pgPort} is already in use. Another Postgres instance may be running.\n` +
117
+ ` Check: lsof -i :${pgPort}`,
118
+ )
119
+ process.exit(1)
120
+ }
121
+ if (await isPortInUse(Number(serverPort))) {
122
+ console.error(
123
+ `[supatype] Port ${serverPort} is already in use. Another supatype-server may be running.\n` +
124
+ ` Check: lsof -i :${serverPort}`,
37
125
  )
38
- if (up.status !== 0) {
39
- console.error("docker compose up failed.")
40
- process.exit(1)
126
+ process.exit(1)
127
+ }
128
+ if (await isPortInUse(Number(postgrestPort))) {
129
+ console.error(
130
+ `[supatype] Port ${postgrestPort} is already in use. Another service may be running.\n` +
131
+ ` Check: lsof -i :${postgrestPort}`,
132
+ )
133
+ process.exit(1)
134
+ }
135
+
136
+ // ── 5–7. Start Postgres ───────────────────────────────────────────────
137
+ let dbURL: string
138
+ let stopPostgres: () => void | Promise<void>
139
+ const pgPassword = "postgres"
140
+ // pgBinDir is set on the native path and used to add DLL search path for
141
+ // PostgREST on Windows (PostgREST links against libpq + SSL from MinGW).
142
+ let pgBinDir: string | null = null
143
+
144
+ {
145
+ // native — resolve pg bin dir and manage with pg_ctl
146
+ pgBinDir = await resolvePgBinDir(config)
147
+ const dataDir = config.database.data_dir ?? join(stateRoot, "data")
148
+ mkdirSync(dataDir, { recursive: true })
149
+ const pgOpts = { pgBinDir, dataDir, port: pgPort, logPath: join(logsDir, "postgres.log") }
150
+
151
+ console.log("[supatype] Initialising Postgres data directory...")
152
+ initdb(pgOpts)
153
+ console.log("[supatype] Starting Postgres...")
154
+ pgStart(pgOpts)
155
+ await pgWaitReady(pgOpts, 15_000)
156
+ console.log("[supatype] Postgres is ready.")
157
+ dbURL = `postgres://postgres:postgres@127.0.0.1:${pgPort}/${projectName}?sslmode=disable`
158
+ stopPostgres = () => pgStop(pgOpts)
159
+
160
+ // Create project database if it doesn't exist (native only).
161
+ const psqlBin = join(pgBinDir, process.platform === "win32" ? "psql.exe" : "psql")
162
+ const createdbBin = join(pgBinDir, process.platform === "win32" ? "createdb.exe" : "createdb")
163
+ const pgConnArgs = ["-h", "127.0.0.1", "-p", String(pgPort), "-U", "postgres"]
164
+ const pgEnv = pgSpawnEnv(pgBinDir)
165
+ const createDbResult = spawnSync(
166
+ createdbBin,
167
+ [...pgConnArgs, projectName],
168
+ { stdio: "pipe", encoding: "utf8", env: pgEnv },
169
+ )
170
+ if (createDbResult.status !== 0) {
171
+ const stderr = createDbResult.stderr ?? ""
172
+ if (!stderr.includes("already exists")) {
173
+ throw new Error(`Failed to create database "${projectName}": ${stderr}`)
174
+ }
175
+ } else {
176
+ console.log(`[supatype] Created database "${projectName}".`)
41
177
  }
42
- } else {
43
- if (!existsSync(resolve(cwd, "docker-compose.yml"))) {
44
- console.error(
45
- "docker-compose.yml not found. Run: supatype init",
178
+
179
+ // Create roles required by PostgREST and grant them to postgres so
180
+ // PostgREST can SET ROLE when processing requests.
181
+ // anon – unauthenticated requests (RLS enforced)
182
+ // authenticated – signed-in user requests (RLS enforced)
183
+ // service_role – developer/admin bypass (BYPASSRLS)
184
+ const rolesSql = `
185
+ CREATE SCHEMA IF NOT EXISTS auth;
186
+ DO $$ BEGIN
187
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon')
188
+ THEN CREATE ROLE anon NOLOGIN; END IF;
189
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated')
190
+ THEN CREATE ROLE authenticated NOLOGIN; END IF;
191
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'service_role')
192
+ THEN CREATE ROLE service_role NOLOGIN BYPASSRLS; END IF;
193
+ END $$;
194
+ GRANT anon, authenticated, service_role TO postgres;
195
+ GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
196
+ -- Table-level privileges (RLS restricts rows; roles still need table access)
197
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;
198
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO authenticated;
199
+ GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
200
+ GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO authenticated, service_role;
201
+ -- Default privileges so tables created by the engine push inherit these grants
202
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO anon;
203
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO authenticated;
204
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO service_role;
205
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated, service_role;
206
+ `
207
+ spawnSync(psqlBin, [...pgConnArgs, "-d", projectName, "-c", rolesSql],
208
+ { stdio: "pipe", encoding: "utf8", env: pgEnv })
209
+ }
210
+
211
+ const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
212
+ const authDbURL = dbURL.includes("?")
213
+ ? `${dbURL}&search_path=auth`
214
+ : `${dbURL}?search_path=auth`
215
+
216
+ // ── 8. GoTrue migrations (auth.users before engine studio SQL) ─────────
217
+ console.log("[supatype] Running GoTrue migrations...")
218
+ const migrateEnv = gotrueMigrateEnv(serverPort, dbURL, LOCAL_JWT_SECRET)
219
+ runGotrueMigrations(serverBin, migrateEnv)
220
+
221
+ // ── 9. Engine: apply schema ───────────────────────────────────────────
222
+ const schemaPath = schemaPathFromProject(config, cwd)
223
+ const supatypeDir = join(cwd, ".supatype")
224
+ const manifestPath = join(supatypeDir, "manifest.json")
225
+ const adminConfigPath = join(supatypeDir, "admin-config.json")
226
+ mkdirSync(supatypeDir, { recursive: true })
227
+
228
+ const localStoragePath = config.storage?.provider !== "s3" ? join(stateRoot, "storage") : undefined
229
+ // Native Postgres builds don't include PostGIS — skip geo fields rather than failing.
230
+ const skipFieldKinds: ReadonlySet<string> = new Set(["geo", "vector"])
231
+
232
+ await runSchemaPush(cwd, engineBin, schemaPath, dbURL, manifestPath, adminConfigPath, localStoragePath, skipFieldKinds, config).catch(
233
+ (e: unknown) => console.error("[supatype] Initial schema push failed:", (e as Error).message),
234
+ )
235
+
236
+ // ── 10. Spawn supatype-server ─────────────────────────────────────────
237
+
238
+ // Resolve edge functions config: only enable Deno if a functions dir exists.
239
+ const functionsDir = functionsPathCandidatesFromProject(config, cwd).find(dir => existsSync(dir))
240
+ const hasFunctionsDir = functionsDir !== undefined
241
+ /** Always set when a functions dir exists so Studio admin API can list functions; Deno runtime is separate. */
242
+ const denoFunctionsDir = hasFunctionsDir ? functionsDir : ""
243
+ const functionRoutes = hasFunctionsDir && functionsDir !== undefined
244
+ ? discoverTsFunctionsInDir(functionsDir)
245
+ : []
246
+ const denoServeScriptAbs = hasFunctionsDir && functionsDir !== undefined
247
+ ? (writeDevFunctionsRouter(cwd, functionsDir, functionRoutes) ?? "")
248
+ : ""
249
+
250
+ let denoBinPath: string | undefined
251
+ if (hasFunctionsDir) {
252
+ console.log(`[supatype] Edge functions enabled (${functionsDir})`)
253
+ try {
254
+ denoBinPath = await ensureBinary("deno", config)
255
+ console.log(`[supatype] Deno runtime: ${denoBinPath} (v${config.versions.deno})`)
256
+ if (functionRoutes.length > 0) {
257
+ console.log(
258
+ `[supatype] Edge functions router: ${relative(cwd, denoServeScriptAbs) || ".supatype/functions-router.ts"} ` +
259
+ `(${functionRoutes.length} function(s): ${functionRoutes.map(fn => fn.name).join(", ")})`,
260
+ )
261
+ } else {
262
+ console.log("[supatype] Edge functions router not generated (no handler files discovered yet)")
263
+ }
264
+ } catch (err) {
265
+ console.warn(
266
+ `[supatype] ⚠ Found ${functionsDir} but could not provision Deno v${config.versions.deno} — edge functions will not run.\n` +
267
+ ` ${(err as Error).message}\n` +
268
+ " (Functions still appear in Studio; invocations need Deno.)",
46
269
  )
47
- process.exit(1)
48
- }
49
- console.log("Starting services...")
50
- const up = spawnSync(
51
- "docker",
52
- ["compose", "up", "-d", "--wait"],
53
- { cwd, stdio: "inherit" },
54
- )
55
- if (up.status !== 0) {
56
- console.error("docker compose up failed.")
57
- process.exit(1)
58
270
  }
59
271
  }
60
272
 
61
- console.log("Waiting for PostgREST to be ready...")
62
- await waitForPostgREST()
273
+ // Matches GOTRUE_HOOK_SEND_EMAIL_SECRETS symmetric format (dev only). Override via .env.
274
+ const LOCAL_SEND_EMAIL_HOOK_SECRETS =
275
+ "v1,whsec_abcdefghijklmnopqrstuvwxyz01234567"
276
+ const now = Math.floor(Date.now() / 1000)
277
+ const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
278
+ const anonKey = signJwt({ ...jwtBase, role: "anon" }, LOCAL_JWT_SECRET)
279
+ const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, LOCAL_JWT_SECRET)
280
+
281
+
282
+ const emailProvider = config.email?.provider ?? "console"
283
+ const gotrueMailerProvider =
284
+ emailProvider === "console"
285
+ ? "console"
286
+ : emailProvider === "resend"
287
+ ? "resend"
288
+ : emailProvider === "ses"
289
+ ? "ses"
290
+ : "smtp"
291
+
292
+ const serverEnv: Record<string, string> = {
293
+ // supatype-server outer layer
294
+ SUPATYPE_MODE: config.server.mode ?? "dev",
295
+ SUPATYPE_MANIFEST_PATH: manifestPath,
296
+ SUPATYPE_ADMIN_CONFIG_PATH: adminConfigPath,
297
+ SUPATYPE_POSTGREST_URL: `http://127.0.0.1:${postgrestPort}`,
298
+ SUPATYPE_DENO_FUNCTIONS_DIR: denoFunctionsDir,
299
+ ...(denoFunctionsDir !== "" ? { SUPATYPE_SHARED_ENV_FILE: resolve(denoFunctionsDir, ".env.local") } : {}),
300
+ ...(denoBinPath !== undefined ? { SUPATYPE_DENO_PATH: denoBinPath } : {}),
301
+ ...(denoServeScriptAbs !== ""
302
+ ? { SUPATYPE_DENO_SERVE_SCRIPT: denoServeScriptAbs }
303
+ : {}),
304
+ SUPATYPE_URL: `http://localhost:${serverPort}`,
305
+ SUPATYPE_ANON_KEY: anonKey,
306
+ SUPATYPE_SERVICE_ROLE_KEY: serviceRoleKey,
307
+ PORT: serverPort,
308
+ SUPATYPE_APP_MODE: config.app.mode ?? "none",
309
+ ...(config.app.mode === "static" && config.app.static_dir?.trim()
310
+ ? { SUPATYPE_APP_STATIC_DIR: resolve(cwd, config.app.static_dir.trim()) }
311
+ : {}),
312
+ ...(config.app.mode === "proxy" && config.app.upstream?.trim()
313
+ ? { SUPATYPE_APP_UPSTREAM: config.app.upstream.trim() }
314
+ : {}),
315
+ ...(config.app.vite_dev_url !== undefined && config.app.vite_dev_url.trim() !== ""
316
+ ? { SUPATYPE_VITE_DEV_URL: config.app.vite_dev_url.trim() }
317
+ : {}),
318
+ // GoTrue required fields (sensible local-dev defaults)
319
+ GOTRUE_DB_DATABASE_URL: authDbURL,
320
+ DATABASE_URL: authDbURL,
321
+ SUPATYPE_SQL_DATABASE_URL: dbURL,
322
+ PGSSLMODE: "disable",
323
+ GOTRUE_DB_NAMESPACE: "auth",
324
+ GOTRUE_DB_DRIVER: "postgres",
325
+ GOTRUE_JWT_SECRET: LOCAL_JWT_SECRET,
326
+ GOTRUE_JWT_EXP: "3600",
327
+ GOTRUE_JWT_AUD: "authenticated",
328
+ GOTRUE_JWT_ADMIN_ROLES: "supatype_admin,service_role",
329
+ API_EXTERNAL_URL: `http://localhost:${serverPort}/auth/v1`,
330
+ GOTRUE_API_HOST: "localhost",
331
+ GOTRUE_SITE_URL: `http://localhost:${serverPort}`,
332
+ GOTRUE_MAILER_MAILER_PROVIDER: gotrueMailerProvider,
333
+ GOTRUE_MAILER_AUTOCONFIRM: "true",
334
+ GOTRUE_LOG_LEVEL: "info",
335
+ GOTRUE_DISABLE_SIGNUP: "false",
336
+ ...(config.email?.resend_api_key !== undefined && config.email.resend_api_key !== ""
337
+ ? { RESEND_API_KEY: config.email.resend_api_key }
338
+ : {}),
339
+ ...(gotrueMailerProvider === "resend" &&
340
+ config.email?.resend_from !== undefined &&
341
+ config.email.resend_from.trim() !== ""
342
+ ? { RESEND_FROM: config.email.resend_from.trim() }
343
+ : {}),
344
+ ...(gotrueMailerProvider === "ses" &&
345
+ config.email?.ses_from !== undefined &&
346
+ config.email.ses_from.trim() !== ""
347
+ ? { SES_FROM: config.email.ses_from.trim() }
348
+ : {}),
349
+ ...(gotrueMailerProvider === "smtp" ? gotrueSMTPFromEmailConfig(config.email) : {}),
350
+ ...(config.email?.send_email_hook === true
351
+ ? {
352
+ GOTRUE_HOOK_SEND_EMAIL_ENABLED: "true",
353
+ GOTRUE_HOOK_SEND_EMAIL_URI:
354
+ config.email?.send_email_hook_uri !== undefined &&
355
+ config.email.send_email_hook_uri.trim() !== ""
356
+ ? config.email.send_email_hook_uri.trim()
357
+ : `http://127.0.0.1:${serverPort}/internal/v0hooks/send-email`,
358
+ GOTRUE_HOOK_SEND_EMAIL_SECRETS:
359
+ config.email?.send_email_hook_secrets !== undefined &&
360
+ config.email.send_email_hook_secrets.trim() !== ""
361
+ ? config.email.send_email_hook_secrets.trim()
362
+ : LOCAL_SEND_EMAIL_HOOK_SECRETS,
363
+ }
364
+ : {}),
365
+ ...(config.storage?.provider !== "s3" ? localStorageEnv(stateRoot) : {}),
366
+ ...loadDotEnv(cwd),
367
+ }
63
368
 
64
- const children: ChildProcess[] = []
369
+ const serverProc = new ProcessManager(serverBin, [], {
370
+ label: "server",
371
+ pidDir,
372
+ colour: "\x1b[32m",
373
+ env: serverEnv,
374
+ })
375
+ serverProc.start()
376
+
377
+ // ── 9b. PostgREST ────────────────────────────────────────────────────
378
+ let postgrestProc: ProcessManager | null = null
379
+ const postgrestBin = await resolvePostgrestBin(config.overrides?.postgrest)
380
+ if (postgrestBin) {
381
+ // Windows PostgREST builds are dynamically linked and require libpq/OpenSSL
382
+ // DLLs from a Postgres bin directory, even when the database runs in Docker.
383
+ let postgrestRuntimeBinDir = pgBinDir
384
+ if (process.platform === "win32" && postgrestRuntimeBinDir === null) {
385
+ try {
386
+ postgrestRuntimeBinDir = await resolvePgBinDir(config)
387
+ } catch (error) {
388
+ console.warn(
389
+ `[supatype] ⚠ Could not resolve Postgres runtime DLL directory for PostgREST: ${(error as Error).message}\n` +
390
+ " PostgREST may fail to start on Windows until Postgres binaries are available locally.",
391
+ )
392
+ }
393
+ }
65
394
 
66
- if (opts.local) {
67
- console.log("\nStarting local services from source...")
68
- children.push(
69
- ...startLocalServices(cwd),
395
+ const postgrestEnv: Record<string, string> = {
396
+ PGRST_DB_URI: dbURL,
397
+ PGRST_DB_SCHEMA: "public, supatype, graphql_public",
398
+ PGRST_DB_ANON_ROLE: "anon",
399
+ PGRST_SERVER_PORT: postgrestPort,
400
+ PGRST_SERVER_HOST: "127.0.0.1",
401
+ PGRST_JWT_SECRET: serverEnv["GOTRUE_JWT_SECRET"] ?? "",
402
+ PGRST_LOG_LEVEL: "warn",
403
+ // On Windows, PostgREST (MinGW/GHC binary) needs libpq.dll and
404
+ // OpenSSL DLLs. Prepend a Postgres bin dir which bundles these
405
+ // runtime dependencies.
406
+ ...(process.platform === "win32" && postgrestRuntimeBinDir !== null
407
+ ? { PATH: `${postgrestRuntimeBinDir};${process.env["PATH"] ?? ""}` }
408
+ : {}),
409
+ }
410
+
411
+ const preflight = spawnSync(
412
+ postgrestBin,
413
+ ["--help"],
414
+ { env: { ...process.env, ...postgrestEnv }, stdio: "pipe", encoding: "utf8" },
70
415
  )
416
+ if (preflight.status !== 0) {
417
+ const detail = (preflight.stderr || preflight.stdout || "").trim()
418
+ console.warn(
419
+ `[supatype] ⚠ PostgREST failed preflight (exit ${preflight.status}). ` +
420
+ "Skipping /rest/v1 startup to avoid crash loop.",
421
+ )
422
+ if (detail) {
423
+ console.warn(`[supatype] PostgREST preflight output:\n${detail}`)
424
+ }
425
+ } else {
426
+ postgrestProc = new ProcessManager(postgrestBin, [], {
427
+ label: "postgrest",
428
+ pidDir,
429
+ colour: "\x1b[36m",
430
+ env: postgrestEnv,
431
+ })
432
+ postgrestProc.start()
433
+ }
71
434
  }
72
435
 
73
- console.log("\nServices running:")
74
- console.log(" Postgres postgresql://localhost:5432")
75
- console.log(" PostgREST http://localhost:3000")
76
- console.log(" Kong http://localhost:8000")
77
- console.log(" REST API http://localhost:8000/rest/v1/")
78
- console.log(" GraphQL http://localhost:8000/graphql/v1")
79
- if (opts.local) {
80
- console.log(" Storage http://localhost:5000 (from source)")
81
- console.log(" Realtime http://localhost:4000 (from source)")
82
- console.log(" Studio http://localhost:3002 (from source)")
436
+ // ── 9d. Studio (optional) ─────────────────────────────────────────────
437
+ const studioPort = 3002
438
+ let studioProc: ProcessManager | null = null
439
+
440
+ const studioOverride = config.overrides?.studio
441
+ if (studioOverride) {
442
+ studioProc = startStudioViteDevServer({
443
+ cwd,
444
+ studioOverride,
445
+ pidDir,
446
+ serviceRoleKey,
447
+ proxyTarget: `http://localhost:${serverPort}`,
448
+ viteSupatypeUrl: `http://localhost:${studioPort}`,
449
+ })
450
+ studioProc?.start()
83
451
  }
84
- console.log()
85
452
 
86
- // Clean shutdown on Ctrl+C
87
- const cleanup = () => {
88
- for (const child of children) {
89
- child.kill()
90
- }
453
+ const appProc = startProxyDevApp(cwd, config, pidDir)
454
+
455
+ // ── Print status ──────────────────────────────────────────────────────
456
+ console.log(`
457
+ [supatype] Services running:
458
+ Postgres ${dbURL}
459
+ supatype-server http://localhost:${serverPort}
460
+ REST API http://localhost:${serverPort}/rest/v1/
461
+ Auth http://localhost:${serverPort}/auth/v1/
462
+ Storage http://localhost:${serverPort}/storage/v1/
463
+ Realtime ws://localhost:${serverPort}/realtime/v1/${studioProc ? `\n Studio http://localhost:${studioPort}` : ""}
464
+
465
+ API keys (local dev only):
466
+ anon key ${anonKey}
467
+ service_role ${serviceRoleKey}
468
+
469
+ JWT secret: ${LOCAL_JWT_SECRET}
470
+
471
+ Press Ctrl+C to stop.
472
+ `)
473
+
474
+
475
+ // ── Shutdown handler ──────────────────────────────────────────────────
476
+ const cleanup = async () => {
477
+ console.log("\n[supatype] Shutting down...")
478
+ await Promise.all([
479
+ serverProc.stop(),
480
+ postgrestProc?.stop(),
481
+ studioProc?.stop(),
482
+ appProc?.stop(),
483
+ ])
484
+ await stopPostgres()
91
485
  process.exit(0)
92
486
  }
93
- process.on("SIGINT", cleanup)
94
- process.on("SIGTERM", cleanup)
487
+ process.once("SIGINT", cleanup)
488
+ process.once("SIGTERM", cleanup)
95
489
 
490
+ // ── 10. Schema watch ──────────────────────────────────────────────────
96
491
  if (opts.watch) {
97
- await watchAndPush(cwd)
492
+ const schemaDir = resolve(cwd, schemaPath, "..")
493
+ console.log(`[supatype] Watching ${schemaDir} for changes...`)
494
+
495
+ const { watch } = await import("node:fs")
496
+ // Debounce: Windows fs.watch fires multiple events per save.
497
+ // Wait 300 ms after the last event before pushing.
498
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
499
+ watch(schemaDir, { recursive: true }, (_eventType, filename) => {
500
+ if (!filename?.endsWith(".ts")) return
501
+ if (debounceTimer) clearTimeout(debounceTimer)
502
+ debounceTimer = setTimeout(() => {
503
+ debounceTimer = null
504
+ console.log(`\n[supatype] Change detected in ${filename}, checking schema...`)
505
+ runSchemaPush(cwd, engineBin, schemaPath, dbURL, manifestPath, adminConfigPath, localStoragePath, skipFieldKinds, config).catch((e: unknown) =>
506
+ console.error("[supatype] Schema push failed:", (e as Error).message),
507
+ )
508
+ }, 300)
509
+ })
98
510
  }
511
+
512
+ // Block until killed.
513
+ await new Promise<never>(() => undefined)
99
514
  })
100
515
  }
101
516
 
102
- function ensureInfraCompose(cwd: string): void {
103
- const projectName = resolve(cwd).split(/[\\/]/).pop() ?? "supatype"
104
- const content = `# Generated by supatype dev --local — infra services only
105
- services:
106
- db:
107
- image: supatype/postgres:17-latest
108
- environment:
109
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
110
- POSTGRES_DB: \${POSTGRES_DB:-${projectName}}
111
- ports:
112
- - "5432:5432"
113
- volumes:
114
- - db-data:/var/lib/postgresql/data
115
- healthcheck:
116
- test: ["CMD-SHELL", "pg_isready -U postgres"]
117
- interval: 5s
118
- timeout: 5s
119
- retries: 20
120
-
121
- pgbouncer:
122
- image: pgbouncer/pgbouncer:latest
123
- volumes:
124
- - ./.supatype/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
125
- - ./.supatype/userlist.txt:/etc/pgbouncer/userlist.txt:ro
126
- depends_on:
127
- db:
128
- condition: service_healthy
129
- healthcheck:
130
- test: ["CMD", "pg_isready", "-h", "localhost", "-p", "6432", "-U", "postgres"]
131
- interval: 5s
132
- timeout: 5s
133
- retries: 10
134
-
135
- gotrue:
136
- image: supatype/auth:v1.0.0
137
- environment:
138
- GOTRUE_API_HOST: 0.0.0.0
139
- GOTRUE_API_PORT: 9999
140
- GOTRUE_DB_DRIVER: postgres
141
- GOTRUE_DB_DATABASE_URL: "postgres://postgres:\${POSTGRES_PASSWORD:-postgres}@pgbouncer:6432/\${POSTGRES_DB:-${projectName}}?search_path=auth"
142
- GOTRUE_SITE_URL: \${SITE_URL:-http://localhost:3000}
143
- GOTRUE_JWT_SECRET: \${JWT_SECRET:-super-secret-jwt-token-change-in-production}
144
- GOTRUE_JWT_EXP: 3600
145
- GOTRUE_JWT_AUD: authenticated
146
- GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
147
- GOTRUE_JWT_ADMIN_ROLES: service_role
148
- GOTRUE_MAILER_AUTOCONFIRM: \${GOTRUE_MAILER_AUTOCONFIRM:-true}
149
- GOTRUE_DISABLE_SIGNUP: \${DISABLE_SIGNUP:-false}
150
- ports:
151
- - "9999:9999"
152
- depends_on:
153
- pgbouncer:
154
- condition: service_healthy
155
-
156
- postgrest:
157
- image: postgrest/postgrest:v12.2.8
158
- environment:
159
- PGRST_DB_URI: postgresql://authenticator:\${POSTGRES_PASSWORD:-postgres}@pgbouncer:6432/\${POSTGRES_DB:-${projectName}}
160
- PGRST_DB_SCHEMA: public
161
- PGRST_DB_ANON_ROLE: anon
162
- PGRST_JWT_SECRET: \${JWT_SECRET:-super-secret-jwt-token-change-in-production}
163
- PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
164
- PGRST_DB_POOL: 3
165
- ports:
166
- - "3000:3000"
167
- depends_on:
168
- pgbouncer:
169
- condition: service_healthy
170
-
171
- minio:
172
- image: minio/minio:RELEASE.2024-11-07T00-52-20Z
173
- command: server /data --console-address ":9001"
174
- environment:
175
- MINIO_ROOT_USER: supatype
176
- MINIO_ROOT_PASSWORD: supatype-secret
177
- ports:
178
- - "9000:9000"
179
- - "9001:9001"
180
- volumes:
181
- - minio-data:/data
182
- healthcheck:
183
- test: ["CMD", "mc", "ready", "local"]
184
- interval: 5s
185
- timeout: 5s
186
- retries: 10
187
-
188
- kong:
189
- image: kong:3.6
190
- environment:
191
- KONG_DATABASE: "off"
192
- KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml
193
- KONG_PROXY_ACCESS_LOG: /dev/stdout
194
- KONG_ADMIN_ACCESS_LOG: /dev/stdout
195
- KONG_PROXY_ERROR_LOG: /dev/stderr
196
- KONG_ADMIN_ERROR_LOG: /dev/stderr
197
- volumes:
198
- - ./.supatype/kong.yml:/etc/kong/kong.yml:ro
199
- ports:
200
- - "8000:8000"
201
- depends_on:
202
- - postgrest
203
- - gotrue
204
-
205
- volumes:
206
- db-data:
207
- minio-data:
208
- `
209
- writeFileSync(resolve(cwd, "docker-compose.yml"), content, "utf8")
210
- console.log(" created docker-compose.yml (infra only)\n")
211
-
212
- // Also ensure pgbouncer config exists
213
- const supatypeDir = resolve(cwd, ".supatype")
214
- mkdirSync(supatypeDir, { recursive: true })
215
-
216
- if (!existsSync(resolve(supatypeDir, "pgbouncer.ini"))) {
217
- writeFileSync(resolve(supatypeDir, "pgbouncer.ini"), `[databases]
218
- * = host=db port=5432
219
-
220
- [pgbouncer]
221
- listen_addr = 0.0.0.0
222
- listen_port = 6432
223
- auth_type = trust
224
- auth_file = /etc/pgbouncer/userlist.txt
225
- pool_mode = transaction
226
- default_pool_size = 20
227
- max_db_connections = 60
228
- max_client_conn = 100
229
- server_reset_query = DEALLOCATE ALL
230
- ignore_startup_parameters = extra_float_digits
231
- `, "utf8")
517
+ // ---------------------------------------------------------------------------
518
+ // Schema push (engine subprocess)
519
+ // ---------------------------------------------------------------------------
520
+
521
+ // Last successfully-pushed AST JSON — used to skip no-op re-fires.
522
+ let _lastPushedAst: string | null = null
523
+ // AST that failed on its last attempt — always retried even if content is unchanged.
524
+ let _lastFailedAst: string | null = null
525
+
526
+ async function runSchemaPush(
527
+ cwd: string,
528
+ engineBin: string,
529
+ schemaPath: string,
530
+ dbURL: string,
531
+ manifestPath: string,
532
+ adminConfigPath?: string,
533
+ storagePath?: string,
534
+ skipFieldKinds?: ReadonlySet<string>,
535
+ config?: import("../project-config.js").SupatypeProjectConfig,
536
+ ): Promise<void> {
537
+ // Build AST JSON from schema file.
538
+ const { loadSchemaAst } = await import("../config.js")
539
+ let ast = loadSchemaAst(schemaPath, cwd)
540
+
541
+ // Strip fields whose kind requires an unavailable Postgres extension.
542
+ if (skipFieldKinds && skipFieldKinds.size > 0) {
543
+ const { filtered, adapted } = adaptUnsupportedKinds(ast, skipFieldKinds)
544
+ ast = filtered
545
+ if (adapted.length > 0) {
546
+ console.warn(
547
+ `[supatype] ⚠ ${adapted.length} field(s) replaced with JSONB — required extensions not available:\n` +
548
+ adapted.map((s: string) => ` ${s}`).join("\n"),
549
+ )
550
+ }
551
+ }
552
+
553
+ const astJson = JSON.stringify(ast)
554
+
555
+ // Skip only when the last push of this exact AST succeeded.
556
+ // If it previously failed we always retry so the user can trigger a re-run
557
+ // by simply saving the file again without needing to make a content change.
558
+ if (astJson === _lastPushedAst && astJson !== _lastFailedAst) {
559
+ return
560
+ }
561
+
562
+ const astPath = join(cwd, ".supatype", "schema.ast.json")
563
+ writeFileSync(astPath, astJson)
564
+
565
+ // Push schema.
566
+ console.log("[supatype] Applying schema...")
567
+ const pushResult = spawnSync(
568
+ engineBin,
569
+ ["push", "-i", astPath, "--database-url", dbURL, "--force", "--non-interactive"],
570
+ { cwd, stdio: "inherit", encoding: "utf8" },
571
+ )
572
+ if (pushResult.status !== 0) {
573
+ _lastFailedAst = astJson
574
+ throw new Error(`Engine schema push failed (exit ${pushResult.status})`)
575
+ }
576
+ _lastPushedAst = astJson
577
+ _lastFailedAst = null
578
+
579
+ // Provision storage buckets declared in the schema.
580
+ if (storagePath) {
581
+ const parseResult = spawnSync(engineBin, ["parse", "-i", astPath], { cwd, stdio: "pipe", encoding: "utf8" })
582
+ if (parseResult.status === 0 && parseResult.stdout) {
583
+ try {
584
+ const resolvedAst = JSON.parse(parseResult.stdout) as {
585
+ storageBuckets?: Array<{
586
+ id: string
587
+ public: boolean
588
+ allowedMimeTypes?: string[]
589
+ fileSizeLimit?: number
590
+ accessMode?: string
591
+ s3BucketPolicy?: string | null
592
+ }>
593
+ }
594
+ if (resolvedAst.storageBuckets && resolvedAst.storageBuckets.length > 0) {
595
+ provisionStorageBuckets(resolvedAst.storageBuckets, storagePath)
596
+ }
597
+ } catch { /* ignore parse errors */ }
598
+ }
232
599
  }
233
600
 
234
- if (!existsSync(resolve(supatypeDir, "userlist.txt"))) {
235
- writeFileSync(resolve(supatypeDir, "userlist.txt"), "", "utf8")
601
+ // Generate manifest.
602
+ const genResult = spawnSync(
603
+ engineBin,
604
+ ["generate", "-i", astPath, "-o", manifestPath],
605
+ { cwd, stdio: "pipe", encoding: "utf8" },
606
+ )
607
+ if (genResult.status !== 0) {
608
+ console.warn("[supatype] Manifest generation failed — server routing may be stale.")
236
609
  }
237
610
 
238
- if (!existsSync(resolve(supatypeDir, "kong.yml"))) {
239
- writeFileSync(resolve(supatypeDir, "kong.yml"), `_format_version: "3.0"
240
-
241
- services:
242
- - name: rest-v1
243
- url: http://postgrest:3000
244
- routes:
245
- - name: rest-v1-all
246
- strip_path: true
247
- paths:
248
- - /rest/v1/
249
- - name: auth-v1
250
- url: http://gotrue:9999
251
- routes:
252
- - name: auth-v1-all
253
- strip_path: true
254
- paths:
255
- - /auth/v1/
256
- - name: storage-v1
257
- url: http://host.docker.internal:5000
258
- routes:
259
- - name: storage-v1-all
260
- strip_path: true
261
- paths:
262
- - /storage/v1/
263
- - name: realtime-v1
264
- url: http://host.docker.internal:4000
265
- routes:
266
- - name: realtime-v1-all
267
- strip_path: true
268
- paths:
269
- - /realtime/v1/
270
- protocols:
271
- - http
272
- - https
273
- - ws
274
- - wss
275
- - name: functions-v1
276
- url: http://host.docker.internal:54321
277
- routes:
278
- - name: functions-v1-all
279
- strip_path: false
280
- paths:
281
- - /functions/v1/
282
- - name: studio
283
- url: http://host.docker.internal:3002
284
- routes:
285
- - name: studio-all
286
- strip_path: true
287
- paths:
288
- - /studio/
289
- `, "utf8")
611
+ // Generate admin config (for Studio). Engine writes to stdout.
612
+ if (adminConfigPath) {
613
+ const adminResult = spawnSync(
614
+ engineBin,
615
+ ["admin", "-i", astPath],
616
+ { cwd, stdio: "pipe", encoding: "utf8" },
617
+ )
618
+ if (adminResult.status === 0 && adminResult.stdout) {
619
+ const { withAdminRoles } = await import("../studio-admin-roles.js")
620
+ let admin: unknown
621
+ try {
622
+ admin = JSON.parse(adminResult.stdout) as unknown
623
+ } catch {
624
+ admin = adminResult.stdout
625
+ }
626
+ const merged = config ? withAdminRoles(admin, config) : admin
627
+ writeFileSync(adminConfigPath, `${JSON.stringify(merged, null, 2)}\n`)
628
+ }
290
629
  }
630
+
631
+ console.log("[supatype] Schema applied.")
291
632
  }
292
633
 
293
- function ensureDevEnv(cwd: string): void {
294
- const envPath = resolve(cwd, ".env")
295
- if (existsSync(envPath)) return
296
-
297
- const projectName = resolve(cwd).split(/[\\/]/).pop() ?? "supatype"
298
- const content = `# Generated by supatype dev — all defaults for local development
299
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${projectName}
300
- POSTGRES_PASSWORD=postgres
301
- POSTGRES_DB=${projectName}
302
-
303
- JWT_SECRET=super-secret-jwt-token-change-in-production
304
- ANON_KEY=
305
- SERVICE_ROLE_KEY=
306
-
307
- SITE_URL=http://localhost:3000
308
-
309
- # Storage (MinIO)
310
- S3_ENDPOINT=http://localhost:9000
311
- S3_REGION=us-east-1
312
- S3_ACCESS_KEY=supatype
313
- S3_SECRET_KEY=supatype-secret
314
- S3_FORCE_PATH_STYLE=true
315
-
316
- # SMTP — leave empty for email autoconfirm in dev
317
- SMTP_HOST=
318
- SMTP_PORT=
319
- SMTP_USER=
320
- SMTP_PASS=
321
- `
322
- writeFileSync(envPath, content, "utf8")
323
- console.log(" created .env (local dev defaults)\n")
634
+ // ---------------------------------------------------------------------------
635
+ // Storage bucket provisioning (local dev only)
636
+ // ---------------------------------------------------------------------------
637
+
638
+ function provisionStorageBuckets(
639
+ declared: Array<{
640
+ id: string
641
+ public: boolean
642
+ allowedMimeTypes?: string[]
643
+ fileSizeLimit?: number
644
+ accessMode?: string
645
+ s3BucketPolicy?: string | null
646
+ }>,
647
+ storagePath: string,
648
+ ): void {
649
+ const bucketsDir = join(storagePath, ".supatype")
650
+ const bucketsFile = join(bucketsDir, "buckets.json")
651
+ mkdirSync(bucketsDir, { recursive: true })
652
+
653
+ let existing: Array<Record<string, unknown>> = []
654
+ try {
655
+ existing = JSON.parse(readFileSync(bucketsFile, "utf8")) as Array<Record<string, unknown>>
656
+ } catch { /* file doesn't exist yet */ }
657
+
658
+ const existingIds = new Set(existing.map((b) => b["id"] as string))
659
+ let added = 0
660
+
661
+ for (const bucket of declared) {
662
+ if (existingIds.has(bucket.id)) continue
663
+ const now = new Date().toISOString()
664
+ existing.push({
665
+ id: bucket.id,
666
+ name: bucket.id,
667
+ public: bucket.public,
668
+ file_size_limit: bucket.fileSizeLimit ?? null,
669
+ allowed_mime_types: bucket.allowedMimeTypes ?? null,
670
+ access_mode:
671
+ bucket.accessMode ?? (bucket.public ? "public" : "private"),
672
+ s3_bucket_policy: bucket.s3BucketPolicy ?? null,
673
+ created_at: now,
674
+ updated_at: now,
675
+ })
676
+ mkdirSync(join(storagePath, bucket.id), { recursive: true })
677
+ added++
678
+ }
679
+
680
+ if (added > 0) {
681
+ writeFileSync(bucketsFile, JSON.stringify(existing, null, 2))
682
+ console.log(`[supatype] Storage: provisioned ${added} bucket(s).`)
683
+ }
324
684
  }
325
685
 
326
- function loadDotEnv(cwd: string): Record<string, string> {
327
- const envPath = resolve(cwd, ".env")
328
- if (!existsSync(envPath)) return {}
329
- const vars: Record<string, string> = {}
330
- for (const line of readFileSync(envPath, "utf8").split("\n")) {
331
- const trimmed = line.trim()
332
- if (!trimmed || trimmed.startsWith("#")) continue
333
- const eq = trimmed.indexOf("=")
334
- if (eq === -1) continue
335
- const key = trimmed.slice(0, eq)
336
- const value = trimmed.slice(eq + 1)
337
- vars[key] = value
686
+ // ---------------------------------------------------------------------------
687
+ // Resolve Postgres bin dir
688
+ // ---------------------------------------------------------------------------
689
+
690
+ async function resolvePgBinDir(config: Awaited<ReturnType<typeof loadConfig>>): Promise<string> {
691
+ const override = config.overrides?.postgres_dir
692
+ if (override) {
693
+ // Normalize Git Bash (/c/Users/...) paths to Win32 form (C:\Users\...) on Windows.
694
+ const normalised = normalisePlatformPath(override)
695
+ const resolved = resolve(process.cwd(), normalised)
696
+ const binDir = join(resolved, "bin")
697
+ if (!existsSync(binDir)) {
698
+ throw new Error(`[overrides] postgres_dir does not contain a bin/ directory: ${resolved}`)
699
+ }
700
+ console.warn(`\u26a0 Using local Postgres build: ${resolved}`)
701
+ return binDir
338
702
  }
339
- return vars
703
+
704
+ // Locate cached Postgres archive.
705
+ const { cachePath } = await import("../binary-cache.js")
706
+ const version = config.versions.postgres
707
+ const { currentPlatform } = await import("../binary-cache.js")
708
+ const platform = currentPlatform()
709
+
710
+ const pgCacheDir = cachePath("postgres", version)
711
+ const extractedDir = join(pgCacheDir, `pg-${version}`)
712
+
713
+ const pgCtlName = platform.os === "windows" ? "pg_ctl.exe" : "pg_ctl"
714
+ if (!existsSync(join(extractedDir, "bin", pgCtlName))) {
715
+ // Try to extract the cached archive.
716
+ await extractPostgresArchive(pgCacheDir, version, platform, extractedDir)
717
+ }
718
+
719
+ return join(extractedDir, "bin")
340
720
  }
341
721
 
342
- function localDevDefaults(cwd: string): Record<string, string> {
343
- const projectName = resolve(cwd).split(/[\\/]/).pop() ?? "supatype"
344
- // Known defaults that match docker-compose local dev setup
345
- const defaults: Record<string, string> = {
346
- DATABASE_URL: `postgresql://postgres:postgres@localhost:5432/${projectName}`,
347
- POSTGRES_PASSWORD: "postgres",
348
- POSTGRES_DB: projectName,
349
- JWT_SECRET: "super-secret-jwt-token-change-in-production",
350
- SITE_URL: "http://localhost:3000",
351
- S3_ENDPOINT: "http://localhost:9000",
352
- S3_REGION: "us-east-1",
353
- S3_ACCESS_KEY: "supatype",
354
- S3_SECRET_KEY: "supatype-secret",
355
- S3_FORCE_PATH_STYLE: "true",
356
- SLOT_NAME: "realtime_slot",
357
- REPLICATION_POLL_INTERVAL: "100",
358
- SECURE_CHANNELS: "true",
722
+ async function extractPostgresArchive(
723
+ pgCacheDir: string,
724
+ version: string,
725
+ platform: { os: string; arch: string },
726
+ extractDir: string,
727
+ ): Promise<void> {
728
+ const ext = platform.os === "windows" ? ".zip" : ".tar.gz"
729
+ const archiveName = `supatype-pg-${postgresArchiveTag(version)}-${platform.os}-${platform.arch}${ext}`
730
+ const archivePath = join(pgCacheDir, archiveName)
731
+
732
+ if (!existsSync(archivePath)) {
733
+ throw new Error(
734
+ `Postgres ${version} archive not found. Run: supatype update`,
735
+ )
736
+ }
737
+
738
+ mkdirSync(extractDir, { recursive: true })
739
+
740
+ // On Windows, Git Bash tar is typically first in PATH and chokes on drive-letter
741
+ // paths (C:\...). Use PowerShell's Expand-Archive instead, which handles Windows
742
+ // paths natively. On Linux/macOS, use tar as normal.
743
+ const result = platform.os === "windows"
744
+ ? spawnSync(
745
+ "powershell.exe",
746
+ ["-NoProfile", "-Command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`],
747
+ { stdio: "inherit" },
748
+ )
749
+ : spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], { stdio: "inherit" })
750
+
751
+ if (result.status !== 0) {
752
+ throw new Error(`Failed to extract Postgres archive: ${archivePath}`)
359
753
  }
360
- // .env file values override defaults
361
- const dotEnv = loadDotEnv(cwd)
362
- return { ...defaults, ...dotEnv }
363
754
  }
364
755
 
365
- function startLocalServices(cwd: string): ChildProcess[] {
366
- const children: ChildProcess[] = []
367
- const devEnv = localDevDefaults(cwd)
756
+ // ---------------------------------------------------------------------------
757
+ // PostgREST resolver downloads from GitHub releases if not cached
758
+ // ---------------------------------------------------------------------------
368
759
 
369
- const services = [
370
- { name: "storage", filter: "@supatype/storage", color: "\x1b[34m" },
371
- { name: "realtime", filter: "@supatype/realtime", color: "\x1b[35m" },
372
- { name: "studio", filter: "@supatype/studio", color: "\x1b[36m" },
373
- ]
760
+ const POSTGREST_DEFAULT_VERSION = "12.2.3"
761
+ const POSTGREST_GITHUB = "https://github.com/PostgREST/postgrest/releases/download"
374
762
 
375
- for (const svc of services) {
376
- const pkgDir = resolve(cwd, "..", "packages", svc.name)
763
+ async function resolvePostgrestBin(overridePath?: string): Promise<string | null> {
764
+ // Honour local override (same pattern as engine/server).
765
+ if (overridePath) {
766
+ let p = resolve(process.cwd(), normalisePlatformPath(overridePath))
767
+ if (process.platform === "win32" && !p.endsWith(".exe") && !existsSync(p)) {
768
+ const withExe = p + ".exe"
769
+ if (existsSync(withExe)) p = withExe
770
+ }
771
+ if (existsSync(p)) return p
772
+ console.warn(`[supatype] ⚠ PostgREST override not found at ${p}`)
773
+ return null
774
+ }
377
775
 
378
- if (!existsSync(resolve(pkgDir, "package.json"))) {
379
- console.warn(` Skipping ${svc.name} not found at ${pkgDir}`)
380
- continue
776
+ const version = POSTGREST_DEFAULT_VERSION
777
+ const platform = currentPlatform()
778
+ const arch = platform.arch === "arm64" ? "aarch64" : "x86_64"
779
+ const binName = platform.os === "windows" ? "postgrest.exe" : "postgrest"
780
+ const cacheDir = cachePath("postgres", version).replace(/postgres/, "postgrest")
781
+ const binPath = join(cacheDir, binName)
782
+ const archiveName = platform.os === "windows"
783
+ ? `postgrest-v${version}-windows-x64.zip`
784
+ : platform.os === "darwin"
785
+ ? `postgrest-v${version}-macos-${arch}.tar.xz`
786
+ : `postgrest-v${version}-linux-static-${arch}.tar.xz`
787
+ const archivePath = join(cacheDir, archiveName)
788
+
789
+ if (existsSync(binPath)) {
790
+ // Backfill DLLs for older cached Windows installs where only postgrest.exe
791
+ // was copied from the release archive.
792
+ if (platform.os === "windows" && !hasLikelyWindowsRuntimeDlls(cacheDir) && existsSync(archivePath)) {
793
+ const repaired = repairWindowsPostgrestRuntime(cacheDir, archivePath, binPath)
794
+ if (!repaired) {
795
+ console.warn("[supatype] ⚠ PostgREST runtime DLL repair failed; REST API may be unavailable.")
796
+ }
381
797
  }
798
+ return binPath
799
+ }
382
800
 
383
- const reset = "\x1b[0m"
384
- const prefix = `${svc.color}[${svc.name}]${reset}`
385
-
386
- const child = spawn("pnpm", ["dev"], {
387
- cwd: pkgDir,
388
- stdio: ["ignore", "pipe", "pipe"],
389
- shell: true,
390
- env: {
391
- ...process.env,
392
- ...devEnv,
393
- PORT: svc.name === "storage" ? "5000" : svc.name === "realtime" ? "4000" : "3002",
394
- },
395
- })
801
+ // Download from GitHub releases.
802
+ const url = `${POSTGREST_GITHUB}/v${version}/${archiveName}`
396
803
 
397
- child.stdout?.on("data", (data: Buffer) => {
398
- for (const line of data.toString().trimEnd().split("\n")) {
399
- console.log(`${prefix} ${line}`)
400
- }
401
- })
402
- child.stderr?.on("data", (data: Buffer) => {
403
- for (const line of data.toString().trimEnd().split("\n")) {
404
- console.error(`${prefix} ${line}`)
405
- }
406
- })
407
- child.on("exit", (code) => {
408
- if (code !== 0 && code !== null) {
409
- console.error(`${prefix} exited with code ${code}`)
410
- }
411
- })
804
+ console.log(`[supatype] Downloading PostgREST v${version}...`)
805
+ mkdirSync(cacheDir, { recursive: true })
412
806
 
413
- children.push(child)
414
- console.log(` ${prefix} started (pnpm dev)`)
807
+ let resp: Response
808
+ try {
809
+ resp = await fetch(url)
810
+ } catch (e) {
811
+ console.warn(
812
+ `[supatype] ⚠ Could not download PostgREST (${(e as Error).message}).\n` +
813
+ ` REST API (/rest/v1/) will be unavailable until the download succeeds.\n` +
814
+ ` Re-run 'supatype dev' once network access to github.com:443 is restored.`,
815
+ )
816
+ return null
817
+ }
818
+ if (!resp.ok) {
819
+ console.warn(`[supatype] ⚠ Could not download PostgREST: HTTP ${resp.status}. REST API will be unavailable.`)
820
+ return null
415
821
  }
416
822
 
417
- return children
418
- }
419
-
420
- async function waitForPostgREST(): Promise<void> {
421
- const deadline = Date.now() + HEALTH_TIMEOUT_MS
422
- while (Date.now() < deadline) {
423
- try {
424
- const res = await fetch(POSTGREST_URL, { signal: AbortSignal.timeout(2000) })
425
- if (res.ok || res.status === 401) return // 401 = JWT required = server up
426
- } catch {
427
- // not ready yet
823
+ const buf = Buffer.from(await resp.arrayBuffer())
824
+ writeFileSync(archivePath, buf)
825
+
826
+ // Extract. The Windows zip may nest postgrest.exe inside a subdirectory, so
827
+ // after Expand-Archive we copy postgrest.exe and sibling DLLs to cacheDir.
828
+ if (platform.os === "windows") {
829
+ const r = spawnSync(
830
+ "powershell.exe",
831
+ [
832
+ "-NoProfile", "-Command",
833
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${cacheDir}' -Force; ` +
834
+ `$exe = Get-ChildItem -Path '${cacheDir}' -Recurse -Filter 'postgrest.exe' | Select-Object -First 1; ` +
835
+ `if ($exe) { ` +
836
+ ` Copy-Item -Path $exe.FullName -Destination '${binPath}' -Force; ` +
837
+ ` Get-ChildItem -Path $exe.Directory.FullName -Filter '*.dll' | ` +
838
+ ` ForEach-Object { Copy-Item -Path $_.FullName -Destination '${cacheDir}' -Force }; ` +
839
+ `}`,
840
+ ],
841
+ { stdio: "pipe", encoding: "utf8" },
842
+ )
843
+ if (r.status !== 0) {
844
+ console.warn(`[supatype] ⚠ PostgREST extraction failed: ${r.stderr?.trim() ?? "unknown error"}. REST API will be unavailable.`)
845
+ return null
846
+ }
847
+ } else {
848
+ const r = spawnSync("tar", ["-xJf", archivePath, "-C", cacheDir], { stdio: "pipe" })
849
+ if (r.status !== 0) {
850
+ console.warn("[supatype] ⚠ PostgREST extraction failed. REST API will be unavailable.")
851
+ return null
428
852
  }
429
- await sleep(HEALTH_POLL_MS)
430
853
  }
431
- throw new Error(
432
- `PostgREST did not become healthy within ${HEALTH_TIMEOUT_MS / 1000}s.\n` +
433
- "Check: docker compose logs postgrest",
854
+
855
+ if (!existsSync(binPath)) {
856
+ console.warn("[supatype] ⚠ PostgREST binary not found after extraction. REST API will be unavailable.")
857
+ return null
858
+ }
859
+
860
+ if (platform.os !== "windows") {
861
+ const { chmod } = await import("node:fs/promises")
862
+ await chmod(binPath, 0o755)
863
+ }
864
+
865
+ console.log(`[supatype] PostgREST v${version} ready.`)
866
+ return binPath
867
+ }
868
+
869
+ function hasLikelyWindowsRuntimeDlls(dir: string): boolean {
870
+ if (process.platform !== "win32") return true
871
+ return (
872
+ existsSync(join(dir, "libpq.dll")) ||
873
+ existsSync(join(dir, "libpq-5.dll")) ||
874
+ existsSync(join(dir, "libssl-3-x64.dll"))
875
+ )
876
+ }
877
+
878
+ function repairWindowsPostgrestRuntime(cacheDir: string, archivePath: string, binPath: string): boolean {
879
+ const r = spawnSync(
880
+ "powershell.exe",
881
+ [
882
+ "-NoProfile",
883
+ "-Command",
884
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${cacheDir}' -Force; ` +
885
+ `$exe = Get-ChildItem -Path '${cacheDir}' -Recurse -Filter 'postgrest.exe' | Select-Object -First 1; ` +
886
+ `if ($exe) { ` +
887
+ ` Copy-Item -Path $exe.FullName -Destination '${binPath}' -Force; ` +
888
+ ` Get-ChildItem -Path $exe.Directory.FullName -Filter '*.dll' | ` +
889
+ ` ForEach-Object { Copy-Item -Path $_.FullName -Destination '${cacheDir}' -Force }; ` +
890
+ `}`,
891
+ ],
892
+ { stdio: "pipe", encoding: "utf8" },
434
893
  )
894
+ return r.status === 0
435
895
  }
436
896
 
437
- async function watchAndPush(cwd: string): Promise<void> {
438
- const config = loadConfig(cwd)
439
- const schemaDir = resolve(cwd, config.schema, "..")
897
+ // ---------------------------------------------------------------------------
898
+ // Local-dev JWT generator (no external dep — pure crypto)
899
+ // ---------------------------------------------------------------------------
440
900
 
441
- console.log(`Watching ${schemaDir} for changes... (Ctrl+C to stop)\n`)
442
901
 
443
- // Initial push on start
444
- await runPush(cwd)
902
+ // ---------------------------------------------------------------------------
903
+ // AST adaptation — replace extension-dependent fields with JSONB fallbacks
904
+ // ---------------------------------------------------------------------------
445
905
 
446
- const { watch } = await import("node:fs")
447
- watch(schemaDir, { recursive: true }, (eventType, filename) => {
448
- if (!filename?.endsWith(".ts")) return
449
- console.log(`\nChange detected in ${filename}, pushing...`)
450
- runPush(cwd).catch((e: unknown) =>
451
- console.error("Push failed:", (e as Error).message),
452
- )
906
+ interface AstField { kind: string; required?: boolean; [k: string]: unknown }
907
+ interface AstModel { name: string; fields?: Record<string, AstField> }
908
+ interface AstSchema { models?: AstModel[] }
909
+
910
+ // Field kinds that require Postgres extensions not available in all builds.
911
+ // Maps kind → { extension name, JSONB fallback AST }
912
+ const EXTENSION_FIELDS: Record<string, { ext: string; fallback: AstField }> = {
913
+ geo: { ext: "PostGIS", fallback: { kind: "json", pgType: "JSONB" } },
914
+ vector: { ext: "pgvector", fallback: { kind: "json", pgType: "JSONB" } },
915
+ }
916
+
917
+ function adaptUnsupportedKinds(
918
+ ast: unknown,
919
+ skipKinds: ReadonlySet<string>,
920
+ ): { filtered: unknown; adapted: string[] } {
921
+ const adapted: string[] = []
922
+ if (!ast || typeof ast !== "object") return { filtered: ast, adapted }
923
+ const schema = ast as AstSchema
924
+ if (!Array.isArray(schema.models)) return { filtered: ast, adapted }
925
+
926
+ const models = schema.models.map((model) => {
927
+ const fields: Record<string, AstField> = {}
928
+ for (const [name, field] of Object.entries(model.fields ?? {})) {
929
+ const info = skipKinds.has(field.kind) ? EXTENSION_FIELDS[field.kind] : undefined
930
+ if (info) {
931
+ fields[name] = { ...info.fallback, required: field.required ?? false }
932
+ adapted.push(`${model.name}.${name} (${info.ext} → JSONB)`)
933
+ } else {
934
+ fields[name] = field
935
+ }
936
+ }
937
+ return { ...model, fields }
453
938
  })
454
939
 
455
- // Block forever
456
- await new Promise<never>(() => undefined)
940
+ return { filtered: { ...schema, models }, adapted }
457
941
  }
458
942
 
459
- async function runPush(cwd: string): Promise<void> {
460
- const { loadConfig, loadSchemaAst } = await import("../config.js")
461
- const config = loadConfig(cwd)
462
- const ast = loadSchemaAst(config.schema, cwd)
463
- await ensureEngine()
464
- const result = invokeEngine(
465
- ["migrate", "--connection", config.connection],
466
- JSON.stringify(ast),
467
- )
468
- if (result.exitCode !== 0) {
469
- console.error(result.stderr || result.stdout)
470
- return
943
+ // ---------------------------------------------------------------------------
944
+ // .env loader
945
+ // ---------------------------------------------------------------------------
946
+
947
+ /** Minimal GoTrue env for `migrate` (matches required fields in serverEnv below). */
948
+ function gotrueMigrateEnv(
949
+ serverPort: string,
950
+ sqlDbURL: string,
951
+ jwtSecret: string,
952
+ ): Record<string, string> {
953
+ const base = `http://localhost:${serverPort}`
954
+ return {
955
+ // envconfig: gotrue + DB.DATABASE_URL → GOTRUE_DB_DATABASE_URL
956
+ GOTRUE_DB_DATABASE_URL: sqlDbURL,
957
+ DATABASE_URL: sqlDbURL,
958
+ GOTRUE_DB_DRIVER: "postgres",
959
+ GOTRUE_DB_NAMESPACE: "auth",
960
+ PGSSLMODE: "disable",
961
+ GOTRUE_JWT_SECRET: jwtSecret,
962
+ API_EXTERNAL_URL: `${base}/auth/v1`,
963
+ GOTRUE_API_HOST: "localhost",
964
+ GOTRUE_SITE_URL: base,
965
+ GOTRUE_MAILER_AUTOCONFIRM: "true",
966
+ }
967
+ }
968
+
969
+ /** Apply GoTrue DDL (auth.users, etc.) before engine push references auth schema. */
970
+ function runGotrueMigrations(
971
+ serverBin: string,
972
+ migrateEnv: Record<string, string>,
973
+ ): void {
974
+ const result = spawnSync(serverBin, ["migrate"], {
975
+ stdio: "pipe",
976
+ encoding: "utf8",
977
+ env: {
978
+ ...process.env,
979
+ ...migrateEnv,
980
+ },
981
+ })
982
+ if (result.status !== 0) {
983
+ const detail = (result.stderr ?? result.stdout ?? "").trim()
984
+ throw new Error(
985
+ `GoTrue migrations failed (exit ${result.status ?? "unknown"})` +
986
+ (detail ? `:\n${detail}` : ""),
987
+ )
471
988
  }
472
- console.log(result.stdout || "Schema up to date.")
473
989
  }
474
990
 
475
- function sleep(ms: number): Promise<void> {
476
- return new Promise((r) => setTimeout(r, ms))
991
+ function loadDotEnv(cwd: string): Record<string, string> {
992
+ const candidates = [resolve(cwd, ".env"), resolve(cwd, ".env.local")]
993
+ const vars: Record<string, string> = {}
994
+ for (const envPath of candidates) {
995
+ if (!existsSync(envPath)) continue
996
+ for (const line of readFileSync(envPath, "utf8").split("\n")) {
997
+ const trimmed = line.trim()
998
+ if (!trimmed || trimmed.startsWith("#")) continue
999
+ const eq = trimmed.indexOf("=")
1000
+ if (eq === -1) continue
1001
+ vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1)
1002
+ }
1003
+ }
1004
+ return vars
477
1005
  }