@supatype/cli 0.1.4 → 0.1.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 (94) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +99 -97
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app-vite-env.d.ts +4 -0
  5. package/dist/app-vite-env.d.ts.map +1 -0
  6. package/dist/app-vite-env.js +22 -0
  7. package/dist/app-vite-env.js.map +1 -0
  8. package/dist/augmentation-generator.d.ts.map +1 -1
  9. package/dist/augmentation-generator.js +26 -4
  10. package/dist/augmentation-generator.js.map +1 -1
  11. package/dist/binary-cache.d.ts +7 -5
  12. package/dist/binary-cache.d.ts.map +1 -1
  13. package/dist/binary-cache.js +28 -10
  14. package/dist/binary-cache.js.map +1 -1
  15. package/dist/cli-standalone.d.ts +10 -0
  16. package/dist/cli-standalone.d.ts.map +1 -0
  17. package/dist/cli-standalone.js +56 -0
  18. package/dist/cli-standalone.js.map +1 -0
  19. package/dist/commands/admin.d.ts.map +1 -1
  20. package/dist/commands/admin.js +10 -3
  21. package/dist/commands/admin.js.map +1 -1
  22. package/dist/commands/dev.d.ts.map +1 -1
  23. package/dist/commands/dev.js +34 -0
  24. package/dist/commands/dev.js.map +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +3 -1
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/keys.d.ts.map +1 -1
  29. package/dist/commands/keys.js +16 -0
  30. package/dist/commands/keys.js.map +1 -1
  31. package/dist/commands/self-update.d.ts +1 -1
  32. package/dist/commands/self-update.d.ts.map +1 -1
  33. package/dist/commands/self-update.js +33 -9
  34. package/dist/commands/self-update.js.map +1 -1
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/update.js +9 -1
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/components.d.ts +2 -2
  39. package/dist/components.d.ts.map +1 -1
  40. package/dist/components.js +2 -2
  41. package/dist/components.js.map +1 -1
  42. package/dist/dev-compose.d.ts.map +1 -1
  43. package/dist/dev-compose.js +122 -12
  44. package/dist/dev-compose.js.map +1 -1
  45. package/dist/project-config.d.ts +2 -0
  46. package/dist/project-config.d.ts.map +1 -1
  47. package/dist/project-config.js.map +1 -1
  48. package/dist/realtime-launch.d.ts +11 -0
  49. package/dist/realtime-launch.d.ts.map +1 -0
  50. package/dist/realtime-launch.js +39 -0
  51. package/dist/realtime-launch.js.map +1 -0
  52. package/dist/required-host-components.js +1 -1
  53. package/dist/required-host-components.js.map +1 -1
  54. package/dist/route-manifest.d.ts +3 -0
  55. package/dist/route-manifest.d.ts.map +1 -0
  56. package/dist/route-manifest.js +15 -0
  57. package/dist/route-manifest.js.map +1 -0
  58. package/dist/schema-push-lock.d.ts +17 -0
  59. package/dist/schema-push-lock.d.ts.map +1 -0
  60. package/dist/schema-push-lock.js +106 -0
  61. package/dist/schema-push-lock.js.map +1 -0
  62. package/dist/self-host-compose.d.ts.map +1 -1
  63. package/dist/self-host-compose.js +18 -0
  64. package/dist/self-host-compose.js.map +1 -1
  65. package/dist/type-extractor.d.ts.map +1 -1
  66. package/dist/type-extractor.js +125 -13
  67. package/dist/type-extractor.js.map +1 -1
  68. package/package.json +1 -1
  69. package/scripts/build-release-bundle.mjs +27 -0
  70. package/src/app-vite-env.ts +27 -0
  71. package/src/augmentation-generator.ts +29 -5
  72. package/src/binary-cache.ts +29 -11
  73. package/src/cli-standalone.ts +62 -0
  74. package/src/commands/admin.ts +13 -3
  75. package/src/commands/dev.ts +38 -0
  76. package/src/commands/init.ts +3 -1
  77. package/src/commands/keys.ts +16 -0
  78. package/src/commands/self-update.ts +34 -13
  79. package/src/commands/update.ts +9 -1
  80. package/src/components.ts +2 -2
  81. package/src/dev-compose.ts +146 -14
  82. package/src/project-config.ts +2 -0
  83. package/src/realtime-launch.ts +52 -0
  84. package/src/required-host-components.ts +1 -1
  85. package/src/route-manifest.ts +17 -0
  86. package/src/schema-push-lock.ts +131 -0
  87. package/src/self-host-compose.ts +18 -0
  88. package/src/type-extractor.ts +156 -13
  89. package/tests/augmentation-generator.test.ts +44 -4
  90. package/tests/realtime-launch.test.ts +25 -0
  91. package/tests/required-host-components.test.ts +2 -0
  92. package/tests/route-manifest.test.ts +29 -0
  93. package/tests/type-extractor.test.ts +52 -0
  94. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Binary cache — manages supatype component binaries.
