@supatype/cli 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +7 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/bin/dev-entry.ts +2 -0
  5. package/bin/supatype.js +5 -0
  6. package/dist/app/framework.d.ts +44 -0
  7. package/dist/app/framework.d.ts.map +1 -0
  8. package/dist/app/framework.js +200 -0
  9. package/dist/app/framework.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +55 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/admin.d.ts +4 -0
  15. package/dist/commands/admin.d.ts.map +1 -0
  16. package/dist/commands/admin.js +270 -0
  17. package/dist/commands/admin.js.map +1 -0
  18. package/dist/commands/app.d.ts +3 -0
  19. package/dist/commands/app.d.ts.map +1 -0
  20. package/dist/commands/app.js +235 -0
  21. package/dist/commands/app.js.map +1 -0
  22. package/dist/commands/cloud.d.ts +3 -0
  23. package/dist/commands/cloud.d.ts.map +1 -0
  24. package/dist/commands/cloud.js +256 -0
  25. package/dist/commands/cloud.js.map +1 -0
  26. package/dist/commands/db.d.ts +8 -0
  27. package/dist/commands/db.d.ts.map +1 -0
  28. package/dist/commands/db.js +123 -0
  29. package/dist/commands/db.js.map +1 -0
  30. package/dist/commands/deploy-types.d.ts +14 -0
  31. package/dist/commands/deploy-types.d.ts.map +1 -0
  32. package/dist/commands/deploy-types.js +38 -0
  33. package/dist/commands/deploy-types.js.map +1 -0
  34. package/dist/commands/deploy.d.ts +14 -0
  35. package/dist/commands/deploy.d.ts.map +1 -0
  36. package/dist/commands/deploy.js +295 -0
  37. package/dist/commands/deploy.js.map +1 -0
  38. package/dist/commands/dev.d.ts +3 -0
  39. package/dist/commands/dev.d.ts.map +1 -0
  40. package/dist/commands/dev.js +428 -0
  41. package/dist/commands/dev.js.map +1 -0
  42. package/dist/commands/diff.d.ts +3 -0
  43. package/dist/commands/diff.d.ts.map +1 -0
  44. package/dist/commands/diff.js +39 -0
  45. package/dist/commands/diff.js.map +1 -0
  46. package/dist/commands/engine.d.ts +9 -0
  47. package/dist/commands/engine.d.ts.map +1 -0
  48. package/dist/commands/engine.js +99 -0
  49. package/dist/commands/engine.js.map +1 -0
  50. package/dist/commands/functions.d.ts +3 -0
  51. package/dist/commands/functions.d.ts.map +1 -0
  52. package/dist/commands/functions.js +762 -0
  53. package/dist/commands/functions.js.map +1 -0
  54. package/dist/commands/generate.d.ts +3 -0
  55. package/dist/commands/generate.d.ts.map +1 -0
  56. package/dist/commands/generate.js +28 -0
  57. package/dist/commands/generate.js.map +1 -0
  58. package/dist/commands/init.d.ts +7 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +515 -0
  61. package/dist/commands/init.js.map +1 -0
  62. package/dist/commands/keys.d.ts +4 -0
  63. package/dist/commands/keys.d.ts.map +1 -0
  64. package/dist/commands/keys.js +57 -0
  65. package/dist/commands/keys.js.map +1 -0
  66. package/dist/commands/logs.d.ts +6 -0
  67. package/dist/commands/logs.d.ts.map +1 -0
  68. package/dist/commands/logs.js +52 -0
  69. package/dist/commands/logs.js.map +1 -0
  70. package/dist/commands/migrate.d.ts +3 -0
  71. package/dist/commands/migrate.d.ts.map +1 -0
  72. package/dist/commands/migrate.js +71 -0
  73. package/dist/commands/migrate.js.map +1 -0
  74. package/dist/commands/plugins.d.ts +3 -0
  75. package/dist/commands/plugins.d.ts.map +1 -0
  76. package/dist/commands/plugins.js +431 -0
  77. package/dist/commands/plugins.js.map +1 -0
  78. package/dist/commands/pull.d.ts +3 -0
  79. package/dist/commands/pull.d.ts.map +1 -0
  80. package/dist/commands/pull.js +73 -0
  81. package/dist/commands/pull.js.map +1 -0
  82. package/dist/commands/push.d.ts +3 -0
  83. package/dist/commands/push.d.ts.map +1 -0
  84. package/dist/commands/push.js +87 -0
  85. package/dist/commands/push.js.map +1 -0
  86. package/dist/commands/seed.d.ts +3 -0
  87. package/dist/commands/seed.d.ts.map +1 -0
  88. package/dist/commands/seed.js +22 -0
  89. package/dist/commands/seed.js.map +1 -0
  90. package/dist/commands/self-host.d.ts +3 -0
  91. package/dist/commands/self-host.d.ts.map +1 -0
  92. package/dist/commands/self-host.js +796 -0
  93. package/dist/commands/self-host.js.map +1 -0
  94. package/dist/commands/status.d.ts +6 -0
  95. package/dist/commands/status.d.ts.map +1 -0
  96. package/dist/commands/status.js +69 -0
  97. package/dist/commands/status.js.map +1 -0
  98. package/dist/config.d.ts +106 -0
  99. package/dist/config.d.ts.map +1 -0
  100. package/dist/config.js +66 -0
  101. package/dist/config.js.map +1 -0
  102. package/dist/engine/cache.d.ts +37 -0
  103. package/dist/engine/cache.d.ts.map +1 -0
  104. package/dist/engine/cache.js +121 -0
  105. package/dist/engine/cache.js.map +1 -0
  106. package/dist/engine/download.d.ts +19 -0
  107. package/dist/engine/download.d.ts.map +1 -0
  108. package/dist/engine/download.js +108 -0
  109. package/dist/engine/download.js.map +1 -0
  110. package/dist/engine/platform.d.ts +24 -0
  111. package/dist/engine/platform.d.ts.map +1 -0
  112. package/dist/engine/platform.js +50 -0
  113. package/dist/engine/platform.js.map +1 -0
  114. package/dist/engine/resolve.d.ts +37 -0
  115. package/dist/engine/resolve.d.ts.map +1 -0
  116. package/dist/engine/resolve.js +133 -0
  117. package/dist/engine/resolve.js.map +1 -0
  118. package/dist/engine/update-notify.d.ts +11 -0
  119. package/dist/engine/update-notify.d.ts.map +1 -0
  120. package/dist/engine/update-notify.js +43 -0
  121. package/dist/engine/update-notify.js.map +1 -0
  122. package/dist/engine/verify.d.ts +50 -0
  123. package/dist/engine/verify.d.ts.map +1 -0
  124. package/dist/engine/verify.js +161 -0
  125. package/dist/engine/verify.js.map +1 -0
  126. package/dist/engine-version.d.ts +35 -0
  127. package/dist/engine-version.d.ts.map +1 -0
  128. package/dist/engine-version.js +35 -0
  129. package/dist/engine-version.js.map +1 -0
  130. package/dist/engine.d.ts +34 -0
  131. package/dist/engine.d.ts.map +1 -0
  132. package/dist/engine.js +76 -0
  133. package/dist/engine.js.map +1 -0
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +10 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/jwt.d.ts +3 -0
  139. package/dist/jwt.d.ts.map +1 -0
  140. package/dist/jwt.js +13 -0
  141. package/dist/jwt.js.map +1 -0
  142. package/dist/pull-utils.d.ts +16 -0
  143. package/dist/pull-utils.d.ts.map +1 -0
  144. package/dist/pull-utils.js +65 -0
  145. package/dist/pull-utils.js.map +1 -0
  146. package/dist/scripts/postinstall.d.ts +12 -0
  147. package/dist/scripts/postinstall.d.ts.map +1 -0
  148. package/dist/scripts/postinstall.js +31 -0
  149. package/dist/scripts/postinstall.js.map +1 -0
  150. package/dist/tsx-runner.d.ts +18 -0
  151. package/dist/tsx-runner.d.ts.map +1 -0
  152. package/dist/tsx-runner.js +62 -0
  153. package/dist/tsx-runner.js.map +1 -0
  154. package/package.json +36 -0
  155. package/src/app/framework.ts +249 -0
  156. package/src/cli.ts +58 -0
  157. package/src/commands/admin.ts +371 -0
  158. package/src/commands/app.ts +261 -0
  159. package/src/commands/cloud.ts +326 -0
  160. package/src/commands/db.ts +145 -0
  161. package/src/commands/deploy-types.ts +49 -0
  162. package/src/commands/deploy.ts +366 -0
  163. package/src/commands/dev.ts +477 -0
  164. package/src/commands/diff.ts +61 -0
  165. package/src/commands/engine.ts +133 -0
  166. package/src/commands/functions.ts +919 -0
  167. package/src/commands/generate.ts +31 -0
  168. package/src/commands/init.ts +532 -0
  169. package/src/commands/keys.ts +66 -0
  170. package/src/commands/logs.ts +58 -0
  171. package/src/commands/migrate.ts +83 -0
  172. package/src/commands/plugins.ts +508 -0
  173. package/src/commands/pull.ts +96 -0
  174. package/src/commands/push.ts +119 -0
  175. package/src/commands/seed.ts +26 -0
  176. package/src/commands/self-host.ts +932 -0
  177. package/src/commands/status.ts +83 -0
  178. package/src/config.ts +190 -0
  179. package/src/engine/cache.ts +135 -0
  180. package/src/engine/download.ts +143 -0
  181. package/src/engine/platform.ts +66 -0
  182. package/src/engine/resolve.ts +197 -0
  183. package/src/engine/update-notify.ts +50 -0
  184. package/src/engine/verify.ts +206 -0
  185. package/src/engine-version.ts +39 -0
  186. package/src/engine.ts +99 -0
  187. package/src/index.ts +19 -0
  188. package/src/jwt.ts +14 -0
  189. package/src/pull-utils.ts +57 -0
  190. package/src/scripts/postinstall.ts +40 -0
  191. package/src/tsx-runner.ts +79 -0
  192. package/tests/cli-help.test.ts +107 -0
  193. package/tests/config.test.ts +117 -0
  194. package/tests/engine-distribution.test.ts +418 -0
  195. package/tests/init.test.ts +184 -0
  196. package/tests/keys.test.ts +160 -0
  197. package/tests/pull-utils.test.ts +115 -0
  198. package/tests/tsx-runner.test.ts +66 -0
  199. package/tsconfig.json +10 -0
  200. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,796 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { signJwt } from "../jwt.js";
6
+ export function registerSelfHost(program) {
7
+ const selfHostCmd = program
8
+ .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);
19
+ });
20
+ selfHostCmd
21
+ .command("status")
22
+ .description("Show running service health for the production stack")
23
+ .action(() => {
24
+ runDockerCompose(["ps", "--format", "table"], "status");
25
+ });
26
+ selfHostCmd
27
+ .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")
31
+ .action((opts) => {
32
+ const args = ["logs"];
33
+ if (opts.follow)
34
+ args.push("--follow");
35
+ if (opts.service)
36
+ 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);
45
+ });
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")
57
+ .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),
66
+ });
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;
212
+ }
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),
224
+ });
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");
239
+ 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 */ }
275
+ }
276
+ }
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
+ 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
+ }
336
+ }
337
+ 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;
382
+ }
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);
390
+ }
391
+ }
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}`);
397
+ }
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);
430
+ }
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;
450
+ }
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;
461
+ }
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
+ }
481
+ }
482
+ failed.push(svc.composeName);
483
+ continue;
484
+ }
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");
505
+ 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
+ `;
739
+ }
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
+ `;
795
+ }
796
+ //# sourceMappingURL=self-host.js.map