@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,932 +1,364 @@
1
- import type { Command } from "commander"
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- writeFileSync,
6
- readFileSync,
7
- copyFileSync,
8
- } from "node:fs"
9
- import { resolve, join } from "node:path"
10
- import { randomBytes } from "node:crypto"
1
+ /**
2
+ * self-host commands — manage self-hosted deployments.
3
+ *
4
+ * Compose-based commands are the canonical path.
5
+ * Native/systemd commands are kept temporarily for migration compatibility.
6
+ */
7
+
8
+ import { Command } from "commander"
9
+ import { existsSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from "node:fs"
10
+ import { join, resolve } from "node:path"
11
+ import { homedir } from "node:os"
11
12
  import { spawnSync } from "node:child_process"
12
- import { signJwt } from "../jwt.js"
13
- import type { SelfHostConfig, ServiceVersionPin } from "../config.js"
13
+ import { gzipSync } from "node:zlib"
14
+ import { loadConfig } from "../config.js"
15
+ import { connectionString } from "../project-config.js"
16
+ import { resolveBinary } from "../binary-cache.js"
17
+ import { generateUnits } from "../systemd.js"
18
+ import { readPid } from "../process-manager.js"
19
+ import { localStorageEnv } from "../local-storage.js"
20
+ import { runDockerCompose, writeSelfHostCompose } from "../self-host-compose.js"
14
21
 
15
22
  export function registerSelfHost(program: Command): void {
16
23
  const selfHostCmd = program
17
24
  .command("self-host")
18
- .description("Manage self-hosted production deployments")
25
+ .description("Manage self-hosted deployments (Docker Compose only)")
19
26
 
20
- selfHostCmd
21
- .command("setup")
22
- .description("Generate a production-ready deploy/ directory with Caddy, PgBouncer, and all secrets")
23
- .option("--domain <domain>", "Production domain (e.g. api.example.com)")
24
- .option("--app-dockerfile <path>", "Path to your app Dockerfile (omit to skip app service)")
25
- .option("--app-port <port>", "Port your app listens on", "3000")
26
- .option("--ssl-email <email>", "Email address for Let's Encrypt registration")
27
- .action(async (opts: { domain?: string; appDockerfile?: string; appPort: string; sslEmail?: string }) => {
28
- await setup(process.cwd(), opts)
29
- })
27
+ const composeCmd = selfHostCmd
28
+ .command("compose")
29
+ .description("Manage compose-based self-host runtime")
30
30
 
31
- selfHostCmd
32
- .command("status")
33
- .description("Show running service health for the production stack")
31
+ composeCmd
32
+ .command("render")
33
+ .description("Render deterministic self-host compose artifacts")
34
34
  .action(() => {
35
- runDockerCompose(["ps", "--format", "table"], "status")
35
+ const cwd = process.cwd()
36
+ const config = loadConfig(cwd)
37
+ const out = writeSelfHostCompose(cwd, config)
38
+ console.log(`Wrote ${out.composePath}`)
39
+ console.log(`Wrote ${out.kongPath}`)
36
40
  })
37
41
 
38
- selfHostCmd
39
- .command("logs")
40
- .description("Tail logs from production services")
41
- .option("--service <name>", "Show logs for a specific service only")
42
- .option("--follow", "Follow log output")
43
- .action((opts: { service?: string; follow?: boolean }) => {
44
- const args = ["logs"]
45
- if (opts.follow) args.push("--follow")
46
- if (opts.service) args.push(opts.service)
47
- runDockerCompose(args, "logs")
42
+ composeCmd
43
+ .command("up")
44
+ .description("Render and start compose services")
45
+ .option("-d, --detach", "Start in detached mode", true)
46
+ .action((opts: { detach?: boolean }) => {
47
+ const cwd = process.cwd()
48
+ const config = loadConfig(cwd)
49
+ const out = writeSelfHostCompose(cwd, config)
50
+ const status = runDockerCompose(out.composePath, opts.detach ? ["up", "-d"] : ["up"], cwd)
51
+ process.exitCode = status
48
52
  })
49
53
 
50
- selfHostCmd
51
- .command("backup")
52
- .description("Create a Postgres dump and store it locally")
53
- .option("--output <path>", "Output file path", `./backups/backup-${timestamp()}.sql.gz`)
54
- .action((opts: { output: string }) => {
55
- backup(process.cwd(), opts.output)
54
+ composeCmd
55
+ .command("down")
56
+ .description("Stop compose services")
57
+ .action(() => {
58
+ const cwd = process.cwd()
59
+ const config = loadConfig(cwd)
60
+ const out = writeSelfHostCompose(cwd, config)
61
+ process.exitCode = runDockerCompose(out.composePath, ["down"], cwd)
56
62
  })
57
63
 
58
- selfHostCmd
59
- .command("update")
60
- .description("Pull latest images and restart the production stack (use 'upgrade' for safe rolling upgrades)")
64
+ composeCmd
65
+ .command("status")
66
+ .description("Show compose service status")
61
67
  .action(() => {
62
- update(process.cwd())
68
+ const cwd = process.cwd()
69
+ const config = loadConfig(cwd)
70
+ const out = writeSelfHostCompose(cwd, config)
71
+ process.exitCode = runDockerCompose(out.composePath, ["ps"], cwd)
63
72
  })
64
73
 
65
- selfHostCmd
66
- .command("upgrade")
67
- .description("Safely upgrade services with backup, rolling restart, and automatic rollback")
68
- .option("--skip-backup", "Skip automatic pre-upgrade backup")
69
- .option("--skip-migrations", "Skip database migration step")
70
- .action(async (opts: { skipBackup?: boolean; skipMigrations?: boolean }) => {
71
- await upgrade(process.cwd(), opts)
74
+ composeCmd
75
+ .command("logs")
76
+ .description("Tail compose logs")
77
+ .option("--service <name>", "Filter to one service")
78
+ .option("-f, --follow", "Follow log output", true)
79
+ .action((opts: { service?: string; follow?: boolean }) => {
80
+ const cwd = process.cwd()
81
+ const config = loadConfig(cwd)
82
+ const out = writeSelfHostCompose(cwd, config)
83
+ const args = ["logs"]
84
+ if (opts.follow) args.push("-f")
85
+ if (opts.service) args.push(opts.service)
86
+ process.exitCode = runDockerCompose(out.composePath, args, cwd)
72
87
  })
73
- }
74
-
75
- // ─── Setup ────────────────────────────────────────────────────────────────────
76
88
 
77
- interface SetupOpts {
78
- domain?: string
79
- appDockerfile?: string
80
- appPort: string
81
- sslEmail?: string
82
- }
89
+ // ── Legacy native/systemd helpers (hidden; use compose for self-host) ─────
83
90
 
84
- async function fetchLatestTag(repo: string, fallback: string): Promise<string> {
85
- try {
86
- const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
87
- headers: { Accept: "application/vnd.github+json" },
88
- signal: AbortSignal.timeout(5000),
89
- })
90
- if (!res.ok) return fallback
91
- const data = await res.json() as { tag_name?: string }
92
- return data.tag_name ?? fallback
93
- } catch {
94
- return fallback
95
- }
96
- }
91
+ const legacyCmd = new Command("native")
92
+ selfHostCmd.addCommand(legacyCmd, { hidden: true })
97
93
 
98
- async function setup(cwd: string, opts: SetupOpts): Promise<void> {
99
- // Load domain from opts or supatype.config.ts
100
- const domain = opts.domain ?? loadDomainFromConfig(cwd)
101
- if (!domain) {
102
- console.error(
103
- "Error: --domain is required (or set selfHost.domain in supatype.config.ts)",
94
+ legacyCmd
95
+ .command(
96
+ "install-service",
97
+ "Generate systemd unit files and (on Linux) install + enable them",
104
98
  )
105
- process.exit(1)
106
- }
107
-
108
- console.log("Fetching latest image versions...")
109
- const [postgresTag, authTag] = await Promise.all([
110
- fetchLatestTag("supatype/postgres", "17-latest"),
111
- fetchLatestTag("supatype/auth", "v1.0.0"),
112
- ])
113
- console.log(` postgres supatype/postgres:${postgresTag}`)
114
- console.log(` auth supatype/auth:${authTag}`)
115
-
116
- const deployDir = resolve(cwd, "deploy")
117
- mkdirSync(deployDir, { recursive: true })
118
-
119
- const write = (rel: string, content: string) => {
120
- const full = join(deployDir, rel)
121
- mkdirSync(resolve(full, ".."), { recursive: true })
122
- writeFileSync(full, content, "utf8")
123
- console.log(` created deploy/${rel}`)
124
- }
125
-
126
- // Generate all secrets
127
- const pgPassword = randomBytes(24).toString("hex")
128
- const jwtSecret = randomBytes(32).toString("hex")
129
- const now = Math.floor(Date.now() / 1000)
130
- const exp = now + 10 * 365 * 24 * 60 * 60 // 10 years
131
- const anonKey = signJwt({ iss: "supatype", role: "anon", iat: now, exp }, jwtSecret)
132
- const serviceKey = signJwt({ iss: "supatype", role: "service_role", iat: now, exp }, jwtSecret)
133
-
134
- console.log("\nGenerating production deployment files...\n")
135
-
136
- write(".env.production", envProductionTemplate(domain, pgPassword, jwtSecret, anonKey, serviceKey))
137
- write("docker-compose.yml", productionComposeTemplate(domain, opts, postgresTag, authTag))
138
- write("Caddyfile", caddyfileTemplate(domain, opts.sslEmail))
139
- write("pgbouncer.ini", productionPgbouncerIni())
140
- write("userlist.txt", productionUserlist(pgPassword))
141
- write("deploy.sh", deployScript(domain))
142
-
143
- // Copy kong.yml if it exists
144
- const kongSrc = resolve(cwd, ".supatype/kong.yml")
145
- if (existsSync(kongSrc)) {
146
- copyFileSync(kongSrc, join(deployDir, "kong.yml"))
147
- console.log(" copied deploy/kong.yml")
148
- }
149
-
150
- // Make deploy.sh executable on Unix
151
- try {
152
- spawnSync("chmod", ["+x", join(deployDir, "deploy.sh")])
153
- } catch { /* non-Unix, ignore */ }
154
-
155
- console.log(`
156
- ╔══════════════════════════════════════════════════════════════╗
157
- ║ SAVE THESE SECRETS — they will not be shown again! ║
158
- ╚══════════════════════════════════════════════════════════════╝
159
-
160
- POSTGRES_PASSWORD=${pgPassword}
161
- JWT_SECRET=${jwtSecret}
162
- ANON_KEY=${anonKey}
163
- SERVICE_ROLE_KEY=${serviceKey}
164
-
165
- These are also written to deploy/.env.production — back it up securely.
166
- DO NOT commit deploy/.env.production to source control.
167
-
168
- Next steps:
169
- 1. Copy the deploy/ directory to your VPS
170
- 2. SSH into the VPS and run: bash deploy.sh
171
- 3. Your app will be live at https://${domain}
172
- `)
173
- }
174
-
175
- // ─── Operations ───────────────────────────────────────────────────────────────
176
-
177
- function runDockerCompose(args: string[], label: string): void {
178
- const deployDir = resolve(process.cwd(), "deploy")
179
- if (!existsSync(join(deployDir, "docker-compose.yml"))) {
180
- console.error("deploy/docker-compose.yml not found. Run: supatype self-host setup")
181
- process.exit(1)
182
- }
183
- const result = spawnSync("docker", ["compose", "-f", join(deployDir, "docker-compose.yml"), ...args], {
184
- stdio: "inherit",
185
- cwd: deployDir,
186
- })
187
- if (result.status !== 0) process.exit(result.status ?? 1)
188
- }
189
-
190
- function backup(cwd: string, outputPath: string): void {
191
- const deployDir = resolve(cwd, "deploy")
192
- if (!existsSync(join(deployDir, "docker-compose.yml"))) {
193
- console.error("deploy/docker-compose.yml not found. Run: supatype self-host setup")
194
- process.exit(1)
195
- }
196
-
197
- const fullOutput = resolve(cwd, outputPath)
198
- mkdirSync(resolve(fullOutput, ".."), { recursive: true })
199
-
200
- console.log(`Backing up database to ${outputPath}...`)
201
- const result = spawnSync(
202
- "docker",
203
- [
204
- "compose",
205
- "-f", join(deployDir, "docker-compose.yml"),
206
- "exec", "-T", "db",
207
- "sh", "-c", "pg_dumpall -U postgres | gzip",
208
- ],
209
- { cwd: deployDir, encoding: "buffer" },
210
- )
211
-
212
- if (result.status !== 0) {
213
- console.error("Backup failed:", result.stderr?.toString())
214
- process.exit(1)
215
- }
99
+ .option("--output-dir <path>", "Write unit files here instead of /etc/systemd/system/")
100
+ .option("--user <name>", "User to run services as")
101
+ .option("--no-enable", "Generate unit files but do not enable/start them")
102
+ .action(
103
+ async (opts: {
104
+ outputDir?: string
105
+ user?: string
106
+ enable: boolean
107
+ }) => {
108
+ logLegacyWarning("install-service")
109
+ const cwd = process.cwd()
110
+ const config = loadConfig(cwd)
111
+
112
+ const systemdDir = opts.outputDir ?? ".supatype/systemd"
113
+ const absSystemdDir = resolve(cwd, systemdDir)
114
+
115
+ console.log("Generating systemd unit files...")
116
+ const { postgres, server } = generateUnits(config, cwd, {
117
+ outputDir: absSystemdDir,
118
+ ...(opts.user !== undefined && { user: opts.user }),
119
+ })
120
+ console.log(` wrote ${postgres}`)
121
+ console.log(` wrote ${server}`)
122
+
123
+ if (!opts.enable) {
124
+ console.log(
125
+ `\nTo install manually:\n` +
126
+ ` sudo cp ${postgres} /etc/systemd/system/\n` +
127
+ ` sudo cp ${server} /etc/systemd/system/\n` +
128
+ ` sudo systemctl daemon-reload\n` +
129
+ ` sudo systemctl enable --now supatype-postgres supatype-server`,
130
+ )
131
+ return
132
+ }
216
133
 
217
- writeFileSync(fullOutput, result.stdout)
218
- console.log(`Backup saved to ${outputPath}`)
219
- }
134
+ if (process.platform !== "linux") {
135
+ console.log(
136
+ "\nNote: systemd unit installation is only supported on Linux.\n" +
137
+ `Unit files are at ${absSystemdDir}/`,
138
+ )
139
+ return
140
+ }
220
141
 
221
- function update(cwd: string): void {
222
- const deployDir = resolve(cwd, "deploy")
223
- console.log("Pulling latest images...")
224
- spawnSync("docker", ["compose", "-f", join(deployDir, "docker-compose.yml"), "pull"], {
225
- stdio: "inherit",
226
- cwd: deployDir,
227
- })
228
- console.log("Restarting services...")
229
- spawnSync("docker", ["compose", "-f", join(deployDir, "docker-compose.yml"), "up", "-d", "--wait"], {
230
- stdio: "inherit",
231
- cwd: deployDir,
232
- })
233
- console.log("Update complete.")
234
- }
142
+ // Install to /etc/systemd/system/
143
+ console.log("\nInstalling to /etc/systemd/system/ (requires sudo)...")
144
+ const units = [
145
+ { src: postgres, dest: "/etc/systemd/system/supatype-postgres.service" },
146
+ { src: server, dest: "/etc/systemd/system/supatype-server.service" },
147
+ ]
148
+ for (const { src, dest } of units) {
149
+ const cp = spawnSync("sudo", ["cp", src, dest], { stdio: "inherit" })
150
+ if (cp.status !== 0) {
151
+ console.error(`Failed to copy ${src} to ${dest}`)
152
+ process.exit(1)
153
+ }
154
+ }
235
155
 
236
- // ─── Upgrade (safe rolling upgrade) ──────────────────────────────────────────
156
+ const daemonReload = spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" })
157
+ if (daemonReload.status !== 0) { process.exit(1) }
237
158
 
238
- /** Known services and their GitHub repos (for changelog fetching) and Docker images. */
239
- interface ServiceInfo {
240
- composeName: string
241
- image: string
242
- repo: string | null // GitHub org/repo for changelog lookup, null if third-party
243
- fallbackTag: string
244
- }
159
+ const enable = spawnSync(
160
+ "sudo",
161
+ ["systemctl", "enable", "--now", "supatype-postgres", "supatype-server"],
162
+ { stdio: "inherit" },
163
+ )
164
+ if (enable.status !== 0) { process.exit(1) }
245
165
 
246
- const MANAGED_SERVICES: ServiceInfo[] = [
247
- { composeName: "db", image: "supatype/postgres", repo: "supatype/postgres", fallbackTag: "17-latest" },
248
- { composeName: "gotrue", image: "supatype/auth", repo: "supatype/auth", fallbackTag: "v1.0.0" },
249
- { composeName: "postgrest", image: "postgrest/postgrest", repo: "PostgREST/postgrest", fallbackTag: "v12.2.8" },
250
- { composeName: "kong", image: "kong", repo: null, fallbackTag: "3.6" },
251
- { composeName: "caddy", image: "caddy", repo: null, fallbackTag: "2" },
252
- { composeName: "pgbouncer", image: "pgbouncer/pgbouncer", repo: null, fallbackTag: "latest" },
253
- { composeName: "functions", image: "denoland/deno", repo: "denoland/deno", fallbackTag: "latest" },
254
- ]
255
-
256
- function getCurrentImageTag(deployDir: string, serviceName: string): string | null {
257
- const result = spawnSync(
258
- "docker",
259
- ["compose", "-f", join(deployDir, "docker-compose.yml"), "images", serviceName, "--format", "json"],
260
- { cwd: deployDir, encoding: "utf8" },
261
- )
262
- if (result.status !== 0 || !result.stdout.trim()) return null
263
- try {
264
- // docker compose images --format json outputs one JSON object per line
265
- const lines = result.stdout.trim().split("\n")
266
- for (const line of lines) {
267
- const data = JSON.parse(line) as { Tag?: string }
268
- if (data.Tag) return data.Tag
269
- }
270
- return null
271
- } catch {
272
- return null
273
- }
274
- }
166
+ console.log("\nServices installed and started.")
167
+ console.log(" supatype-postgres.service")
168
+ console.log(" supatype-server.service")
169
+ },
170
+ )
275
171
 
276
- async function fetchLatestRelease(repo: string): Promise<{ tag: string; body: string } | null> {
277
- try {
278
- const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
279
- headers: { Accept: "application/vnd.github+json" },
280
- signal: AbortSignal.timeout(5000),
172
+ // ── serve ──────────────────────────────────────────────────────────────────
173
+
174
+ legacyCmd
175
+ .command("serve", "Start supatype-server in the foreground (for standalone mode)")
176
+ .option("--port <port>", "Override port from config")
177
+ .action(async (opts: { port?: string }) => {
178
+ logLegacyWarning("serve")
179
+ const cwd = process.cwd()
180
+ const config = loadConfig(cwd)
181
+
182
+ const serverBin = await resolveBinary("server", config)
183
+ const port = opts.port ?? String(config.server.port ?? 54321)
184
+
185
+ const args = [
186
+ "--port", port,
187
+ "--mode", config.server.mode,
188
+ ...(config.server.domain ? ["--domain", config.server.domain] : []),
189
+ ]
190
+
191
+ const stateDir = join(homedir(), ".supatype", "projects", config.project.name)
192
+ const storageEnv = config.storage?.provider !== "s3" ? localStorageEnv(stateDir) : {}
193
+
194
+ console.log(`Starting supatype-server on port ${port}...`)
195
+ const result = spawnSync(serverBin, args, {
196
+ stdio: "inherit",
197
+ cwd,
198
+ env: { ...process.env, ...storageEnv },
199
+ })
200
+ process.exitCode = result.status ?? 1
281
201
  })
282
- if (!res.ok) return null
283
- const data = await res.json() as { tag_name?: string; body?: string }
284
- if (!data.tag_name) return null
285
- return { tag: data.tag_name, body: data.body ?? "" }
286
- } catch {
287
- return null
288
- }
289
- }
290
202
 
291
- function loadSelfHostConfig(cwd: string): SelfHostConfig | undefined {
292
- try {
293
- const { loadConfig } = require("../config.js") as typeof import("../config.js")
294
- const config = loadConfig(cwd)
295
- return config.selfHost
296
- } catch {
297
- return undefined
298
- }
299
- }
300
-
301
- function isServicePinned(
302
- selfHostConfig: SelfHostConfig | undefined,
303
- serviceName: string,
304
- ): ServiceVersionPin | undefined {
305
- if (!selfHostConfig?.services) return undefined
306
- return (selfHostConfig.services as Record<string, ServiceVersionPin | undefined>)[serviceName]
307
- }
203
+ // ── reload ─────────────────────────────────────────────────────────────────
308
204
 
309
- function checkServiceHealth(deployDir: string, serviceName: string, timeoutSeconds = 60): boolean {
310
- console.log(` Checking health of ${serviceName}...`)
311
- const deadline = Date.now() + timeoutSeconds * 1000
312
- while (Date.now() < deadline) {
313
- const result = spawnSync(
314
- "docker",
315
- ["compose", "-f", join(deployDir, "docker-compose.yml"), "ps", serviceName, "--format", "json"],
316
- { cwd: deployDir, encoding: "utf8" },
317
- )
318
- if (result.status === 0 && result.stdout.trim()) {
319
- const lines = result.stdout.trim().split("\n")
320
- for (const line of lines) {
321
- try {
322
- const data = JSON.parse(line) as { State?: string; Health?: string; Status?: string }
323
- // A service is considered healthy if:
324
- // - it has a healthcheck and Health is "healthy", or
325
- // - it has no healthcheck and State is "running"
326
- if (data.Health === "healthy") return true
327
- if (!data.Health && data.State === "running") return true
328
- // Status field sometimes contains "Up ... (healthy)"
329
- if (data.Status && data.Status.includes("healthy")) return true
330
- if (data.Status && !data.Status.includes("health") && data.State === "running") return true
331
- } catch { /* skip bad line */ }
205
+ legacyCmd
206
+ .command("reload", "Reload the running supatype-server (SIGHUP for config reload)")
207
+ .action(() => {
208
+ logLegacyWarning("reload")
209
+ const cwd = process.cwd()
210
+ const config = loadConfig(cwd)
211
+ const stateDir = join(homedir(), ".supatype", "projects", config.project.name)
212
+ const pid = readPid(join(stateDir, "pid"), "server")
213
+
214
+ if (!pid) {
215
+ // Try systemctl if running as a service
216
+ if (process.platform === "linux") {
217
+ const result = spawnSync("systemctl", ["reload", "supatype-server"], { stdio: "inherit" })
218
+ process.exitCode = result.status ?? 1
219
+ return
220
+ }
221
+ console.error("Server does not appear to be running (no PID file found).")
222
+ process.exitCode = 1
223
+ return
332
224
  }
333
- }
334
- spawnSync("sleep", ["3"])
335
- }
336
- return false
337
- }
338
225
 
339
- function rollbackService(deployDir: string, serviceName: string, previousImage: string): boolean {
340
- console.log(` Rolling back ${serviceName} to ${previousImage}...`)
341
- // Pull the previous image back
342
- const pullResult = spawnSync(
343
- "docker",
344
- ["pull", previousImage],
345
- { stdio: "inherit", cwd: deployDir },
346
- )
347
- if (pullResult.status !== 0) return false
348
-
349
- // Restart the service (docker compose will use the image now available)
350
- // We need to re-tag or use docker compose up with the old image.
351
- // The simplest reliable approach: stop the service, then start it.
352
- // Since compose file may have :latest or a tag, we re-pull and restart.
353
- const upResult = spawnSync(
354
- "docker",
355
- ["compose", "-f", join(deployDir, "docker-compose.yml"), "up", "-d", "--no-deps", serviceName],
356
- { stdio: "inherit", cwd: deployDir },
357
- )
358
- return upResult.status === 0
359
- }
226
+ try {
227
+ process.kill(pid, "SIGHUP")
228
+ console.log(`Sent SIGHUP to supatype-server (pid ${pid}).`)
229
+ } catch (err) {
230
+ console.error(`Failed to signal pid ${pid}:`, (err as Error).message)
231
+ process.exitCode = 1
232
+ }
233
+ })
360
234
 
361
- function applyDatabaseMigrations(deployDir: string): boolean {
362
- console.log("\nApplying database migrations...")
363
- // Check if migrations directory exists
364
- const migrationsDir = resolve(deployDir, "..", "migrations")
365
- if (!existsSync(migrationsDir)) {
366
- console.log(" No migrations directory found, skipping.")
367
- return true
368
- }
369
-
370
- // Run migrations via docker exec into the db container
371
- const result = spawnSync(
372
- "docker",
373
- [
374
- "compose",
375
- "-f", join(deployDir, "docker-compose.yml"),
376
- "exec", "-T", "db",
377
- "sh", "-c",
378
- `for f in /migrations/*.sql; do [ -f "$f" ] && psql -U postgres -d supatype -f "$f" && echo "Applied: $f"; done`,
379
- ],
380
- {
381
- cwd: deployDir,
382
- stdio: "inherit",
383
- // Mount migrations directory
384
- env: { ...process.env },
385
- },
386
- )
235
+ // ── status ─────────────────────────────────────────────────────────────────
387
236
 
388
- // Also try with a copy approach if the volume isn't mounted
389
- if (result.status !== 0) {
390
- // Copy and run migrations one at a time
391
- const { readdirSync } = require("node:fs") as typeof import("node:fs")
392
- try {
393
- const files = readdirSync(migrationsDir).filter(f => f.endsWith(".sql")).sort()
394
- for (const file of files) {
395
- const sqlPath = resolve(migrationsDir, file)
396
- const sql = readFileSync(sqlPath, "utf8")
397
- const execResult = spawnSync(
398
- "docker",
399
- [
400
- "compose",
401
- "-f", join(deployDir, "docker-compose.yml"),
402
- "exec", "-T", "db",
403
- "psql", "-U", "postgres", "-d", "supatype", "-c", sql,
404
- ],
405
- { cwd: deployDir, encoding: "utf8" },
406
- )
407
- if (execResult.status !== 0) {
408
- console.error(` Failed to apply migration ${file}: ${execResult.stderr}`)
409
- return false
237
+ legacyCmd
238
+ .command("status", "Show running status of supatype services")
239
+ .action(() => {
240
+ logLegacyWarning("status")
241
+ const cwd = process.cwd()
242
+ const config = loadConfig(cwd)
243
+ const stateDir = join(homedir(), ".supatype", "projects", config.project.name)
244
+
245
+ console.log(`Project: ${config.project.name}\n`)
246
+
247
+ if (process.platform === "linux" && existsSync("/run/systemd/system")) {
248
+ // systemd is active
249
+ for (const svc of ["supatype-postgres", "supatype-server"]) {
250
+ const result = spawnSync("systemctl", ["status", "--no-pager", "--lines=0", svc], {
251
+ encoding: "utf8",
252
+ })
253
+ const active = result.stdout?.includes("active (running)") ? "running" : "stopped"
254
+ console.log(` ${svc}: ${active}`)
410
255
  }
411
- console.log(` Applied: ${file}`)
256
+ } else {
257
+ // PID file check
258
+ const serverPid = readPid(join(stateDir, "pid"), "server")
259
+ const pgPid = readPid(join(stateDir, "pid"), "postgres")
260
+ console.log(` postgres: ${pgPid ? `running (pid ${pgPid})` : "stopped"}`)
261
+ console.log(` supatype-server: ${serverPid ? `running (pid ${serverPid})` : "stopped"}`)
412
262
  }
413
- } catch (err) {
414
- console.error(` Error reading migrations: ${err}`)
415
- return false
416
- }
417
- }
418
-
419
- console.log(" Migrations complete.")
420
- return true
421
- }
422
-
423
- /** Summarize a release body to a short changelog line. */
424
- function summarizeChangelog(body: string): string {
425
- if (!body.trim()) return "(no changelog available)"
426
- // Take first 3 non-empty lines, strip markdown headers
427
- const lines = body
428
- .split("\n")
429
- .map(l => l.trim())
430
- .filter(l => l.length > 0)
431
- .map(l => l.replace(/^#+\s*/, ""))
432
- .slice(0, 3)
433
- const summary = lines.join("; ")
434
- return summary.length > 120 ? summary.slice(0, 117) + "..." : summary
435
- }
436
-
437
- interface UpgradeOpts {
438
- skipBackup?: boolean
439
- skipMigrations?: boolean
440
- }
441
263
 
442
- async function upgrade(cwd: string, opts: UpgradeOpts): Promise<void> {
443
- const deployDir = resolve(cwd, "deploy")
444
- if (!existsSync(join(deployDir, "docker-compose.yml"))) {
445
- console.error("deploy/docker-compose.yml not found. Run: supatype self-host setup")
446
- process.exit(1)
447
- }
448
-
449
- const selfHostConfig = loadSelfHostConfig(cwd)
450
-
451
- // ── Step 1: Check current vs latest versions ──────────────────────────────
452
-
453
- console.log("Checking service versions...\n")
454
-
455
- interface UpgradePlan {
456
- service: ServiceInfo
457
- currentTag: string | null
458
- latestTag: string
459
- changelog: string
460
- pinned: boolean
461
- }
462
-
463
- const plans: UpgradePlan[] = []
464
-
465
- for (const svc of MANAGED_SERVICES) {
466
- const pin = isServicePinned(selfHostConfig, svc.composeName)
467
- const currentTag = getCurrentImageTag(deployDir, svc.composeName)
468
-
469
- if (pin) {
470
- console.log(` ${svc.composeName.padEnd(12)} pinned at ${pin.version} (skipping)`)
471
- plans.push({
472
- service: svc,
473
- currentTag,
474
- latestTag: pin.version,
475
- changelog: "",
476
- pinned: true,
477
- })
478
- continue
479
- }
480
-
481
- let latestTag = svc.fallbackTag
482
- let changelog = ""
483
-
484
- if (svc.repo) {
485
- const release = await fetchLatestRelease(svc.repo)
486
- if (release) {
487
- latestTag = release.tag
488
- changelog = summarizeChangelog(release.body)
264
+ const logDir = join(stateDir, "logs")
265
+ if (existsSync(logDir)) {
266
+ console.log(`\nLogs: ${logDir}`)
489
267
  }
490
- }
491
-
492
- const needsUpgrade = currentTag !== latestTag
493
- const marker = needsUpgrade ? " *" : ""
494
- console.log(
495
- ` ${svc.composeName.padEnd(12)} ${(currentTag ?? "unknown").padEnd(16)} -> ${latestTag}${marker}`,
496
- )
497
- if (changelog && needsUpgrade) {
498
- console.log(`${"".padEnd(16)}changelog: ${changelog}`)
499
- }
500
-
501
- plans.push({
502
- service: svc,
503
- currentTag,
504
- latestTag,
505
- changelog,
506
- pinned: false,
507
268
  })
508
- }
509
-
510
- const upgradeable = plans.filter(p => !p.pinned && p.currentTag !== p.latestTag)
511
- if (upgradeable.length === 0) {
512
- console.log("\nAll services are up to date. Nothing to upgrade.")
513
- return
514
- }
515
-
516
- console.log(`\n${upgradeable.length} service(s) will be upgraded.\n`)
517
269
 
518
- // ── Step 2: Pre-upgrade backup ────────────────────────────────────────────
270
+ // ── logs ───────────────────────────────────────────────────────────────────
519
271
 
520
- if (!opts.skipBackup) {
521
- const backupPath = `./backups/pre-upgrade-${timestamp()}.sql.gz`
522
- console.log(`Creating pre-upgrade backup: ${backupPath}`)
523
- backup(cwd, backupPath)
524
- console.log("Backup complete.\n")
525
- } else {
526
- console.log("Skipping pre-upgrade backup (--skip-backup).\n")
527
- }
528
-
529
- // ── Step 3: Apply database migrations ─────────────────────────────────────
530
-
531
- if (!opts.skipMigrations) {
532
- const migrationOk = applyDatabaseMigrations(deployDir)
533
- if (!migrationOk) {
534
- console.error("\nDatabase migration failed. Aborting upgrade.")
535
- console.error("Your pre-upgrade backup is available. To restore:")
536
- console.error(" docker compose exec -T db sh -c 'gunzip | psql -U postgres' < <backup-file>")
537
- process.exit(1)
538
- }
539
- }
540
-
541
- // ── Step 4: Rolling restart with health checks and rollback ───────────────
542
-
543
- console.log("\nStarting rolling upgrade...\n")
544
-
545
- const failed: string[] = []
546
-
547
- for (const plan of upgradeable) {
548
- const svc = plan.service
549
- const fullImage = `${svc.image}:${plan.latestTag}`
550
- const previousImage = plan.currentTag ? `${svc.image}:${plan.currentTag}` : null
551
-
552
- console.log(`Upgrading ${svc.composeName}: ${plan.currentTag ?? "unknown"} -> ${plan.latestTag}`)
272
+ selfHostCmd
273
+ .command("logs", "Tail supatype service logs", { hidden: true })
274
+ .option("--service <name>", "Show logs for: postgres | server")
275
+ .option("--lines <n>", "Number of lines to show", "50")
276
+ .option("-f, --follow", "Follow log output")
277
+ .action((opts: { service?: string; lines: string; follow?: boolean }) => {
278
+ logLegacyWarning("logs")
279
+ const cwd = process.cwd()
280
+ const config = loadConfig(cwd)
281
+ const stateDir = join(homedir(), ".supatype", "projects", config.project.name)
282
+ const logDir = join(stateDir, "logs")
283
+
284
+ if (process.platform === "linux" && existsSync("/run/systemd/system")) {
285
+ const args = ["--no-pager", "--lines", opts.lines]
286
+ if (opts.follow) args.push("--follow")
287
+ if (opts.service) args.push(`-u`, `supatype-${opts.service}`)
288
+ else args.push("-u", "supatype-postgres", "-u", "supatype-server")
289
+ spawnSync("journalctl", args, { stdio: "inherit" })
290
+ return
291
+ }
553
292
 
554
- // Pull new image
555
- console.log(` Pulling ${fullImage}...`)
556
- const pullResult = spawnSync("docker", ["pull", fullImage], {
557
- stdio: "inherit",
558
- cwd: deployDir,
559
- })
560
- if (pullResult.status !== 0) {
561
- console.error(` Failed to pull ${fullImage}. Skipping ${svc.composeName}.`)
562
- failed.push(svc.composeName)
563
- continue
564
- }
565
-
566
- // Restart just this service (zero-downtime: one at a time)
567
- console.log(` Restarting ${svc.composeName}...`)
568
- const upResult = spawnSync(
569
- "docker",
570
- ["compose", "-f", join(deployDir, "docker-compose.yml"), "up", "-d", "--no-deps", svc.composeName],
571
- { stdio: "inherit", cwd: deployDir },
572
- )
573
- if (upResult.status !== 0) {
574
- console.error(` Failed to restart ${svc.composeName}.`)
575
- if (previousImage) {
576
- rollbackService(deployDir, svc.composeName, previousImage)
293
+ // File-based logs
294
+ const targets: Array<{ label: string; file: string }> = []
295
+ if (!opts.service || opts.service === "postgres") {
296
+ targets.push({ label: "postgres", file: join(logDir, "postgres.log") })
577
297
  }
578
- failed.push(svc.composeName)
579
- continue
580
- }
581
-
582
- // Verify health
583
- const healthy = checkServiceHealth(deployDir, svc.composeName)
584
- if (!healthy) {
585
- console.error(` Health check failed for ${svc.composeName} after upgrade.`)
586
- if (previousImage) {
587
- console.log(` Initiating rollback for ${svc.composeName}...`)
588
- const rolledBack = rollbackService(deployDir, svc.composeName, previousImage)
589
- if (rolledBack) {
590
- const healthAfterRollback = checkServiceHealth(deployDir, svc.composeName)
591
- if (healthAfterRollback) {
592
- console.log(` Rolled back ${svc.composeName} to ${previousImage} successfully.`)
593
- } else {
594
- console.error(` WARNING: ${svc.composeName} is unhealthy even after rollback.`)
595
- }
298
+ if (!opts.service || opts.service === "server") {
299
+ targets.push({ label: "server", file: join(logDir, "server.log") })
300
+ }
301
+
302
+ for (const { label, file } of targets) {
303
+ if (!existsSync(file)) {
304
+ console.log(`[${label}] log file not found: ${file}`)
305
+ continue
306
+ }
307
+ if (opts.follow) {
308
+ const tail = spawnSync("tail", ["-f", "-n", opts.lines, file], { stdio: "inherit" })
309
+ process.exitCode = tail.status ?? 0
596
310
  } else {
597
- console.error(` WARNING: Rollback failed for ${svc.composeName}.`)
311
+ const n = parseInt(opts.lines, 10)
312
+ const content = readFileSync(file, "utf8")
313
+ const lines = content.split("\n")
314
+ console.log(lines.slice(-n).join("\n"))
598
315
  }
599
316
  }
600
- failed.push(svc.composeName)
601
- continue
602
- }
603
-
604
- console.log(` ${svc.composeName} upgraded and healthy.\n`)
605
- }
606
-
607
- // ── Step 5: Summary ───────────────────────────────────────────────────────
608
-
609
- if (failed.length === 0) {
610
- console.log("Upgrade complete. All services are healthy.")
611
- } else {
612
- console.error(`\nUpgrade finished with failures in: ${failed.join(", ")}`)
613
- console.error("\nManual intervention may be needed:")
614
- console.error(" 1. Check logs: supatype self-host logs --service <name>")
615
- console.error(" 2. Check status: supatype self-host status")
616
- console.error(" 3. Restore backup: docker compose exec -T db sh -c 'gunzip | psql -U postgres' < <backup-file>")
617
- console.error(" 4. Pin a version: Add services.<name>.version in supatype.config.ts selfHost config")
618
- process.exit(1)
619
- }
620
- }
621
-
622
- // ─── Config helpers ───────────────────────────────────────────────────────────
623
-
624
- function loadDomainFromConfig(cwd: string): string | undefined {
625
- try {
626
- const { loadConfig } = require("../config.js") as typeof import("../config.js")
627
- const config = loadConfig(cwd)
628
- return (config as { selfHost?: { domain?: string } }).selfHost?.domain
629
- } catch {
630
- return undefined
631
- }
632
- }
633
-
634
- function timestamp(): string {
635
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
636
- }
637
-
638
- // ─── Production templates ─────────────────────────────────────────────────────
639
-
640
- function envProductionTemplate(
641
- domain: string,
642
- pgPassword: string,
643
- jwtSecret: string,
644
- anonKey: string,
645
- serviceKey: string,
646
- ): string {
647
- return `# Production secrets — DO NOT commit this file to source control
648
- # Generated by: supatype self-host setup
649
-
650
- DOMAIN=${domain}
651
-
652
- POSTGRES_PASSWORD=${pgPassword}
653
- POSTGRES_DB=supatype
654
-
655
- JWT_SECRET=${jwtSecret}
656
- ANON_KEY=${anonKey}
657
- SERVICE_ROLE_KEY=${serviceKey}
658
-
659
- SITE_URL=https://${domain}
660
-
661
- # SMTP — required for user email confirmation in production
662
- SMTP_HOST=
663
- SMTP_PORT=587
664
- SMTP_USER=
665
- SMTP_PASS=
666
- SMTP_SENDER_NAME=Supatype
667
- `
668
- }
669
-
670
- function productionComposeTemplate(domain: string, opts: SetupOpts, postgresTag: string, authTag: string): string {
671
- const appService = opts.appDockerfile
672
- ? `
673
- app:
674
- build:
675
- context: ..
676
- dockerfile: ${opts.appDockerfile}
677
- environment:
678
- SUPATYPE_URL: http://kong:8000
679
- SUPATYPE_ANON_KEY: \${ANON_KEY}
680
- SUPATYPE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY}
681
- networks:
682
- - supatype
683
- depends_on:
684
- - kong
685
- restart: unless-stopped
686
- `
687
- : ""
688
-
689
- return `# Production docker-compose — generated by supatype self-host setup
690
- # Run with: docker compose up -d (from within the deploy/ directory)
691
-
692
- services:
693
- db:
694
- image: supatype/postgres:${postgresTag}
695
- environment:
696
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
697
- POSTGRES_DB: \${POSTGRES_DB:-supatype}
698
- volumes:
699
- - db-data:/var/lib/postgresql/data
700
- networks:
701
- - supatype
702
- healthcheck:
703
- test: ["CMD-SHELL", "pg_isready -U postgres"]
704
- interval: 10s
705
- timeout: 5s
706
- retries: 20
707
- restart: unless-stopped
708
-
709
- pgbouncer:
710
- image: pgbouncer/pgbouncer:latest
711
- volumes:
712
- - ./pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
713
- - ./userlist.txt:/etc/pgbouncer/userlist.txt:ro
714
- networks:
715
- - supatype
716
- depends_on:
717
- db:
718
- condition: service_healthy
719
- restart: unless-stopped
720
-
721
- gotrue:
722
- image: supatype/auth:${authTag}
723
- environment:
724
- GOTRUE_API_HOST: 0.0.0.0
725
- GOTRUE_API_PORT: 9999
726
- GOTRUE_DB_DRIVER: postgres
727
- GOTRUE_DB_DATABASE_URL: "postgres://postgres:\${POSTGRES_PASSWORD}@pgbouncer:6432/\${POSTGRES_DB:-supatype}?search_path=auth"
728
- GOTRUE_SITE_URL: https://${domain}
729
- GOTRUE_JWT_SECRET: \${JWT_SECRET}
730
- GOTRUE_JWT_EXP: 3600
731
- GOTRUE_JWT_AUD: authenticated
732
- GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
733
- GOTRUE_JWT_ADMIN_ROLES: service_role
734
- GOTRUE_MAILER_AUTOCONFIRM: false
735
- GOTRUE_SMTP_HOST: \${SMTP_HOST}
736
- GOTRUE_SMTP_PORT: \${SMTP_PORT:-587}
737
- GOTRUE_SMTP_USER: \${SMTP_USER}
738
- GOTRUE_SMTP_PASS: \${SMTP_PASS}
739
- GOTRUE_SMTP_SENDER_NAME: \${SMTP_SENDER_NAME:-Supatype}
740
- GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
741
- GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
742
- GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
743
- GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
744
- GOTRUE_DISABLE_SIGNUP: false
745
- networks:
746
- - supatype
747
- depends_on:
748
- pgbouncer:
749
- condition: service_started
750
- restart: unless-stopped
751
-
752
- postgrest:
753
- image: postgrest/postgrest:v12.2.8
754
- environment:
755
- PGRST_DB_URI: postgresql://authenticator:\${POSTGRES_PASSWORD}@pgbouncer:6432/\${POSTGRES_DB:-supatype}
756
- PGRST_DB_SCHEMA: public
757
- PGRST_DB_ANON_ROLE: anon
758
- PGRST_JWT_SECRET: \${JWT_SECRET}
759
- PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
760
- PGRST_DB_POOL: 3
761
- networks:
762
- - supatype
763
- depends_on:
764
- pgbouncer:
765
- condition: service_started
766
- restart: unless-stopped
767
-
768
- kong:
769
- image: kong:3.6
770
- environment:
771
- KONG_DATABASE: "off"
772
- KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml
773
- KONG_PROXY_ACCESS_LOG: /dev/stdout
774
- KONG_ADMIN_ACCESS_LOG: /dev/stdout
775
- KONG_PROXY_ERROR_LOG: /dev/stderr
776
- KONG_ADMIN_ERROR_LOG: /dev/stderr
777
- volumes:
778
- - ./kong.yml:/etc/kong/kong.yml:ro
779
- networks:
780
- - supatype
781
- depends_on:
782
- - postgrest
783
- - gotrue
784
- restart: unless-stopped
785
- ${appService}
786
- functions:
787
- image: denoland/deno:latest
788
- environment:
789
- SUPATYPE_URL: http://kong:8000
790
- SUPATYPE_ANON_KEY: \${ANON_KEY}
791
- SUPATYPE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY}
792
- FUNCTIONS_DIR: /functions
793
- volumes:
794
- - ../supatype/functions:/functions:ro
795
- networks:
796
- - supatype
797
- depends_on:
798
- - kong
799
- mem_limit: 512m
800
- cpus: 1.0
801
- restart: unless-stopped
802
-
803
- caddy:
804
- image: caddy:2
805
- ports:
806
- - "80:80"
807
- - "443:443"
808
- volumes:
809
- - ./Caddyfile:/etc/caddy/Caddyfile:ro
810
- - caddy-data:/data
811
- - caddy-config:/config
812
- networks:
813
- - supatype
814
- depends_on:
815
- - kong
816
- restart: unless-stopped
817
-
818
- networks:
819
- supatype:
820
- driver: bridge
821
-
822
- volumes:
823
- db-data:
824
- caddy-data:
825
- caddy-config:
826
- `
827
- }
828
-
829
- function caddyfileTemplate(domain: string, sslEmail?: string): string {
830
- const emailLine = sslEmail ? `\n\ttls ${sslEmail}\n` : ""
831
- return `${domain} {${emailLine}
832
- \treverse_proxy kong:8000
833
-
834
- \theader {
835
- \t\tStrict-Transport-Security "max-age=31536000; includeSubDomains"
836
- \t\tX-Frame-Options "SAMEORIGIN"
837
- \t\tX-Content-Type-Options "nosniff"
838
- \t}
839
- }
840
- `
841
- }
317
+ })
842
318
 
843
- function productionPgbouncerIni(): string {
844
- return `[databases]
845
- * = host=db port=5432
846
-
847
- [pgbouncer]
848
- listen_addr = 0.0.0.0
849
- listen_port = 6432
850
- auth_type = md5
851
- auth_file = /etc/pgbouncer/userlist.txt
852
- pool_mode = transaction
853
- default_pool_size = 20
854
- max_db_connections = 60
855
- max_client_conn = 100
856
- server_reset_query = DEALLOCATE ALL
857
- ignore_startup_parameters = extra_float_digits
858
- `
859
- }
319
+ // ── backup ─────────────────────────────────────────────────────────────────
320
+
321
+ legacyCmd
322
+ .command("backup", "Create a Postgres dump of the project database")
323
+ .option("--output <path>", "Output file path (default: ./backups/backup-<timestamp>.sql.gz)")
324
+ .option("--connection <url>", "Database connection URL (overrides config)")
325
+ .action((opts: { output?: string; connection?: string }) => {
326
+ logLegacyWarning("backup")
327
+ const cwd = process.cwd()
328
+ const config = loadConfig(cwd)
329
+ const conn = opts.connection ?? connectionString(config)
330
+ const outFile = opts.output ?? resolve(
331
+ cwd,
332
+ "backups",
333
+ `backup-${new Date().toISOString().replace(/[:.]/g, "-")}.sql.gz`,
334
+ )
335
+
336
+ mkdirSync(resolve(outFile, ".."), { recursive: true })
337
+
338
+ console.log(`Backing up database to ${outFile}...`)
339
+ try {
340
+ // Avoid shell interpolation of user-supplied values.
341
+ const pgDump = spawnSync("pg_dump", [conn], {
342
+ stdio: ["ignore", "pipe", "pipe"],
343
+ })
344
+ if (pgDump.status !== 0) {
345
+ const stderr = pgDump.stderr?.toString("utf8") ?? ""
346
+ throw new Error(stderr.trim() || "pg_dump failed")
347
+ }
860
348
 
861
- function productionUserlist(pgPassword: string): string {
862
- // PgBouncer md5 format: "md5" + md5(password + username)
863
- const md5Hash = (s: string) => {
864
- const { createHash } = require("node:crypto") as typeof import("node:crypto")
865
- return createHash("md5").update(s).digest("hex")
866
- }
867
- const postgresHash = "md5" + md5Hash(pgPassword + "postgres")
868
- const authenticatorHash = "md5" + md5Hash(pgPassword + "authenticator")
869
-
870
- return `# PgBouncer userlist — generated by supatype self-host setup
871
- # Regenerate by running: supatype self-host setup
872
- "postgres" "${postgresHash}"
873
- "authenticator" "${authenticatorHash}"
874
- `
349
+ const compressed = gzipSync(pgDump.stdout)
350
+ writeFileSync(outFile, compressed)
351
+ console.log("Backup complete.")
352
+ } catch (err) {
353
+ console.error("Backup failed:", (err as Error).message)
354
+ process.exit(1)
355
+ }
356
+ })
875
357
  }
876
358
 
877
- function deployScript(domain: string): string {
878
- return `#!/usr/bin/env bash
879
- # deploy.sh generated by supatype self-host setup
880
- # Run once on a fresh VPS: bash deploy.sh
881
- set -euo pipefail
882
-
883
- DOMAIN="${domain}"
884
-
885
- echo "Checking prerequisites..."
886
-
887
- # Check Docker
888
- if ! command -v docker &>/dev/null; then
889
- echo "Docker not found. Installing..."
890
- curl -fsSL https://get.docker.com | sh
891
- usermod -aG docker "$USER"
892
- newgrp docker
893
- fi
894
-
895
- # Check ports 80 and 443 are available
896
- for port in 80 443; do
897
- if ss -tlnp 2>/dev/null | grep -q ":$port " ; then
898
- echo "Error: Port $port is already in use. Free it before running deploy.sh."
899
- exit 1
900
- fi
901
- done
902
-
903
- echo "Loading environment..."
904
- if [ ! -f .env.production ]; then
905
- echo "Error: .env.production not found in $(pwd)"
906
- exit 1
907
- fi
908
-
909
- # Export env vars from .env.production
910
- set -a; source .env.production; set +a
911
-
912
- echo "Starting services..."
913
- docker compose up -d --wait
914
-
915
- echo "Waiting for health checks..."
916
- timeout=120
917
- elapsed=0
918
- while ! docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; do
919
- sleep 5
920
- elapsed=$((elapsed + 5))
921
- if [ $elapsed -ge $timeout ]; then
922
- echo "Timeout waiting for services to become healthy."
923
- docker compose ps
924
- exit 1
925
- fi
926
- done
927
-
928
- echo ""
929
- echo "Deployment complete!"
930
- echo "Your app is live at: https://$DOMAIN"
931
- `
359
+ function logLegacyWarning(cmd: string): void {
360
+ console.warn(
361
+ `[supatype] self-host native ${cmd} is deprecated. ` +
362
+ "Use `supatype self-host compose` commands instead.",
363
+ )
932
364
  }