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

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