@supatype/cli 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +7 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/bin/dev-entry.ts +2 -0
  5. package/bin/supatype.js +5 -0
  6. package/dist/app/framework.d.ts +44 -0
  7. package/dist/app/framework.d.ts.map +1 -0
  8. package/dist/app/framework.js +200 -0
  9. package/dist/app/framework.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +55 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/admin.d.ts +4 -0
  15. package/dist/commands/admin.d.ts.map +1 -0
  16. package/dist/commands/admin.js +270 -0
  17. package/dist/commands/admin.js.map +1 -0
  18. package/dist/commands/app.d.ts +3 -0
  19. package/dist/commands/app.d.ts.map +1 -0
  20. package/dist/commands/app.js +235 -0
  21. package/dist/commands/app.js.map +1 -0
  22. package/dist/commands/cloud.d.ts +3 -0
  23. package/dist/commands/cloud.d.ts.map +1 -0
  24. package/dist/commands/cloud.js +256 -0
  25. package/dist/commands/cloud.js.map +1 -0
  26. package/dist/commands/db.d.ts +8 -0
  27. package/dist/commands/db.d.ts.map +1 -0
  28. package/dist/commands/db.js +123 -0
  29. package/dist/commands/db.js.map +1 -0
  30. package/dist/commands/deploy-types.d.ts +14 -0
  31. package/dist/commands/deploy-types.d.ts.map +1 -0
  32. package/dist/commands/deploy-types.js +38 -0
  33. package/dist/commands/deploy-types.js.map +1 -0
  34. package/dist/commands/deploy.d.ts +14 -0
  35. package/dist/commands/deploy.d.ts.map +1 -0
  36. package/dist/commands/deploy.js +295 -0
  37. package/dist/commands/deploy.js.map +1 -0
  38. package/dist/commands/dev.d.ts +3 -0
  39. package/dist/commands/dev.d.ts.map +1 -0
  40. package/dist/commands/dev.js +428 -0
  41. package/dist/commands/dev.js.map +1 -0
  42. package/dist/commands/diff.d.ts +3 -0
  43. package/dist/commands/diff.d.ts.map +1 -0
  44. package/dist/commands/diff.js +39 -0
  45. package/dist/commands/diff.js.map +1 -0
  46. package/dist/commands/engine.d.ts +9 -0
  47. package/dist/commands/engine.d.ts.map +1 -0
  48. package/dist/commands/engine.js +99 -0
  49. package/dist/commands/engine.js.map +1 -0
  50. package/dist/commands/functions.d.ts +3 -0
  51. package/dist/commands/functions.d.ts.map +1 -0
  52. package/dist/commands/functions.js +762 -0
  53. package/dist/commands/functions.js.map +1 -0
  54. package/dist/commands/generate.d.ts +3 -0
  55. package/dist/commands/generate.d.ts.map +1 -0
  56. package/dist/commands/generate.js +28 -0
  57. package/dist/commands/generate.js.map +1 -0
  58. package/dist/commands/init.d.ts +7 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +515 -0
  61. package/dist/commands/init.js.map +1 -0
  62. package/dist/commands/keys.d.ts +4 -0
  63. package/dist/commands/keys.d.ts.map +1 -0
  64. package/dist/commands/keys.js +57 -0
  65. package/dist/commands/keys.js.map +1 -0
  66. package/dist/commands/logs.d.ts +6 -0
  67. package/dist/commands/logs.d.ts.map +1 -0
  68. package/dist/commands/logs.js +52 -0
  69. package/dist/commands/logs.js.map +1 -0
  70. package/dist/commands/migrate.d.ts +3 -0
  71. package/dist/commands/migrate.d.ts.map +1 -0
  72. package/dist/commands/migrate.js +71 -0
  73. package/dist/commands/migrate.js.map +1 -0
  74. package/dist/commands/plugins.d.ts +3 -0
  75. package/dist/commands/plugins.d.ts.map +1 -0
  76. package/dist/commands/plugins.js +431 -0
  77. package/dist/commands/plugins.js.map +1 -0
  78. package/dist/commands/pull.d.ts +3 -0
  79. package/dist/commands/pull.d.ts.map +1 -0
  80. package/dist/commands/pull.js +73 -0
  81. package/dist/commands/pull.js.map +1 -0
  82. package/dist/commands/push.d.ts +3 -0
  83. package/dist/commands/push.d.ts.map +1 -0
  84. package/dist/commands/push.js +87 -0
  85. package/dist/commands/push.js.map +1 -0
  86. package/dist/commands/seed.d.ts +3 -0
  87. package/dist/commands/seed.d.ts.map +1 -0
  88. package/dist/commands/seed.js +22 -0
  89. package/dist/commands/seed.js.map +1 -0
  90. package/dist/commands/self-host.d.ts +3 -0
  91. package/dist/commands/self-host.d.ts.map +1 -0
  92. package/dist/commands/self-host.js +796 -0
  93. package/dist/commands/self-host.js.map +1 -0
  94. package/dist/commands/status.d.ts +6 -0
  95. package/dist/commands/status.d.ts.map +1 -0
  96. package/dist/commands/status.js +69 -0
  97. package/dist/commands/status.js.map +1 -0
  98. package/dist/config.d.ts +106 -0
  99. package/dist/config.d.ts.map +1 -0
  100. package/dist/config.js +66 -0
  101. package/dist/config.js.map +1 -0
  102. package/dist/engine/cache.d.ts +37 -0
  103. package/dist/engine/cache.d.ts.map +1 -0
  104. package/dist/engine/cache.js +121 -0
  105. package/dist/engine/cache.js.map +1 -0
  106. package/dist/engine/download.d.ts +19 -0
  107. package/dist/engine/download.d.ts.map +1 -0
  108. package/dist/engine/download.js +108 -0
  109. package/dist/engine/download.js.map +1 -0
  110. package/dist/engine/platform.d.ts +24 -0
  111. package/dist/engine/platform.d.ts.map +1 -0
  112. package/dist/engine/platform.js +50 -0
  113. package/dist/engine/platform.js.map +1 -0
  114. package/dist/engine/resolve.d.ts +37 -0
  115. package/dist/engine/resolve.d.ts.map +1 -0
  116. package/dist/engine/resolve.js +133 -0
  117. package/dist/engine/resolve.js.map +1 -0
  118. package/dist/engine/update-notify.d.ts +11 -0
  119. package/dist/engine/update-notify.d.ts.map +1 -0
  120. package/dist/engine/update-notify.js +43 -0
  121. package/dist/engine/update-notify.js.map +1 -0
  122. package/dist/engine/verify.d.ts +50 -0
  123. package/dist/engine/verify.d.ts.map +1 -0
  124. package/dist/engine/verify.js +161 -0
  125. package/dist/engine/verify.js.map +1 -0
  126. package/dist/engine-version.d.ts +35 -0
  127. package/dist/engine-version.d.ts.map +1 -0
  128. package/dist/engine-version.js +35 -0
  129. package/dist/engine-version.js.map +1 -0
  130. package/dist/engine.d.ts +34 -0
  131. package/dist/engine.d.ts.map +1 -0
  132. package/dist/engine.js +76 -0
  133. package/dist/engine.js.map +1 -0
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +10 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/jwt.d.ts +3 -0
  139. package/dist/jwt.d.ts.map +1 -0
  140. package/dist/jwt.js +13 -0
  141. package/dist/jwt.js.map +1 -0
  142. package/dist/pull-utils.d.ts +16 -0
  143. package/dist/pull-utils.d.ts.map +1 -0
  144. package/dist/pull-utils.js +65 -0
  145. package/dist/pull-utils.js.map +1 -0
  146. package/dist/scripts/postinstall.d.ts +12 -0
  147. package/dist/scripts/postinstall.d.ts.map +1 -0
  148. package/dist/scripts/postinstall.js +31 -0
  149. package/dist/scripts/postinstall.js.map +1 -0
  150. package/dist/tsx-runner.d.ts +18 -0
  151. package/dist/tsx-runner.d.ts.map +1 -0
  152. package/dist/tsx-runner.js +62 -0
  153. package/dist/tsx-runner.js.map +1 -0
  154. package/package.json +36 -0
  155. package/src/app/framework.ts +249 -0
  156. package/src/cli.ts +58 -0
  157. package/src/commands/admin.ts +371 -0
  158. package/src/commands/app.ts +261 -0
  159. package/src/commands/cloud.ts +326 -0
  160. package/src/commands/db.ts +145 -0
  161. package/src/commands/deploy-types.ts +49 -0
  162. package/src/commands/deploy.ts +366 -0
  163. package/src/commands/dev.ts +477 -0
  164. package/src/commands/diff.ts +61 -0
  165. package/src/commands/engine.ts +133 -0
  166. package/src/commands/functions.ts +919 -0
  167. package/src/commands/generate.ts +31 -0
  168. package/src/commands/init.ts +532 -0
  169. package/src/commands/keys.ts +66 -0
  170. package/src/commands/logs.ts +58 -0
  171. package/src/commands/migrate.ts +83 -0
  172. package/src/commands/plugins.ts +508 -0
  173. package/src/commands/pull.ts +96 -0
  174. package/src/commands/push.ts +119 -0
  175. package/src/commands/seed.ts +26 -0
  176. package/src/commands/self-host.ts +932 -0
  177. package/src/commands/status.ts +83 -0
  178. package/src/config.ts +190 -0
  179. package/src/engine/cache.ts +135 -0
  180. package/src/engine/download.ts +143 -0
  181. package/src/engine/platform.ts +66 -0
  182. package/src/engine/resolve.ts +197 -0
  183. package/src/engine/update-notify.ts +50 -0
  184. package/src/engine/verify.ts +206 -0
  185. package/src/engine-version.ts +39 -0
  186. package/src/engine.ts +99 -0
  187. package/src/index.ts +19 -0
  188. package/src/jwt.ts +14 -0
  189. package/src/pull-utils.ts +57 -0
  190. package/src/scripts/postinstall.ts +40 -0
  191. package/src/tsx-runner.ts +79 -0
  192. package/tests/cli-help.test.ts +107 -0
  193. package/tests/config.test.ts +117 -0
  194. package/tests/engine-distribution.test.ts +418 -0
  195. package/tests/init.test.ts +184 -0
  196. package/tests/keys.test.ts +160 -0
  197. package/tests/pull-utils.test.ts +115 -0
  198. package/tests/tsx-runner.test.ts +66 -0
  199. package/tsconfig.json +10 -0
  200. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,477 @@