3
3
  *
4
- * Components: engine, server, postgres, deno.
4
+ * Components: engine, server, postgres, deno, realtime.
5
5
  * Cache root: ~/.supatype/cache/{component}/{version}/
6
6
  * Override path: config.overrides?.{component} (local build path).
7
7
  *
@@ -85,6 +85,7 @@ export function describeActiveOverrides(config: SupatypeProjectConfig): string[]
85
85
  add("server", o.server)
86
86
  add("postgres_dir", o.postgres_dir)
87
87
  add("deno", o.deno)
88
+ add("realtime", o.realtime)
88
89
  add("studio", o.studio)
89
90
  add("postgrest", o.postgrest)
90
91
  return lines
@@ -151,6 +152,7 @@ const CDN_PATHS: Record<Component, (version: string, platform: PlatformId) => st
151
152
  server: (v, p) => `/server/v${v}/supatype-server-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
152
153
  postgres: (v, p) => `/postgres/v${v}/supatype-pg-${postgresArchiveTag(v)}-${p.os}-${p.arch}${p.os === "windows" ? ".zip" : ".tar.gz"}`,
153
154
  deno: (v, p) => `/deno/v${v}/deno-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
155
+ realtime: (v, p) => `/realtime/v${v}/supatype-realtime-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
154
156
  }
155
157
 
156
158
  // Checksums file path (one per version directory, covers all platform binaries).
@@ -190,6 +192,7 @@ function binaryName(component: Component, version: string, platform: PlatformId)
190
192
  case "server": return `supatype-server-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
191
193
  case "postgres": return `supatype-pg-${postgresArchiveTag(version)}-${platform.os}-${platform.arch}${win ? ".zip" : ".tar.gz"}`
192
194
  case "deno": return `deno-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
195
+ case "realtime": return `supatype-realtime-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
193
196
  }
194
197
  }
195
198
 
@@ -630,7 +633,7 @@ async function streamToFileWithProgress(url: string, destPath: string): Promise<
630
633
  // SHA256 verification
631
634
  // ---------------------------------------------------------------------------
632
635
 
633
- const EXECUTABLE_COMPONENTS = new Set<Component>(["engine", "server", "deno"])
636
+ const EXECUTABLE_COMPONENTS = new Set<Component>(["engine", "server", "deno", "realtime"])
634
637
 
635
638
  /** True when a cached file matches expected format for the current platform. */
