@supatype/cli 0.1.3 → 0.1.5
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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +101 -99
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app-vite-env.d.ts +4 -0
- package/dist/app-vite-env.d.ts.map +1 -0
- package/dist/app-vite-env.js +22 -0
- package/dist/app-vite-env.js.map +1 -0
- package/dist/binary-cache.d.ts +7 -5
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +28 -10
- package/dist/binary-cache.js.map +1 -1
- package/dist/cli-standalone.d.ts +10 -0
- package/dist/cli-standalone.d.ts.map +1 -0
- package/dist/cli-standalone.js +56 -0
- package/dist/cli-standalone.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +34 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +14 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/self-update.d.ts +1 -1
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +33 -9
- package/dist/commands/self-update.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/components.d.ts +2 -2
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +2 -2
- package/dist/components.js.map +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +120 -12
- package/dist/dev-compose.js.map +1 -1
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js.map +1 -1
- package/dist/realtime-launch.d.ts +11 -0
- package/dist/realtime-launch.d.ts.map +1 -0
- package/dist/realtime-launch.js +39 -0
- package/dist/realtime-launch.js.map +1 -0
- package/dist/required-host-components.js +1 -1
- package/dist/required-host-components.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +15 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/schema-push-lock.d.ts +17 -0
- package/dist/schema-push-lock.d.ts.map +1 -0
- package/dist/schema-push-lock.js +106 -0
- package/dist/schema-push-lock.js.map +1 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +18 -0
- package/dist/self-host-compose.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +169 -13
- package/dist/type-extractor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/build-release-bundle.mjs +27 -0
- package/src/app-vite-env.ts +27 -0
- package/src/binary-cache.ts +29 -11
- package/src/cli-standalone.ts +62 -0
- package/src/commands/dev.ts +38 -0
- package/src/commands/init.ts +3 -1
- package/src/commands/keys.ts +14 -0
- package/src/commands/self-update.ts +34 -13
- package/src/commands/update.ts +9 -1
- package/src/components.ts +2 -2
- package/src/dev-compose.ts +144 -14
- package/src/project-config.ts +2 -0
- package/src/realtime-launch.ts +52 -0
- package/src/required-host-components.ts +1 -1
- package/src/route-manifest.ts +17 -0
- package/src/schema-push-lock.ts +131 -0
- package/src/self-host-compose.ts +18 -0
- package/src/type-extractor.ts +213 -13
- package/tests/realtime-launch.test.ts +25 -0
- package/tests/required-host-components.test.ts +2 -0
- package/tests/route-manifest.test.ts +29 -0
- package/tests/type-extractor.test.ts +153 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/binary-cache.ts
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
|
889
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/dev.ts
CHANGED
|
@@ -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(),
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
package/src/commands/keys.ts
CHANGED
|
@@ -70,6 +70,12 @@ 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
|
+
const apiUrl = readEnvVar(content, "PUBLIC_SUPATYPE_URL") ?? readEnvVar(content, "API_EXTERNAL_URL")
|
|
76
|
+
if (apiUrl) {
|
|
77
|
+
content = upsertEnvVar(content, "VITE_SUPATYPE_URL", apiUrl)
|
|
78
|
+
}
|
|
73
79
|
writeFileSync(envPath, content, "utf8")
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -83,6 +89,14 @@ function upsertEnvVar(content: string, key: string, value: string): string {
|
|
|
83
89
|
return `${content}${sep}${key}=${value}\n`
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
function readEnvVar(content: string, key: string): string | undefined {
|
|
93
|
+
const re = new RegExp(`^${key}=(.*)$`, "m")
|
|
94
|
+
const match = re.exec(content)
|
|
95
|
+
if (!match?.[1]) return undefined
|
|
96
|
+
const value = match[1].trim()
|
|
97
|
+
return value.length > 0 ? value : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
86
100
|
export function resolveSecret(dir: string = process.cwd()): string | undefined {
|
|
87
101
|
// 1. Check environment variable
|
|
88
102
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -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
|
|
package/src/dev-compose.ts
CHANGED
|
@@ -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,
|
|
@@ -236,12 +237,69 @@ async function waitComposeHealthy(paths: SelfHostComposePaths, cwd: string, maxM
|
|
|
236
237
|
throw new Error("Compose db service did not become healthy in time")
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
/** True when the named compose service has a running container. */
|
|
241
|
+
function composeServiceIsRunning(
|
|
242
|
+
paths: SelfHostComposePaths,
|
|
243
|
+
cwd: string,
|
|
244
|
+
composeProject: string,
|
|
245
|
+
service: string,
|
|
246
|
+
): boolean {
|
|
247
|
+
const envFile = join(cwd, ".env")
|
|
248
|
+
const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
|
|
249
|
+
if (existsSync(envFile)) args.push("--env-file", envFile)
|
|
250
|
+
args.push("ps", "-q", "--status", "running", service)
|
|
251
|
+
const result = spawnSync("docker", args, { cwd, encoding: "utf8" })
|
|
252
|
+
return result.status === 0 && typeof result.stdout === "string" && result.stdout.trim() !== ""
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Capture Postgres container logs before compose down destroys them.
|
|
257
|
+
* Writes `.supatype/ci-logs/db-*.log` and prints a tail for CI job logs.
|
|
258
|
+
*/
|
|
259
|
+
function dumpComposeDbLogs(
|
|
260
|
+
paths: SelfHostComposePaths,
|
|
261
|
+
cwd: string,
|
|
262
|
+
composeProject: string,
|
|
263
|
+
reason: string,
|
|
264
|
+
): void {
|
|
265
|
+
const logDir = join(cwd, ".supatype", "ci-logs")
|
|
266
|
+
mkdirSync(logDir, { recursive: true })
|
|
267
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
268
|
+
const logPath = join(logDir, `db-${stamp}.log`)
|
|
269
|
+
|
|
270
|
+
const envFile = join(cwd, ".env")
|
|
271
|
+
const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
|
|
272
|
+
if (existsSync(envFile)) args.push("--env-file", envFile)
|
|
273
|
+
args.push("logs", "--no-color", "--timestamps", "--tail", "800", "db")
|
|
274
|
+
|
|
275
|
+
const result = spawnSync("docker", args, {
|
|
276
|
+
cwd,
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
279
|
+
})
|
|
280
|
+
const body = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
281
|
+
const content = body || `(empty — docker compose logs db exit ${result.status ?? "?"})\n`
|
|
282
|
+
try {
|
|
283
|
+
writeFileSync(logPath, content)
|
|
284
|
+
} catch {
|
|
285
|
+
/* best-effort */
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const lines = content.split("\n")
|
|
289
|
+
const tail = lines.slice(-200).join("\n")
|
|
290
|
+
console.error(`[supatype] Postgres logs after ${reason} (saved ${logPath}):`)
|
|
291
|
+
console.error(tail)
|
|
292
|
+
}
|
|
293
|
+
|
|
239
294
|
async function waitKongReady(kongPort: number, maxSec: number): Promise<void> {
|
|
240
295
|
const base = `http://localhost:${kongPort}`
|
|
241
296
|
for (let i = 0; i < maxSec; i++) {
|
|
242
297
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
298
|
+
const [auth, realtime] = await Promise.all([
|
|
299
|
+
fetch(`${base}/auth/v1/health`),
|
|
300
|
+
fetch(`${base}/realtime/v1/health`),
|
|
301
|
+
])
|
|
302
|
+
if (auth.ok && realtime.ok) return
|
|
245
303
|
} catch {
|
|
246
304
|
/* retry */
|
|
247
305
|
}
|
|
@@ -405,12 +463,20 @@ async function runComposeSchemaPush(
|
|
|
405
463
|
|
|
406
464
|
console.log("[supatype] Applying schema via compose schema-engine...")
|
|
407
465
|
const sources = writeSchemaSourcePushArtifacts(cwd)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
466
|
+
const runPush = async () => {
|
|
467
|
+
let result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
|
|
468
|
+
// Windows Docker bind mounts can lag briefly after the host write.
|
|
469
|
+
if (result.status !== 0) {
|
|
470
|
+
await new Promise((r) => setTimeout(r, 250))
|
|
471
|
+
result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
|
|
472
|
+
}
|
|
473
|
+
return result
|
|
413
474
|
}
|
|
475
|
+
// B: only hold the advisory lock when realtime is already decoding — first-boot
|
|
476
|
+
// push (db only) must not take the lock; that path crashed under lock+DDL in CI.
|
|
477
|
+
const push = composeServiceIsRunning(paths, cwd, composeProject, "realtime")
|
|
478
|
+
? await withComposeSchemaPushLock(paths, cwd, composeProject, runPush)
|
|
479
|
+
: await runPush()
|
|
414
480
|
if (push.status !== 0) {
|
|
415
481
|
_lastFailedAst = astJson
|
|
416
482
|
const detail = filterComposeNoise(push.output) || push.output
|
|
@@ -750,23 +816,71 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
750
816
|
await recoverStaleDevSession(cwd)
|
|
751
817
|
await handleComposeProjectRename(cwd, config.project.name, paths)
|
|
752
818
|
|
|
753
|
-
console.log("[supatype] Bringing up
|
|
754
|
-
const
|
|
819
|
+
console.log("[supatype] Bringing up Postgres (compose db)...")
|
|
820
|
+
const upDbStatus = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
|
|
755
821
|
quiet: true,
|
|
756
822
|
brand: devBrand,
|
|
757
823
|
})
|
|
758
|
-
if (
|
|
824
|
+
if (upDbStatus !== 0) {
|
|
759
825
|
endDevSession()
|
|
760
|
-
exitComposeFailed(
|
|
826
|
+
exitComposeFailed(upDbStatus, "Could not start Postgres (compose db service).", devBrand)
|
|
761
827
|
}
|
|
762
828
|
|
|
763
829
|
console.log("[supatype] Waiting for Postgres (compose)...")
|
|
764
830
|
await waitComposeHealthy(paths, cwd, 180_000, project)
|
|
831
|
+
// Settle before DDL — pg_isready can pass slightly before the instance is stable.
|
|
832
|
+
await new Promise((r) => setTimeout(r, 3000))
|
|
765
833
|
|
|
834
|
+
// A: apply schema before realtime (and the rest of the stack) starts decoding WAL.
|
|
766
835
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
836
|
+
{
|
|
837
|
+
const maxAttempts = 3
|
|
838
|
+
let lastErr: unknown
|
|
839
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
840
|
+
try {
|
|
841
|
+
await runComposeSchemaPush(cwd, config, paths, schemaPath, project)
|
|
842
|
+
lastErr = undefined
|
|
843
|
+
break
|
|
844
|
+
} catch (e: unknown) {
|
|
845
|
+
lastErr = e
|
|
846
|
+
console.error(
|
|
847
|
+
`[supatype] Initial schema push failed (attempt ${attempt}/${maxAttempts}):`,
|
|
848
|
+
(e as Error).message,
|
|
849
|
+
)
|
|
850
|
+
dumpComposeDbLogs(paths, cwd, project, `schema push attempt ${attempt}/${maxAttempts}`)
|
|
851
|
+
if (attempt < maxAttempts) {
|
|
852
|
+
console.log("[supatype] Resetting Postgres after failed schema push...")
|
|
853
|
+
runDockerCompose(paths.composePath, ["down", "-v"], cwd, project, {
|
|
854
|
+
quiet: true,
|
|
855
|
+
brand: devBrand,
|
|
856
|
+
})
|
|
857
|
+
runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
|
|
858
|
+
quiet: true,
|
|
859
|
+
brand: devBrand,
|
|
860
|
+
})
|
|
861
|
+
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
862
|
+
await new Promise((r) => setTimeout(r, 3000 * attempt))
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (lastErr) {
|
|
867
|
+
dumpComposeDbLogs(paths, cwd, project, "initial schema push exhausted")
|
|
868
|
+
endDevSession()
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Initial schema push failed after ${maxAttempts} attempts: ${(lastErr as Error).message}`,
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
console.log("[supatype] Bringing up Docker Compose services...")
|
|
876
|
+
const upStatus = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, {
|
|
877
|
+
quiet: true,
|
|
878
|
+
brand: devBrand,
|
|
879
|
+
})
|
|
880
|
+
if (upStatus !== 0) {
|
|
881
|
+
endDevSession()
|
|
882
|
+
exitComposeFailed(upStatus, "Could not start the local Compose stack.", devBrand)
|
|
883
|
+
}
|
|
770
884
|
|
|
771
885
|
if (localServerImage !== undefined) {
|
|
772
886
|
console.log("[supatype] Recreating server with local image...")
|
|
@@ -783,6 +897,22 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
783
897
|
}
|
|
784
898
|
}
|
|
785
899
|
|
|
900
|
+
const pinnedRealtimeImage = readEnvValue(cwd, "SUPATYPE_REALTIME_IMAGE", "").trim()
|
|
901
|
+
if (pinnedRealtimeImage !== "") {
|
|
902
|
+
console.log("[supatype] Recreating realtime with pinned image...")
|
|
903
|
+
const rtStatus = runDockerCompose(
|
|
904
|
+
paths.composePath,
|
|
905
|
+
["up", "-d", "--force-recreate", "--no-deps", "realtime"],
|
|
906
|
+
cwd,
|
|
907
|
+
project,
|
|
908
|
+
{ quiet: true, brand: devBrand },
|
|
909
|
+
)
|
|
910
|
+
if (rtStatus !== 0) {
|
|
911
|
+
endDevSession()
|
|
912
|
+
exitComposeFailed(rtStatus, "Could not recreate the realtime container.", devBrand)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
786
916
|
console.log("[supatype] Waiting for API gateway...")
|
|
787
917
|
await waitKongReady(kongPort, 120)
|
|
788
918
|
console.log("[supatype] Waiting for storage API...")
|
package/src/project-config.ts
CHANGED
|
@@ -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. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { ensureBinary } from "./ensure-binary.js"
|
|
5
|
+
import type { SupatypeProjectConfig } from "./project-config.js"
|
|
6
|
+
|
|
7
|
+
export interface RealtimeLaunchSpec {
|
|
8
|
+
bin: string
|
|
9
|
+
args: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CLI_PKG_DIR = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
|
|
14
|
+
/** Monorepo contributor fallback: packages/realtime/dist/index.js relative to CLI package. */
|
|
15
|
+
function monorepoRealtimeEntry(): string | null {
|
|
16
|
+
const candidate = resolve(CLI_PKG_DIR, "../../realtime/dist/index.js")
|
|
17
|
+
return existsSync(candidate) ? candidate : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve how to spawn supatype-realtime for native dev.
|
|
22
|
+
* Order: overrides.realtime → CDN binary → node + monorepo dist.
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveRealtimeLaunch(
|
|
25
|
+
config: SupatypeProjectConfig,
|
|
26
|
+
cwd: string,
|
|
27
|
+
): Promise<RealtimeLaunchSpec> {
|
|
28
|
+
const override = config.overrides?.realtime?.trim()
|
|
29
|
+
if (override) {
|
|
30
|
+
const resolved = resolve(cwd, override)
|
|
31
|
+
if (!existsSync(resolved)) {
|
|
32
|
+
throw new Error(`[overrides] realtime path does not exist: ${resolved}`)
|
|
33
|
+
}
|
|
34
|
+
if (resolved.endsWith(".js") || resolved.endsWith(".mjs") || resolved.endsWith(".cjs")) {
|
|
35
|
+
return { bin: process.execPath, args: [resolved] }
|
|
36
|
+
}
|
|
37
|
+
return { bin: resolved, args: [] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const bin = await ensureBinary("realtime", config)
|
|
42
|
+
return { bin, args: [] }
|
|
43
|
+
} catch {
|
|
44
|
+
const entry = monorepoRealtimeEntry()
|
|
45
|
+
if (entry) {
|
|
46
|
+
return { bin: process.execPath, args: [entry] }
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
"realtime binary not available — run `supatype update` or set overrides.realtime in supatype.config.ts",
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -23,7 +23,7 @@ export function requiredHostComponents(
|
|
|
23
23
|
return ["engine"]
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const components: Component[] = ["engine", "server", "postgres"]
|
|
26
|
+
const components: Component[] = ["engine", "server", "postgres", "realtime"]
|
|
27
27
|
const hasFunctions = functionsPathCandidatesFromProject(config, cwd).some((dir) =>
|
|
28
28
|
existsSync(dir),
|
|
29
29
|
)
|