1
+ import type { Command } from "commander"
2
+ import { spawnSync, spawn, type ChildProcess } from "node:child_process"
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
4
+ import { resolve } from "node:path"
5
+ 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
11
+
12
+ export function registerDev(program: Command): void {
13
+ program
14
+ .command("dev")
15
+ .description(
16
+ "Start local Postgres, PostgREST, and Kong via Docker Compose, then watch for schema changes",
17
+ )
18
+ .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 }) => {
21
+ const cwd = process.cwd()
22
+
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)
31
+ }
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" },
37
+ )
38
+ if (up.status !== 0) {
39
+ console.error("docker compose up failed.")
40
+ process.exit(1)
41
+ }
42
+ } else {
43
+ if (!existsSync(resolve(cwd, "docker-compose.yml"))) {
44
+ console.error(
45
+ "docker-compose.yml not found. Run: supatype init",
46
+ )
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
+ }
59
+ }
60
+
61
+ console.log("Waiting for PostgREST to be ready...")
62
+ await waitForPostgREST()
63
+
64
+ const children: ChildProcess[] = []
65
+
66
+ if (opts.local) {
67
+ console.log("\nStarting local services from source...")
68
+ children.push(
69
+ ...startLocalServices(cwd),
70
+ )
71
+ }
72
+
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)")
83
+ }
84
+ console.log()
85
+
86
+ // Clean shutdown on Ctrl+C
87
+ const cleanup = () => {
88
+ for (const child of children) {
89
+ child.kill()
90
+ }
91
+ process.exit(0)
92
+ }
93
+ process.on("SIGINT", cleanup)
94
+ process.on("SIGTERM", cleanup)
95
+
96
+ if (opts.watch) {
97
+ await watchAndPush(cwd)
98
+ }
99
+ })
100
+ }
101
+
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")
232
+ }
233
+
234
+ if (!existsSync(resolve(supatypeDir, "userlist.txt"))) {
235
+ writeFileSync(resolve(supatypeDir, "userlist.txt"), "", "utf8")
236
+ }
237
+
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")
290
+ }
291
+ }
292
+
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")
324
+ }
325
+
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
338
+ }
339
+ return vars
340
+ }
341
+
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",
359
+ }
360
+ // .env file values override defaults
361
+ const dotEnv = loadDotEnv(cwd)
362
+ return { ...defaults, ...dotEnv }
363
+ }
364
+
365
+ function startLocalServices(cwd: string): ChildProcess[] {
366
+ const children: ChildProcess[] = []
367
+ const devEnv = localDevDefaults(cwd)
368
+
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
+ ]
374
+
375
+ for (const svc of services) {
376
+ const pkgDir = resolve(cwd, "..", "packages", svc.name)
377
+
378
+ if (!existsSync(resolve(pkgDir, "package.json"))) {
379
+ console.warn(` Skipping ${svc.name} — not found at ${pkgDir}`)
380
+ continue
381
+ }
382
+
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
+ })
396
+
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
+ })
412
+
413
+ children.push(child)
414
+ console.log(` ${prefix} started (pnpm dev)`)
415
+ }
416
+
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
428
+ }
429
+ await sleep(HEALTH_POLL_MS)
430
+ }
431
+ throw new Error(
432
+ `PostgREST did not become healthy within ${HEALTH_TIMEOUT_MS / 1000}s.\n` +
433
+ "Check: docker compose logs postgrest",
434
+ )
435
+ }
436
+
437
+ async function watchAndPush(cwd: string): Promise<void> {
438
+ const config = loadConfig(cwd)
439
+ const schemaDir = resolve(cwd, config.schema, "..")
440
+
441
+ console.log(`Watching ${schemaDir} for changes... (Ctrl+C to stop)\n`)
442
+
443
+ // Initial push on start
444
+ await runPush(cwd)
445
+
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
+ )
453
+ })
454
+
455
+ // Block forever
456
+ await new Promise<never>(() => undefined)
457
+ }
458
+
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
471
+ }
472
+ console.log(result.stdout || "Schema up to date.")
473
+ }
474
+
475
+ function sleep(ms: number): Promise<void> {
476
+ return new Promise((r) => setTimeout(r, ms))
477
+ }
@@ -0,0 +1,61 @@
1
+ import type { Command } from "commander"
2
+ import { loadConfig, loadSchemaAst } from "../config.js"
3
+ import { ensureEngine, invokeEngine } from "../engine.js"
4
+
5
+ interface DiffResult {
6
+ operations: Array<{
7
+ kind: string
8
+ risk: "safe" | "cautious" | "destructive"
9
+ description: string
10
+ }>
11
+ }
12
+
13
+ export function registerDiff(program: Command): void {
14
+ program
15
+ .command("diff")
16
+ .description("Show planned schema changes without applying them (dry run)")
17
+ .option("--connection <url>", "Database connection URL (overrides config)")
18
+ .action(async (opts: { connection?: string }) => {
19
+ const cwd = process.cwd()
20
+ const config = loadConfig(cwd)
21
+ const connection = opts.connection ?? config.connection
22
+
23
+ await ensureEngine()
24
+
25
+ console.log("Loading schema...")
26
+ const ast = loadSchemaAst(config.schema, cwd)
27
+
28
+ const result = invokeEngine(
29
+ ["diff", "--connection", connection, "--format", "json"],
30
+ JSON.stringify(ast),
31
+ )
32
+ if (result.exitCode !== 0) {
33
+ console.error(result.stderr || result.stdout)
34
+ process.exit(1)
35
+ }
36
+
37
+ const diff = JSON.parse(result.stdout) as DiffResult
38
+ const ops = diff.operations ?? []
39
+
40
+ if (ops.length === 0) {
41
+ console.log("No changes.")
42
+ return
43
+ }
44
+
45
+ const symbol = { safe: "+", cautious: "~", destructive: "!" }
46
+ const legend = { safe: "safe", cautious: "cautious", destructive: "DESTRUCTIVE" }
47
+
48
+ console.log(`\n${ops.length} change(s):\n`)
49
+ for (const op of ops) {
50
+ console.log(
51
+ ` [${symbol[op.risk]}] ${op.description} (${legend[op.risk]})`,
52
+ )
53
+ }
54
+
55
+ const destructive = ops.filter((o) => o.risk === "destructive").length
56
+ if (destructive > 0) {
57
+ console.log(`\n ${destructive} destructive operation(s). Run with --yes to skip confirmation.`)
58
+ }
59
+ console.log()
60
+ })
61
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Engine management commands:
3
+ * supatype engine version — show pinned, cached, and latest versions
4
+ * supatype engine update-check — check for newer engine versions
5
+ * supatype engine prune — remove old cached engine versions
6
+ */
7
+
8
+ import type { Command } from "commander"
9
+ import { ENGINE_VERSION } from "../engine-version.js"
10
+ import { detectPlatform } from "../engine/platform.js"
11
+ import {
12
+ hasCachedBinary,
13
+ listCachedVersions,
14
+ pruneCacheExcept,
15
+ saveUpdateCheck,
16
+ } from "../engine/cache.js"
17
+ import { checkLatestVersion } from "../engine/resolve.js"
18
+
19
+ export function registerEngine(program: Command): void {
20
+ const engine = program
21
+ .command("engine")
22
+ .description("Manage the Supatype engine binary")
23
+
24
+ // supatype engine version
25
+ engine
26
+ .command("version")
27
+ .description("Show engine version information")
28
+ .action(async () => {
29
+ const platform = detectPlatform()
30
+ const cached = hasCachedBinary(ENGINE_VERSION, platform)
31
+ const cachedVersions = listCachedVersions()
32
+
33
+ console.log(`Engine: v${ENGINE_VERSION} (pinned)`)
34
+ console.log(
35
+ `Cache: ${cached ? `v${ENGINE_VERSION} ✓` : "not downloaded"}`,
36
+ )
37
+
38
+ if (cachedVersions.length > 1) {
39
+ const others = cachedVersions.filter((v) => v !== ENGINE_VERSION)
40
+ console.log(`Other cached versions: ${others.join(", ")}`)
41
+ }
42
+
43
+ // Check latest
44
+ try {
45
+ const latest = await checkLatestVersion()
46
+ if (latest) {
47
+ await saveUpdateCheck(latest.version)
48
+ if (latest.version !== ENGINE_VERSION) {
49
+ console.log(
50
+ `Latest: v${latest.version} — update available, run: npm update @supatype/cli`,
51
+ )
52
+ } else {
53
+ console.log(`Latest: v${latest.version} (up to date)`)
54
+ }
55
+ }
56
+ } catch {
57
+ console.log("Latest: unable to check (offline?)")
58
+ }
59
+ })
60
+
61
+ // supatype engine update-check
62
+ engine
63
+ .command("update-check")
64
+ .description("Check if a newer engine version is available")
65
+ .action(async () => {
66
+ const latest = await checkLatestVersion()
67
+
68
+ if (!latest) {
69
+ console.error(
70
+ "Could not check for updates. Check your internet connection.",
71
+ )
72
+ process.exitCode = 1
73
+ return
74
+ }
75
+
76
+ await saveUpdateCheck(latest.version)
77
+
78
+ if (latest.version !== ENGINE_VERSION) {
79
+ console.log(
80
+ `Supatype engine v${latest.version} is available (current: v${ENGINE_VERSION}).`,
81
+ )
82
+ console.log(`Run: npm update @supatype/cli`)
83
+ } else {
84
+ console.log(`Engine v${ENGINE_VERSION} is up to date.`)
85
+ }
86
+ })
87
+
88
+ // supatype engine prune
89
+ engine
90
+ .command("prune")
91
+ .description("Remove all cached engine versions except the current one")
92
+ .action(() => {
93
+ const { removed, bytesFreed } = pruneCacheExcept(ENGINE_VERSION)
94
+
95
+ if (removed.length === 0) {
96
+ console.log("Nothing to prune — only the current version is cached.")
97
+ return
98
+ }
99
+
100
+ const mb = (bytesFreed / (1024 * 1024)).toFixed(1)
101
+ console.log(`Removed ${removed.length} cached version(s): ${removed.join(", ")}`)
102
+ console.log(`Space reclaimed: ${mb}MB`)
103
+ })
104
+
105
+ // supatype engine versions (list all released versions)
106
+ engine
107
+ .command("versions")
108
+ .description("List all released engine versions")
109
+ .action(async () => {
110
+ const { fetchJson } = await import("../engine/download.js")
111
+ const { CDN_BASE_URL } = await import("../engine-version.js")
112
+
113
+ interface VersionEntry {
114
+ version: string
115
+ date: string
116
+ }
117
+
118
+ const versions = await fetchJson<VersionEntry[]>(
119
+ `${CDN_BASE_URL}/versions.json`,
120
+ )
121
+
122
+ if (!versions || versions.length === 0) {
123
+ console.log("No released versions found.")
124
+ return
125
+ }
126
+
127
+ console.log("Released engine versions:")
128
+ for (const v of versions) {
129
+ const current = v.version === ENGINE_VERSION ? " (current)" : ""
130
+ console.log(` v${v.version} ${v.date}${current}`)
131
+ }
132
+ })
133
+ }