636
639
  function cachedArtifactLooksValid(component: Component, filePath: string): boolean {
@@ -655,7 +658,7 @@ export function validateArtifactFormat(
655
658
 
656
659
  /**
657
660
  * Per-component CDN artifact shapes:
658
- * engine, server, deno — native executable (ELF / Mach-O / PE)
661
+ * engine, server, deno, realtime — native executable (ELF / Mach-O / PE)
659
662
  * postgres (unix) — .tar.gz (gzip)
660
663
  * postgres (windows) — .zip
661
664
  */
@@ -867,12 +870,22 @@ export async function fetchLatestVersion(component: Component): Promise<string>
867
870
  return data.version.trim()
868
871
  }
869
872
 
870
- /** Fetch the latest version for all components concurrently. */
871
- export async function fetchAllLatestVersions(): Promise<Record<Component, string>> {
873
+ /** Fetch the latest version for all components concurrently. Missing CDN entries are omitted. */
874
+ export async function fetchAllLatestVersions(): Promise<Partial<Record<Component, string>>> {
872
875
  const results = await Promise.all(
873
- BINARY_COMPONENTS.map(async (c) => [c, await fetchLatestVersion(c)] as const),
876
+ BINARY_COMPONENTS.map(async (c) => {
877
+ try {
878
+ return [c, await fetchLatestVersion(c)] as const
879
+ } catch {
880
+ return [c, undefined] as const
881
+ }
882
+ }),
874
883
  )
875
- return Object.fromEntries(results) as Record<Component, string>
884
+ const out: Partial<Record<Component, string>> = {}
885
+ for (const [c, v] of results) {
886
+ if (v) out[c] = v
887
+ }
888
+ return out
876
889
  }
877
890
 
878
891
  // ---------------------------------------------------------------------------
@@ -885,8 +898,10 @@ export async function fetchAllLatestVersions(): Promise<Record<Component, string
885
898
  * Fails gracefully when graceful=true (suitable for postinstall).
886
899
  */
887
900
  /**
888
- * Verify all cached binaries for the current platform (used by integration CI).
889
- * Throws if any cached component is missing or fails format checks.
901
+ * Verify cached binaries for the current platform (used by integration CI).
902
+ * Only checks components present in `versions` unpublished CDN components
903
+ * (no latest.json yet) are skipped with a log so CI can land before first release.
904
+ * Throws if a pinned component is missing from cache or fails format checks.
890
905
  */
891
906
  export function verifyCachedBinaries(versions: Partial<ComponentVersions> | undefined): void {
892
907
  if (!versions) {
@@ -896,7 +911,10 @@ export function verifyCachedBinaries(versions: Partial<ComponentVersions> | unde
896
911
  for (const component of BINARY_COMPONENTS) {
897
912
  const version = versions[component]
898
913
  if (typeof version !== "string" || version.trim() === "") {
899
- throw new Error(`[supatype] versions.${component} must be set`)
914
+ console.log(
915
+ `[supatype] skipping verify for ${component} (no version — CDN latest.json not published yet)`,
916
+ )
917
+ continue
900
918
  }
901
919
  const destPath = join(cachePath(component, version), binaryName(component, version, platform))
902
920
  if (!cachedArtifactLooksValid(component, destPath)) {
@@ -918,7 +936,7 @@ export async function downloadAll(
918
936
 
919
937
  for (const component of components) {
920
938
  const version = versions?.[component] ?? latest[component]
921
- if (version === VERSION_PIN_LOCAL) continue
939
+ if (!version || version === VERSION_PIN_LOCAL) continue
922
940
  try {
923
941
  await download(component, version, platform)
924
942
  } catch (err) {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Standalone CLI binary CDN download (self-update + curl|sh installers).
3
+ */
4
+
5
+ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
6
+ import { homedir } from "node:os"
7
+ import { join } from "node:path"
8
+ import { currentPlatform, type PlatformId } from "./binary-cache.js"
9
+
10
+ const CDN_BASE = "https://releases.supatype.com"
11
+
12
+ export function cliCdnPath(version: string, platform: PlatformId): string {
13
+ const win = platform.os === "windows"
14
+ return `/cli/v${version}/supatype-cli-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
15
+ }
16
+
17
+ export function cliCachePath(version: string, platform: PlatformId = currentPlatform()): string {
18
+ const win = platform.os === "windows"
19
+ const name = `supatype-cli-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
20
+ return join(homedir(), ".supatype", "cache", "cli", version, name)
21
+ }
22
+
23
+ export async function fetchStandaloneCliLatestVersion(): Promise<string> {
24
+ const resp = await fetch(`${CDN_BASE}/cli/latest.json`)
25
+ if (!resp.ok) {
26
+ throw new Error(`Failed to fetch CLI latest.json: HTTP ${resp.status}`)
27
+ }
28
+ const data = await resp.json() as { version?: unknown }
29
+ if (typeof data.version !== "string" || data.version.trim() === "") {
30
+ throw new Error("Invalid cli/latest.json: missing version")
31
+ }
32
+ return data.version.trim()
33
+ }
34
+
35
+ /** Download the standalone CLI binary for the current platform. */
36
+ export async function downloadStandaloneCli(version?: string): Promise<string> {
37
+ const platform = currentPlatform()
38
+ const resolved = version ?? await fetchStandaloneCliLatestVersion()
39
+ const dest = cliCachePath(resolved, platform)
40
+ if (existsSync(dest)) return dest
41
+
42
+ const url = `${CDN_BASE}${cliCdnPath(resolved, platform)}`
43
+ const resp = await fetch(url)
44
+ if (!resp.ok) {
45
+ throw new Error(`Failed to download CLI from ${url}: HTTP ${resp.status}`)
46
+ }
47
+ const buf = Buffer.from(await resp.arrayBuffer())
48
+ if (buf.length < 64) {
49
+ throw new Error("Downloaded CLI artifact is too small")
50
+ }
51
+ const head = buf.subarray(0, 5).toString("utf8").toLowerCase()
52
+ if (head.startsWith("<!doc") || head.startsWith("{")) {
53
+ throw new Error("Downloaded CLI artifact looks like an error page, not a binary")
54
+ }
55
+
56
+ mkdirSync(join(dest, ".."), { recursive: true })
57
+ writeFileSync(dest, buf)
58
+ if (platform.os !== "windows") {
59
+ chmodSync(dest, 0o755)
60
+ }
61
+ return dest
62
+ }
@@ -69,6 +69,16 @@ export interface EnsureFirstAdminOptions {
69
69
  compose?: { project: string; composePath: string }
70
70
  }
71
71
 
72
+ function resolveAdminConnection(
73
+ cwd: string,
74
+ config: SupatypeProjectConfig,
75
+ override?: string,
76
+ ): string {
77
+ if (override !== undefined) return override
78
+ if (hasEngineOverride(config)) return hostComposeDbUrlFromEnv(cwd)
79
+ return readEnvValue(cwd, "DATABASE_URL", connectionString(config))
80
+ }
81
+
72
82
  export function registerAdmin(program: Command): void {
73
83
  const adminCmd = program
74
84
  .command("admin")
@@ -90,7 +100,7 @@ export function registerAdmin(program: Command): void {
90
100
  }) => {
91
101
  const cwd = process.cwd()
92
102
  const config = loadConfig(cwd)
93
- const connection = opts.connection ?? connectionString(config)
103
+ const connection = resolveAdminConnection(cwd, config, opts.connection)
94
104
 
95
105
  const email = opts.email ?? (await promptText("Admin email"))
96
106
  if (!email || !email.includes("@")) {
@@ -150,7 +160,7 @@ export function registerAdmin(program: Command): void {
150
160
  async (opts: { email: string; role: string; connection?: string }) => {
151
161
  const cwd = process.cwd()
152
162
  const config = loadConfig(cwd)
153
- const connection = opts.connection ?? connectionString(config)
163
+ const connection = resolveAdminConnection(cwd, config, opts.connection)
154
164
 
155
165
  const pg = await importPg()
156
166
  const pool = new pg.Pool({ connectionString: connection, max: 2 })
@@ -198,7 +208,7 @@ export function registerAdmin(program: Command): void {
198
208
  .action(async (opts: { connection?: string }) => {
199
209
  const cwd = process.cwd()
200
210
  const config = loadConfig(cwd)
201
- const connection = opts.connection ?? connectionString(config)
211
+ const connection = resolveAdminConnection(cwd, config, opts.connection)
202
212
 
203
213
  const pg = await importPg()
204
214
  const pool = new pg.Pool({ connectionString: connection, max: 2 })
@@ -44,6 +44,9 @@ import { publishDevReady } from "../dev-ready-panel.js"
44
44
  import { probeDockerDaemon, reportDockerUnavailable } from "../docker-runtime.js"
45
45
  import { fatalError } from "../ui/fatal.js"
46
46
  import { registerDevShutdown } from "../dev-shutdown.js"
47
+ import { patchRouteManifest } from "../route-manifest.js"
48
+ import { resolveRealtimeLaunch } from "../realtime-launch.js"
49
+ import { writeAppViteEnv } from "../app-vite-env.js"
47
50
  import { ensureValkeySidecar, stopValkeySidecar } from "../valkey-sidecar.js"
48
51
  import {
49
52
  initdb,
@@ -72,6 +75,7 @@ function gotrueSMTPFromEmailConfig(email: SupatypeProjectConfig["email"] | undef
72
75
  }
73
76
 
74
77
  const NATIVE_PG_PORT = 5432
78
+ const REALTIME_PORT = "4000"
75
79
 
76
80
  function portCheckCommand(port: number): string {
77
81
  return process.platform === "win32"
@@ -316,6 +320,38 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
316
320
  const anonKey = signJwt({ ...jwtBase, role: "anon" }, LOCAL_JWT_SECRET)
317
321
  const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, LOCAL_JWT_SECRET)
318
322
 
323
+ const realtimeUrl = `http://127.0.0.1:${REALTIME_PORT}`
324
+ patchRouteManifest(manifestPath, {
325
+ realtime_enabled: true,
326
+ realtime_url: realtimeUrl,
327
+ })
328
+
329
+ writeAppViteEnv(cwd, config, `http://localhost:${serverPort}`, anonKey)
330
+
331
+ let realtimeProc: ProcessManager | null = null
332
+ try {
333
+ const launch = await resolveRealtimeLaunch(config, cwd)
334
+ realtimeProc = new ProcessManager(launch.bin, launch.args, {
335
+ label: "realtime",
336
+ pidDir,
337
+ colour: "\x1b[35m",
338
+ env: {
339
+ DATABASE_URL: dbURL,
340
+ JWT_SECRET: LOCAL_JWT_SECRET,
341
+ PORT: REALTIME_PORT,
342
+ SLOT_NAME: "supatype_realtime",
343
+ PUBLICATION_NAME: "supatype_realtime_pub",
344
+ },
345
+ })
346
+ realtimeProc.start()
347
+ console.log(`[supatype] Realtime service: ${realtimeUrl}`)
348
+ } catch (err) {
349
+ console.warn(
350
+ `[supatype] ⚠ Realtime unavailable — WebSocket subscriptions will not work.\n` +
351
+ ` ${(err as Error).message}`,
352
+ )
353
+ }
354
+
319
355
 
320
356
  const emailProvider = config.email?.provider ?? "console"
321
357
  const gotrueMailerProvider =
@@ -344,6 +380,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
344
380
  ? { SUPATYPE_DENO_SERVE_SCRIPT: denoServeScriptAbs }
345
381
  : {}),
346
382
  SUPATYPE_URL: `http://localhost:${serverPort}`,
383
+ SUPATYPE_REALTIME_URL: realtimeUrl,
347
384
  SUPATYPE_ANON_KEY: anonKey,
348
385
  SUPATYPE_SERVICE_ROLE_KEY: serviceRoleKey,
349
386
  PORT: serverPort,
@@ -518,6 +555,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
518
555
  console.log("[supatype] Shutting down...")
519
556
  await Promise.all([
520
557
  serverProc.stop(),
558
+ realtimeProc?.stop(),
521
559
  postgrestProc?.stop(),
522
560
  studioProc?.stop(),
523
561
  appProc?.stop(),
@@ -1201,7 +1201,9 @@ SITE_URL=http://localhost:3000`)
1201
1201
  `# Local API gateway (Kong) — unique per project so multiple stacks can run concurrently
1202
1202
  SUPATYPE_KONG_PORT=${opts.kongPort}
1203
1203
  PUBLIC_SUPATYPE_URL=${apiUrl}
1204
- API_EXTERNAL_URL=${apiUrl}`,
1204
+ API_EXTERNAL_URL=${apiUrl}
1205
+ VITE_SUPATYPE_URL=${apiUrl}
1206
+ VITE_SUPATYPE_ANON_KEY=`,
1205
1207
  )
1206
1208
  }
1207
1209
 
@@ -70,6 +70,14 @@ export function generateAndWriteKeys(
70
70
  let content = readFileSync(envPath, "utf8")
71
71
  content = upsertEnvVar(content, "ANON_KEY", anonKey)
72
72
  content = upsertEnvVar(content, "SERVICE_ROLE_KEY", serviceKey)
73
+ content = upsertEnvVar(content, "VITE_SUPATYPE_ANON_KEY", anonKey)
74
+ content = upsertEnvVar(content, "PUBLIC_SUPATYPE_ANON_KEY", anonKey)
75
+ content = upsertEnvVar(content, "EXPO_PUBLIC_SUPATYPE_ANON_KEY", anonKey)
76
+ const apiUrl = readEnvVar(content, "PUBLIC_SUPATYPE_URL") ?? readEnvVar(content, "API_EXTERNAL_URL")
77
+ if (apiUrl) {
78
+ content = upsertEnvVar(content, "VITE_SUPATYPE_URL", apiUrl)
79
+ content = upsertEnvVar(content, "EXPO_PUBLIC_SUPATYPE_URL", apiUrl)
80
+ }
73
81
  writeFileSync(envPath, content, "utf8")
74
82
  }
75
83
 
@@ -83,6 +91,14 @@ function upsertEnvVar(content: string, key: string, value: string): string {
83
91
  return `${content}${sep}${key}=${value}\n`
84
92
  }
85
93
 
94
+ function readEnvVar(content: string, key: string): string | undefined {
95
+ const re = new RegExp(`^${key}=(.*)$`, "m")
96
+ const match = re.exec(content)
97
+ if (!match?.[1]) return undefined
98
+ const value = match[1].trim()
99
+ return value.length > 0 ? value : undefined
100
+ }
101
+
86
102
  export function resolveSecret(dir: string = process.cwd()): string | undefined {
87
103
  // 1. Check environment variable
88
104
  const fromEnv = process.env["JWT_SECRET"]
@@ -2,12 +2,14 @@
2
2
  * supatype self-update — Phase 10.6C8.
3
3
  *
4
4
  * npm-installed CLI: instruct to use `npm update -g`.
5
- * Standalone / future curl|sh installs: CDN-published CLI binary replacement is not wired yet.
5
+ * Standalone curl|sh installs: replace binary from releases.supatype.com/cli/.
6
6
  */
7
7
 
8
8
  import type { Command } from "commander"
9
+ import { copyFileSync, renameSync } from "node:fs"
9
10
  import { sep } from "node:path"
10
- import { error, plain } from "../ui/messages.js"
11
+ import { downloadStandaloneCli, fetchStandaloneCliLatestVersion } from "../cli-standalone.js"
12
+ import { error, info, plain } from "../ui/messages.js"
11
13
 
12
14
  function looksLikeNpmOrWorkspaceCLI(): boolean {
13
15
  const main = process.argv[1] ?? ""
@@ -23,24 +25,43 @@ function looksLikeNpmOrWorkspaceCLI(): boolean {
23
25
  export function registerSelfUpdate(program: Command): void {
24
26
  program
25
27
  .command("self-update")
26
- .description(
27
- "Update the supatype CLI (npm: use npm update; standalone binary swap from CDN is not available yet)",
28
- )
29
- .action(() => {
28
+ .description("Update the supatype CLI (npm or standalone CDN binary)")
29
+ .action(async () => {
30
30
  if (looksLikeNpmOrWorkspaceCLI()) {
31
31
  plain(
32
32
  "This CLI was installed via npm (or runs from the package workspace).\n" +
33
33
  "Update with:\n\n npm update -g @supatype/cli\n\n" +
34
- "To bump engine/server/postgres/deno pinned in supatype.config.ts, use:\n\n supatype update\n",
34
+ "To bump engine/server/postgres/deno/realtime pinned in supatype.config.ts, use:\n\n supatype update\n",
35
35
  )
36
36
  return
37
37
  }
38
38
 
39
- error(
40
- "Standalone CLI self-update (replace binary from CDN) is not published yet.\n\n" +
41
- "Install or upgrade via npm:\n npm install -g @supatype/cli\n\n" +
42
- "Component binaries (engine, server, postgres, deno):\n supatype update\n",
43
- )
44
- process.exit(1)
39
+ const currentPath = process.argv[1]
40
+ if (!currentPath) {
41
+ error("Could not determine current CLI path.")
42
+ process.exit(1)
43
+ }
44
+
45
+ try {
46
+ const latest = await fetchStandaloneCliLatestVersion()
47
+ info(`Downloading supatype CLI v${latest}...`)
48
+ const downloaded = await downloadStandaloneCli(latest)
49
+ const backup = `${currentPath}.bak`
50
+ copyFileSync(currentPath, backup)
51
+ try {
52
+ renameSync(downloaded, currentPath)
53
+ } catch (err) {
54
+ copyFileSync(downloaded, currentPath)
55
+ }
56
+ info(`Updated to v${latest}.`)
57
+ plain("Run `supatype --version` to verify.")
58
+ } catch (err) {
59
+ error(
60
+ `Standalone CLI self-update failed: ${(err as Error).message}\n\n` +
61
+ "Install or upgrade via npm:\n npm install -g @supatype/cli\n\n" +
62
+ "Component binaries (engine, server, postgres, deno, realtime):\n supatype update\n",
63
+ )
64
+ process.exit(1)
65
+ }
45
66
  })
46
67
  }
@@ -59,7 +59,7 @@ export function registerUpdate(program: Command): void {
59
59
 
60
60
  const platform = currentPlatform()
61
61
 
62
- const components: Component[] = ["engine", "server", "postgres", "deno"]
62
+ const components: Component[] = ["engine", "server", "postgres", "deno", "realtime"]
63
63
  const updates: Array<{ component: Component; from: string; to: string }> = []
64
64
 
65
65
  info("Fetching latest component versions from CDN...")
@@ -67,6 +67,14 @@ export function registerUpdate(program: Command): void {
67
67
 
68
68
  for (const component of components) {
69
69
  const latest = latestVersions[component]
70
+ if (!latest) {
71
+ if (component === "realtime") {
72
+ info(`Skipping ${component}: not published on CDN yet`)
73
+ continue
74
+ }
75
+ error(`No latest version for ${component} on CDN`)
76
+ continue
77
+ }
70
78
  const current = pinnedVersion(component, config)
71
79
  if (!current) {
72
80
  if (opts.check) {
package/src/components.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** CDN-cached binaries resolved by the CLI (`engine`, `server`, `postgres`, `deno`). */
2
- export const BINARY_COMPONENTS = ["engine", "server", "postgres", "deno"] as const
1
+ /** CDN-cached binaries resolved by the CLI (`engine`, `server`, `postgres`, `deno`, `realtime`). */
2
+ export const BINARY_COMPONENTS = ["engine", "server", "postgres", "deno", "realtime"] as const
3
3
 
4
4
  export type Component = (typeof BINARY_COMPONENTS)[number]
5
5
 
@@ -8,6 +8,7 @@ import { dirname, join, resolve } from "node:path"
8
8
  import { spawnSync } from "node:child_process"
9
9
  import { startProxyDevApp, resolveProxyDevScript } from "./app/proxy-dev-app.js"
10
10
  import { loadSchemaAst } from "./config.js"
11
+ import { withComposeSchemaPushLock } from "./schema-push-lock.js"
11
12
  import {
12
13
  COMPOSE_DEV_KONG_PORT,
13
14
  connectionString,
@@ -187,7 +188,9 @@ function upsertDevComposeEnv(
187
188
  SERVICE_ROLE_KEY: serviceRoleKey,
188
189
  PUBLIC_SUPATYPE_ANON_KEY: anonKey,
189
190
  VITE_SUPATYPE_ANON_KEY: anonKey,
191
+ EXPO_PUBLIC_SUPATYPE_ANON_KEY: anonKey,
190
192
  PUBLIC_SUPATYPE_URL: apiUrl,
193
+ EXPO_PUBLIC_SUPATYPE_URL: apiUrl,
191
194
  SUPATYPE_KONG_PORT: String(kongPort),
192
195
  API_EXTERNAL_URL: apiUrl,
193
196
  SITE_URL: apiUrl,
@@ -236,12 +239,69 @@ async function waitComposeHealthy(paths: SelfHostComposePaths, cwd: string, maxM
236
239
  throw new Error("Compose db service did not become healthy in time")
237
240
  }
238
241
 
242
+ /** True when the named compose service has a running container. */
243
+ function composeServiceIsRunning(
244
+ paths: SelfHostComposePaths,
245
+ cwd: string,
246
+ composeProject: string,
247
+ service: string,
248
+ ): boolean {
249
+ const envFile = join(cwd, ".env")
250
+ const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
251
+ if (existsSync(envFile)) args.push("--env-file", envFile)
252
+ args.push("ps", "-q", "--status", "running", service)
253
+ const result = spawnSync("docker", args, { cwd, encoding: "utf8" })
254
+ return result.status === 0 && typeof result.stdout === "string" && result.stdout.trim() !== ""
255
+ }
256
+
257
+ /**
258
+ * Capture Postgres container logs before compose down destroys them.
259
+ * Writes `.supatype/ci-logs/db-*.log` and prints a tail for CI job logs.
260
+ */
261
+ function dumpComposeDbLogs(
262
+ paths: SelfHostComposePaths,
263
+ cwd: string,
264
+ composeProject: string,
265
+ reason: string,
266
+ ): void {
267
+ const logDir = join(cwd, ".supatype", "ci-logs")
268
+ mkdirSync(logDir, { recursive: true })
269
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
270
+ const logPath = join(logDir, `db-${stamp}.log`)
271
+
272
+ const envFile = join(cwd, ".env")
273
+ const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
274
+ if (existsSync(envFile)) args.push("--env-file", envFile)
275
+ args.push("logs", "--no-color", "--timestamps", "--tail", "800", "db")
276
+
277
+ const result = spawnSync("docker", args, {
278
+ cwd,
279
+ encoding: "utf8",
280
+ maxBuffer: 10 * 1024 * 1024,
281
+ })
282
+ const body = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
283
+ const content = body || `(empty — docker compose logs db exit ${result.status ?? "?"})\n`
284
+ try {
285
+ writeFileSync(logPath, content)
286
+ } catch {
287
+ /* best-effort */
288
+ }
289
+
290
+ const lines = content.split("\n")
291
+ const tail = lines.slice(-200).join("\n")
292
+ console.error(`[supatype] Postgres logs after ${reason} (saved ${logPath}):`)
293
+ console.error(tail)
294
+ }
295
+
239
296
  async function waitKongReady(kongPort: number, maxSec: number): Promise<void> {
240
297
  const base = `http://localhost:${kongPort}`
241
298
  for (let i = 0; i < maxSec; i++) {
242
299
  try {
243
- const res = await fetch(`${base}/auth/v1/health`)
244
- if (res.ok) return
300
+ const [auth, realtime] = await Promise.all([
301
+ fetch(`${base}/auth/v1/health`),
302
+ fetch(`${base}/realtime/v1/health`),
303
+ ])
304
+ if (auth.ok && realtime.ok) return
245
305
  } catch {
246
306
  /* retry */
247
307
  }
@@ -405,12 +465,20 @@ async function runComposeSchemaPush(
405
465
 
406
466
  console.log("[supatype] Applying schema via compose schema-engine...")
407
467
  const sources = writeSchemaSourcePushArtifacts(cwd)
408
- let push = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
409
- // Windows Docker bind mounts can lag briefly after the host write.
410
- if (push.status !== 0) {
411
- await new Promise((r) => setTimeout(r, 250))
412
- push = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
468
+ const runPush = async () => {
469
+ let result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
470
+ // Windows Docker bind mounts can lag briefly after the host write.
471
+ if (result.status !== 0) {
472
+ await new Promise((r) => setTimeout(r, 250))
473
+ result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
474
+ }
475
+ return result
413
476
  }
477
+ // B: only hold the advisory lock when realtime is already decoding — first-boot
478
+ // push (db only) must not take the lock; that path crashed under lock+DDL in CI.
479
+ const push = composeServiceIsRunning(paths, cwd, composeProject, "realtime")
480
+ ? await withComposeSchemaPushLock(paths, cwd, composeProject, runPush)
481
+ : await runPush()
414
482
  if (push.status !== 0) {
415
483
  _lastFailedAst = astJson
416
484
  const detail = filterComposeNoise(push.output) || push.output
@@ -750,23 +818,71 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
750
818
  await recoverStaleDevSession(cwd)
751
819
  await handleComposeProjectRename(cwd, config.project.name, paths)
752
820
 
753
- console.log("[supatype] Bringing up Docker Compose services...")
754
- const upStatus = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, {
821
+ console.log("[supatype] Bringing up Postgres (compose db)...")
822
+ const upDbStatus = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
755
823
  quiet: true,
756
824
  brand: devBrand,
757
825
  })
758
- if (upStatus !== 0) {
826
+ if (upDbStatus !== 0) {
759
827
  endDevSession()
760
- exitComposeFailed(upStatus, "Could not start the local Compose stack.", devBrand)
828
+ exitComposeFailed(upDbStatus, "Could not start Postgres (compose db service).", devBrand)
761
829
  }
762
830
 
763
831
  console.log("[supatype] Waiting for Postgres (compose)...")
764
832
  await waitComposeHealthy(paths, cwd, 180_000, project)
833
+ // Settle before DDL — pg_isready can pass slightly before the instance is stable.
834
+ await new Promise((r) => setTimeout(r, 3000))
765
835
 
836
+ // A: apply schema before realtime (and the rest of the stack) starts decoding WAL.
766
837
  const schemaPath = schemaPathFromProject(config, cwd)
767
- await runComposeSchemaPush(cwd, config, paths, schemaPath, project).catch((e: unknown) =>
768
- console.error("[supatype] Initial schema push failed:", (e as Error).message),
769
- )
838
+ {
839
+ const maxAttempts = 3
840
+ let lastErr: unknown
841
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
842
+ try {
843
+ await runComposeSchemaPush(cwd, config, paths, schemaPath, project)
844
+ lastErr = undefined
845
+ break
846
+ } catch (e: unknown) {
847
+ lastErr = e
848
+ console.error(
849
+ `[supatype] Initial schema push failed (attempt ${attempt}/${maxAttempts}):`,
850
+ (e as Error).message,
851
+ )
852
+ dumpComposeDbLogs(paths, cwd, project, `schema push attempt ${attempt}/${maxAttempts}`)
853
+ if (attempt < maxAttempts) {
854
+ console.log("[supatype] Resetting Postgres after failed schema push...")
855
+ runDockerCompose(paths.composePath, ["down", "-v"], cwd, project, {
856
+ quiet: true,
857
+ brand: devBrand,
858
+ })
859
+ runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
860
+ quiet: true,
861
+ brand: devBrand,
862
+ })
863
+ await waitComposeHealthy(paths, cwd, 120_000, project)
864
+ await new Promise((r) => setTimeout(r, 3000 * attempt))
865
+ }
866
+ }
867
+ }
868
+ if (lastErr) {
869
+ dumpComposeDbLogs(paths, cwd, project, "initial schema push exhausted")
870
+ endDevSession()
871
+ throw new Error(
872
+ `Initial schema push failed after ${maxAttempts} attempts: ${(lastErr as Error).message}`,
873
+ )
874
+ }
875
+ }
876
+
877
+ console.log("[supatype] Bringing up Docker Compose services...")
878
+ const upStatus = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, {
879
+ quiet: true,
880
+ brand: devBrand,
881
+ })
882
+ if (upStatus !== 0) {
883
+ endDevSession()
884
+ exitComposeFailed(upStatus, "Could not start the local Compose stack.", devBrand)
885
+ }
770
886
 
771
887
  if (localServerImage !== undefined) {
772
888
  console.log("[supatype] Recreating server with local image...")
@@ -783,6 +899,22 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
783
899
  }
784
900
  }
785
901
 
902
+ const pinnedRealtimeImage = readEnvValue(cwd, "SUPATYPE_REALTIME_IMAGE", "").trim()
903
+ if (pinnedRealtimeImage !== "") {
904
+ console.log("[supatype] Recreating realtime with pinned image...")
905
+ const rtStatus = runDockerCompose(
906
+ paths.composePath,
907
+ ["up", "-d", "--force-recreate", "--no-deps", "realtime"],
908
+ cwd,
909
+ project,
910
+ { quiet: true, brand: devBrand },
911
+ )
912
+ if (rtStatus !== 0) {
913
+ endDevSession()
914
+ exitComposeFailed(rtStatus, "Could not recreate the realtime container.", devBrand)
915
+ }
916
+ }
917
+
786
918
  console.log("[supatype] Waiting for API gateway...")
787
919
  await waitKongReady(kongPort, 120)
788
920
  console.log("[supatype] Waiting for storage API...")
@@ -116,6 +116,8 @@ export interface SupatypeProjectConfig {
116
116
  postgres_dir?: string
117
117
  /** Path to a local deno binary. */
118
118
  deno?: string
119
+ /** Path to a local supatype-realtime binary (or node entry script). */
120
+ realtime?: string
119
121
  /** Path to the @supatype/studio package directory (starts Vite dev server). */
120
122
  studio?: string
121
123
  /** Path to a local PostgREST binary. */