@supatype/cli 0.1.0-alpha.10 → 0.1.0-alpha.12

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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +98 -65
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/framework.js +1 -3
  5. package/dist/app/framework.js.map +1 -1
  6. package/dist/app/proxy-dev-app.d.ts +14 -0
  7. package/dist/app/proxy-dev-app.d.ts.map +1 -1
  8. package/dist/app/proxy-dev-app.js +109 -6
  9. package/dist/app/proxy-dev-app.js.map +1 -1
  10. package/dist/binary-cache.d.ts +1 -1
  11. package/dist/binary-cache.d.ts.map +1 -1
  12. package/dist/binary-cache.js +6 -1
  13. package/dist/binary-cache.js.map +1 -1
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +6 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/commands/adopt.d.ts +3 -0
  18. package/dist/commands/adopt.d.ts.map +1 -0
  19. package/dist/commands/adopt.js +58 -0
  20. package/dist/commands/adopt.js.map +1 -0
  21. package/dist/commands/cloud.d.ts +4 -9
  22. package/dist/commands/cloud.d.ts.map +1 -1
  23. package/dist/commands/cloud.js +49 -91
  24. package/dist/commands/cloud.js.map +1 -1
  25. package/dist/commands/db.d.ts.map +1 -1
  26. package/dist/commands/db.js +25 -47
  27. package/dist/commands/db.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +117 -74
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +21 -3
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/diff.d.ts.map +1 -1
  35. package/dist/commands/diff.js +37 -37
  36. package/dist/commands/diff.js.map +1 -1
  37. package/dist/commands/doctor.d.ts +3 -0
  38. package/dist/commands/doctor.d.ts.map +1 -0
  39. package/dist/commands/doctor.js +77 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/functions.d.ts.map +1 -1
  42. package/dist/commands/functions.js +80 -33
  43. package/dist/commands/functions.js.map +1 -1
  44. package/dist/commands/init.d.ts +1 -0
  45. package/dist/commands/init.d.ts.map +1 -1
  46. package/dist/commands/init.js +26 -4
  47. package/dist/commands/init.js.map +1 -1
  48. package/dist/commands/introspect.d.ts +3 -0
  49. package/dist/commands/introspect.d.ts.map +1 -0
  50. package/dist/commands/introspect.js +34 -0
  51. package/dist/commands/introspect.js.map +1 -0
  52. package/dist/commands/link-helpers.d.ts +15 -0
  53. package/dist/commands/link-helpers.d.ts.map +1 -0
  54. package/dist/commands/link-helpers.js +187 -0
  55. package/dist/commands/link-helpers.js.map +1 -0
  56. package/dist/commands/migrate.d.ts.map +1 -1
  57. package/dist/commands/migrate.js +116 -14
  58. package/dist/commands/migrate.js.map +1 -1
  59. package/dist/commands/pull.d.ts.map +1 -1
  60. package/dist/commands/pull.js +32 -5
  61. package/dist/commands/pull.js.map +1 -1
  62. package/dist/commands/push.d.ts.map +1 -1
  63. package/dist/commands/push.js +102 -129
  64. package/dist/commands/push.js.map +1 -1
  65. package/dist/commands/status.d.ts +1 -1
  66. package/dist/commands/status.d.ts.map +1 -1
  67. package/dist/commands/status.js +93 -29
  68. package/dist/commands/status.js.map +1 -1
  69. package/dist/commands/update.d.ts.map +1 -1
  70. package/dist/commands/update.js +6 -2
  71. package/dist/commands/update.js.map +1 -1
  72. package/dist/config.d.ts +2 -1
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/dev-compose.d.ts +23 -0
  76. package/dist/dev-compose.d.ts.map +1 -1
  77. package/dist/dev-compose.js +183 -6
  78. package/dist/dev-compose.js.map +1 -1
  79. package/dist/diff-output.d.ts +5 -1
  80. package/dist/diff-output.d.ts.map +1 -1
  81. package/dist/diff-output.js +69 -0
  82. package/dist/diff-output.js.map +1 -1
  83. package/dist/engine-client.d.ts +10 -1
  84. package/dist/engine-client.d.ts.map +1 -1
  85. package/dist/engine-client.js +64 -13
  86. package/dist/engine-client.js.map +1 -1
  87. package/dist/engine-push-output.d.ts +1 -0
  88. package/dist/engine-push-output.d.ts.map +1 -1
  89. package/dist/engine-push-output.js +4 -1
  90. package/dist/engine-push-output.js.map +1 -1
  91. package/dist/gitignore.d.ts +8 -0
  92. package/dist/gitignore.d.ts.map +1 -0
  93. package/dist/gitignore.js +41 -0
  94. package/dist/gitignore.js.map +1 -0
  95. package/dist/link.d.ts +66 -0
  96. package/dist/link.d.ts.map +1 -0
  97. package/dist/link.js +159 -0
  98. package/dist/link.js.map +1 -0
  99. package/dist/process-manager.d.ts +2 -0
  100. package/dist/process-manager.d.ts.map +1 -1
  101. package/dist/process-manager.js +2 -0
  102. package/dist/process-manager.js.map +1 -1
  103. package/dist/project-config.d.ts +8 -0
  104. package/dist/project-config.d.ts.map +1 -1
  105. package/dist/project-config.js.map +1 -1
  106. package/dist/pull-utils.d.ts +50 -14
  107. package/dist/pull-utils.d.ts.map +1 -1
  108. package/dist/pull-utils.js +152 -12
  109. package/dist/pull-utils.js.map +1 -1
  110. package/dist/resolve-target.d.ts +86 -0
  111. package/dist/resolve-target.d.ts.map +1 -0
  112. package/dist/resolve-target.js +291 -0
  113. package/dist/resolve-target.js.map +1 -0
  114. package/dist/runtime-routes.d.ts.map +1 -1
  115. package/dist/runtime-routes.js +7 -0
  116. package/dist/runtime-routes.js.map +1 -1
  117. package/dist/schema-ast-v2.d.ts +1 -1
  118. package/dist/schema-ast-v2.d.ts.map +1 -1
  119. package/dist/schema-ast-v2.js +2 -2
  120. package/dist/schema-ast-v2.js.map +1 -1
  121. package/dist/schema-sources.d.ts +40 -0
  122. package/dist/schema-sources.d.ts.map +1 -0
  123. package/dist/schema-sources.js +183 -0
  124. package/dist/schema-sources.js.map +1 -0
  125. package/dist/self-host-compose.d.ts +10 -0
  126. package/dist/self-host-compose.d.ts.map +1 -1
  127. package/dist/self-host-compose.js +85 -3
  128. package/dist/self-host-compose.js.map +1 -1
  129. package/dist/storage-provision.d.ts +4 -0
  130. package/dist/storage-provision.d.ts.map +1 -1
  131. package/dist/storage-provision.js +24 -2
  132. package/dist/storage-provision.js.map +1 -1
  133. package/dist/target-client.d.ts +10 -0
  134. package/dist/target-client.d.ts.map +1 -0
  135. package/dist/target-client.js +22 -0
  136. package/dist/target-client.js.map +1 -0
  137. package/dist/type-extractor.d.ts +11 -0
  138. package/dist/type-extractor.d.ts.map +1 -1
  139. package/dist/type-extractor.js +95 -8
  140. package/dist/type-extractor.js.map +1 -1
  141. package/package.json +1 -1
  142. package/src/app/framework.ts +1 -3
  143. package/src/app/proxy-dev-app.ts +113 -6
  144. package/src/binary-cache.ts +6 -1
  145. package/src/cli.ts +6 -0
  146. package/src/commands/adopt.ts +83 -0
  147. package/src/commands/cloud.ts +66 -108
  148. package/src/commands/db.ts +28 -52
  149. package/src/commands/deploy.ts +162 -104
  150. package/src/commands/dev.ts +24 -10
  151. package/src/commands/diff.ts +40 -41
  152. package/src/commands/doctor.ts +102 -0
  153. package/src/commands/functions.ts +95 -37
  154. package/src/commands/init.ts +25 -4
  155. package/src/commands/introspect.ts +47 -0
  156. package/src/commands/link-helpers.ts +228 -0
  157. package/src/commands/migrate.ts +163 -15
  158. package/src/commands/pull.ts +37 -9
  159. package/src/commands/push.ts +132 -166
  160. package/src/commands/status.ts +100 -33
  161. package/src/commands/update.ts +6 -2
  162. package/src/config.ts +2 -1
  163. package/src/dev-compose.ts +240 -6
  164. package/src/diff-output.ts +79 -1
  165. package/src/engine-client.ts +70 -13
  166. package/src/engine-push-output.ts +7 -3
  167. package/src/gitignore.ts +48 -0
  168. package/src/link.ts +242 -0
  169. package/src/process-manager.ts +4 -0
  170. package/src/project-config.ts +8 -0
  171. package/src/pull-utils.ts +217 -23
  172. package/src/resolve-target.ts +419 -0
  173. package/src/runtime-routes.ts +7 -0
  174. package/src/schema-ast-v2.ts +2 -1
  175. package/src/schema-sources.ts +248 -0
  176. package/src/self-host-compose.ts +87 -3
  177. package/src/storage-provision.ts +33 -1
  178. package/src/target-client.ts +40 -0
  179. package/src/type-extractor.ts +124 -11
  180. package/tests/cli-help.test.ts +27 -2
  181. package/tests/init.test.ts +1 -1
  182. package/tests/link.test.ts +148 -0
  183. package/tests/proxy-dev-app.test.ts +45 -1
  184. package/tests/pull-utils.test.ts +5 -4
  185. package/tests/runtime-contract.test.ts +44 -1
  186. package/tests/schema-sources.test.ts +119 -0
  187. package/tests/storage-provision.test.ts +100 -0
  188. package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,13 @@
1
1
  /**
2
- * supatype status — show current state of local dev services.
2
+ * supatype status — show linked target or local dev stack state.
3
3
  */
4
4
  import type { Command } from "commander"
5
5
  import { spawnSync } from "node:child_process"
6
+ import { existsSync } from "node:fs"
7
+ import { resolve } from "node:path"
6
8
  import { LOCAL_KONG_HOST_PORT, localKongBaseUrl } from "../local-gateway.js"
9
+ import { loadLocalEnvironment, loadProjectLink } from "../link.js"
10
+ import { resolveTarget, targetStatus } from "../resolve-target.js"
7
11
 
8
12
  interface ServiceStatus {
9
13
  name: string
@@ -16,42 +20,105 @@ interface ServiceStatus {
16
20
  export function registerStatus(program: Command): void {
17
21
  program
18
22
  .command("status")
19
- .description("Show current state of local dev services")
20
- .action(() => {
21
- const services: ServiceStatus[] = [
22
- { name: "Postgres", container: "supatype-postgres", port: 5432 },
23
- { name: "PostgREST", container: "supatype-postgrest", port: 3000 },
24
- { name: "GoTrue", container: "supatype-gotrue", port: 9999 },
25
- { name: "Kong", container: "supatype-kong", port: LOCAL_KONG_HOST_PORT },
26
- { name: "MinIO", container: "supatype-minio", port: 9000 },
27
- { name: "Realtime", container: "supatype-realtime", port: 4000 },
28
- { name: "Studio", container: "supatype-studio", port: 3100 },
29
- ].map((svc) => {
30
- const status = getContainerStatus(svc.container)
31
- const uptime = getContainerUptime(svc.container)
32
- return { ...svc, status, ...(uptime !== undefined && { uptime }) }
33
- })
34
-
35
- console.log("Supatype Local Development Stack\n")
36
-
37
- const maxName = Math.max(...services.map((s) => s.name.length))
38
- for (const svc of services) {
39
- const icon = svc.status === "running" ? "●" : svc.status === "stopped" ? "○" : "✕"
40
- const status = svc.status.padEnd(8)
41
- const port = svc.port ? `:${svc.port}` : ""
42
- const uptime = svc.uptime ? ` (${svc.uptime})` : ""
43
- console.log(` ${icon} ${svc.name.padEnd(maxName)} ${status} ${port}${uptime}`)
23
+ .description("Show linked target status or local dev services")
24
+ .option("--env <name>", "Target environment when linked")
25
+ .action(async (opts: { env?: string }) => {
26
+ const cwd = process.cwd()
27
+ const link = loadProjectLink(cwd)
28
+ const localEnv = loadLocalEnvironment(cwd)
29
+
30
+ if (link || localEnv) {
31
+ try {
32
+ const target = resolveTarget(cwd, { env: opts.env })
33
+ if (target.mode !== "direct") {
34
+ await printLinkedStatus(target)
35
+ return
36
+ }
37
+ } catch (err) {
38
+ console.error((err as Error).message)
39
+ process.exitCode = 1
40
+ return
41
+ }
44
42
  }
45
43
 
46
- const running = services.filter((s) => s.status === "running")
47
- console.log(`\n${running.length}/${services.length} services running`)
44
+ printLocalStackStatus(cwd)
45
+ })
46
+ }
47
+
48
+ async function printLinkedStatus(target: ReturnType<typeof resolveTarget>): Promise<void> {
49
+ console.log(`Target: ${target.mode} (${target.environment})`)
50
+ console.log(`Project: ${target.projectRef}`)
51
+ console.log(`API: ${target.apiBaseUrl}${target.apiPrefix}\n`)
48
52
 
49
- if (running.length > 0) {
50
- console.log(`\nAPI URL: ${localKongBaseUrl()}`)
51
- console.log(`Studio: http://localhost:3100`)
52
- console.log(`Database: postgresql://supatype_admin:postgres@localhost:5432/postgres`)
53
+ try {
54
+ const data = (await targetStatus(target)) as Record<string, unknown>
55
+ if (data.functions && Array.isArray(data.functions)) {
56
+ console.log(`Functions (${data.functions.length}):`)
57
+ for (const fn of data.functions as Array<{ name?: string } | string>) {
58
+ const name = typeof fn === "string" ? fn : fn.name
59
+ if (name) console.log(` • ${name}`)
53
60
  }
54
- })
61
+ console.log()
62
+ }
63
+ if (data.deploymentId) {
64
+ console.log(`Active deployment: ${data.deploymentId}`)
65
+ }
66
+ if (data.controlPlane) {
67
+ console.log(`Control plane: ${data.controlPlane}`)
68
+ }
69
+ } catch (err) {
70
+ console.warn(`Could not fetch remote status: ${(err as Error).message}`)
71
+ }
72
+ }
73
+
74
+ function printLocalStackStatus(cwd: string): void {
75
+ const localEnv = loadLocalEnvironment(cwd)
76
+ const kongPort = localEnv?.kongPort ?? LOCAL_KONG_HOST_PORT
77
+
78
+ const services: ServiceStatus[] = [
79
+ { name: "Postgres", container: "supatype-postgres", port: 5432 },
80
+ { name: "PostgREST", container: "supatype-postgrest", port: 3000 },
81
+ { name: "GoTrue", container: "supatype-gotrue", port: 9999 },
82
+ { name: "Kong", container: "supatype-kong", port: kongPort },
83
+ { name: "Control plane", container: "supatype-control-plane" },
84
+ { name: "MinIO", container: "supatype-minio", port: 9000 },
85
+ { name: "Realtime", container: "supatype-realtime", port: 4000 },
86
+ { name: "Studio", container: "supatype-studio", port: 3100 },
87
+ ].map((svc) => {
88
+ const status = getContainerStatus(svc.container)
89
+ const uptime = getContainerUptime(svc.container)
90
+ return { ...svc, status, ...(uptime !== undefined && { uptime }) }
91
+ })
92
+
93
+ console.log("Supatype Local Development Stack\n")
94
+
95
+ const maxName = Math.max(...services.map((s) => s.name.length))
96
+ for (const svc of services) {
97
+ const icon = svc.status === "running" ? "●" : svc.status === "stopped" ? "○" : "✕"
98
+ const status = svc.status.padEnd(8)
99
+ const port = svc.port ? `:${svc.port}` : ""
100
+ const uptime = svc.uptime ? ` (${svc.uptime})` : ""
101
+ console.log(` ${icon} ${svc.name.padEnd(maxName)} ${status} ${port}${uptime}`)
102
+ }
103
+
104
+ const running = services.filter((s) => s.status === "running")
105
+ console.log(`\n${running.length}/${services.length} services running`)
106
+
107
+ if (running.length > 0) {
108
+ const apiUrl = localEnv?.apiUrl ?? localKongBaseUrl()
109
+ console.log(`\nAPI URL: ${apiUrl}`)
110
+ console.log(`Studio: http://localhost:3100`)
111
+ if (localEnv?.databaseUrl) {
112
+ console.log(`Database: ${localEnv.databaseUrl}`)
113
+ } else {
114
+ console.log(`Database: postgresql://supatype_admin:postgres@localhost:5432/postgres`)
115
+ }
116
+ }
117
+
118
+ if (existsSync(resolve(cwd, ".supatype/environment.json"))) {
119
+ console.log("\nLocal environment file: .supatype/environment.json")
120
+ console.log("Link remote ops: supatype link --url <api> --token $SERVICE_ROLE_KEY")
121
+ }
55
122
  }
56
123
 
57
124
  function getContainerStatus(name: string): "running" | "stopped" | "error" {
@@ -8,7 +8,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
8
8
  import { basename, resolve } from "node:path"
9
9
  import { loadConfig } from "../config.js"
10
10
  import { resolveRuntimeProvider } from "../project-config.js"
11
- import { runDockerCompose, writeSelfHostCompose } from "../self-host-compose.js"
11
+ import { runDockerCompose, writeSelfHostCompose, composePullNeedsIgnoreFailures } from "../self-host-compose.js"
12
12
  import { syncComposeImagePins } from "../dev-compose.js"
13
13
  import { download, currentPlatform, fetchAllLatestVersions, pinnedVersion, type Component } from "../binary-cache.js"
14
14
 
@@ -42,7 +42,11 @@ export function registerUpdate(program: Command): void {
42
42
  const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
43
43
  syncComposeImagePins(cwd, config)
44
44
  console.log("Pulling self-host compose images...")
45
- const status = runDockerCompose(paths.composePath, ["pull"], cwd)
45
+ const pullArgs = ["pull"]
46
+ if (composePullNeedsIgnoreFailures(config, cwd)) {
47
+ pullArgs.push("--ignore-pull-failures")
48
+ }
49
+ const status = runDockerCompose(paths.composePath, pullArgs, cwd)
46
50
  if (status !== 0) process.exit(status)
47
51
  console.log("Compose images updated.")
48
52
  return
package/src/config.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type SupatypeProjectConfig,
10
10
  } from "./project-config.js"
11
11
  import { extractSchemaAstFromTypes } from "./type-extractor.js"
12
+ import type { ExtractedSchemaAstV2 } from "./schema-ast-v2.js"
12
13
 
13
14
  export type { SupatypeProjectConfig } from "./project-config.js"
14
15
 
@@ -206,7 +207,7 @@ process.stdout.write(JSON.stringify(config))
206
207
  export function loadSchemaAst(
207
208
  schemaPath: string,
208
209
  cwd: string = process.cwd(),
209
- ): unknown {
210
+ ): ExtractedSchemaAstV2 {
210
211
  const extracted = extractSchemaAstFromTypes(schemaPath, cwd)
211
212
  if (extracted !== null) return extracted
212
213
 
@@ -10,6 +10,7 @@ import { startProxyDevApp } from "./app/proxy-dev-app.js"
10
10
  import { loadSchemaAst } from "./config.js"
11
11
  import {
12
12
  COMPOSE_DEV_KONG_PORT,
13
+ connectionString,
13
14
  projectRootFromConfig,
14
15
  resolveRuntimeProvider,
15
16
  schemaPathFromProject,
@@ -28,16 +29,21 @@ import {
28
29
  } from "./self-host-compose.js"
29
30
  import { hasEngineOverride } from "./binary-cache.js"
30
31
  import { startStudioViteDevServer } from "./studio-dev-server.js"
31
- import { ensureEngine, engineRequest } from "./engine-client.js"
32
+ import { ensureEngine, engineRequest, type DiffResult } from "./engine-client.js"
33
+ import { writeSchemaSourcePushArtifacts, type SchemaSourcePushArtifacts } from "./schema-sources.js"
32
34
  import { endDevSession } from "./dev-session.js"
35
+ import { writeLocalEnvironment } from "./link.js"
33
36
  import { registerDevShutdown } from "./dev-shutdown.js"
34
37
  import {
35
38
  filterComposeNoise,
36
39
  formatEnginePushMessage,
40
+ parseEngineJsonOutput,
37
41
  parseEnginePushOutput,
38
42
  } from "./engine-push-output.js"
39
43
  import { withAdminRoles } from "./studio-admin-roles.js"
40
44
  import { restoreSystemRelationTargets } from "./restore-system-relation-targets.js"
45
+ import { provisionBucketsFromAst } from "./storage-provision.js"
46
+ import type { ExtractedSchemaAstV2 } from "./schema-ast-v2.js"
41
47
 
42
48
  const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
43
49
 
@@ -94,6 +100,72 @@ function hostComposeDbUrl(cwd: string): string {
94
100
  return `postgresql://${user}:${pass}@127.0.0.1:${port}/${db}?sslmode=disable`
95
101
  }
96
102
 
103
+ /**
104
+ * When `provider: docker` and `overrides.engine` is set, ensure Postgres is published
105
+ * on the host (SUPATYPE_DEV_DB_PORT) so the local engine binary can connect.
106
+ */
107
+ export async function ensureDockerDbPublishedForHostEngine(
108
+ cwd: string,
109
+ config: SupatypeProjectConfig,
110
+ ): Promise<void> {
111
+ if (resolveRuntimeProvider(config) !== "docker") {
112
+ throw new Error("ensureDockerDbPublishedForHostEngine requires provider: docker")
113
+ }
114
+ if (!hasEngineOverride(config)) {
115
+ throw new Error(
116
+ "Docker Postgres is not published to the host without overrides.engine. " +
117
+ "Set overrides.engine in supatype.local.config.ts or pass --connection.",
118
+ )
119
+ }
120
+
121
+ const project = composeProjectName(config.project.name)
122
+ const kongPort = await resolveKongPort(cwd)
123
+ const devDbPort = await resolveDevDbPort(cwd)
124
+
125
+ const now = Math.floor(Date.now() / 1000)
126
+ const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
127
+ const anonKey = signJwt({ ...jwtBase, role: "anon" }, LOCAL_JWT_SECRET)
128
+ const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, LOCAL_JWT_SECRET)
129
+ ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, devDbPort)
130
+
131
+ const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
132
+ const up = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, { quiet: true })
133
+ if (up !== 0) process.exit(up)
134
+ await waitComposeHealthy(paths, cwd, 120_000, project)
135
+ }
136
+
137
+ /**
138
+ * True when CLI should publish local Compose Postgres for the host-side engine
139
+ * (local dev with overrides.engine). False for remote DB URLs via config or --connection.
140
+ */
141
+ export function usesLocalDockerEngineDb(
142
+ config: SupatypeProjectConfig,
143
+ explicitConnection?: string,
144
+ ): boolean {
145
+ if (explicitConnection?.trim()) return false
146
+ if (config.connection?.trim()) return false
147
+ return resolveRuntimeProvider(config) === "docker" && hasEngineOverride(config)
148
+ }
149
+
150
+ /**
151
+ * Resolve a Postgres URL reachable from the host-side engine binary.
152
+ * Local docker + overrides.engine → SUPATYPE_DEV_DB_PORT on localhost.
153
+ * Remote self-host → set `connection` in config or pass `--connection`.
154
+ */
155
+ export async function resolveHostEngineDatabaseUrl(
156
+ cwd: string,
157
+ config: SupatypeProjectConfig,
158
+ explicit?: string,
159
+ ): Promise<string> {
160
+ if (explicit?.trim()) return explicit
161
+ if (config.connection?.trim()) return config.connection
162
+ if (usesLocalDockerEngineDb(config)) {
163
+ await ensureDockerDbPublishedForHostEngine(cwd, config)
164
+ return hostComposeDbUrl(cwd)
165
+ }
166
+ return connectionString(config)
167
+ }
168
+
97
169
  async function resolveKongPort(cwd: string): Promise<number> {
98
170
  const envPath = join(cwd, ".env")
99
171
  if (existsSync(envPath)) {
@@ -151,6 +223,8 @@ function ensureDevComposeEnv(
151
223
  }
152
224
  if (devDbPort !== undefined) {
153
225
  updates.SUPATYPE_DEV_DB_PORT = String(devDbPort)
226
+ updates.DATABASE_URL =
227
+ `postgresql://supatype_admin:postgres@localhost:${devDbPort}/supatype?sslmode=disable`
154
228
  }
155
229
  const removeImageKeys = COMPOSE_PINNED_IMAGE_ENV_KEYS.filter((key) => !(key in imagePins))
156
230
  upsertEnvFile(cwd, updates, removeImageKeys)
@@ -189,6 +263,14 @@ async function waitKongReady(kongPort: number, maxSec: number): Promise<void> {
189
263
  throw new Error(`Kong gateway at ${base} did not become ready within ${maxSec}s`)
190
264
  }
191
265
 
266
+ async function provisionDockerStorageBuckets(
267
+ ast: ExtractedSchemaAstV2,
268
+ kongPort: number,
269
+ serviceRoleKey: string,
270
+ ): Promise<void> {
271
+ await provisionBucketsFromAst(ast, `http://localhost:${kongPort}/storage/v1`, serviceRoleKey)
272
+ }
273
+
192
274
  let _lastPushedAst: string | null = null
193
275
  let _lastFailedAst: string | null = null
194
276
  let _composePushInFlight = false
@@ -279,12 +361,19 @@ async function runComposeSchemaPush(
279
361
  console.log("[supatype] Applying schema via local engine (overrides.engine)...")
280
362
  await ensureEngine()
281
363
  const pgSchema = config.schema?.pg_schema ?? "public"
364
+ const sources = writeSchemaSourcePushArtifacts(cwd)
282
365
  try {
283
366
  await engineRequest("/push", {
284
367
  ast,
285
368
  database_url: hostComposeDbUrl(cwd),
286
369
  schema: pgSchema,
287
370
  force: true,
371
+ ...(sources
372
+ ? {
373
+ schema_sources_gz_base64: sources.payload.dataBase64,
374
+ schema_sources_manifest: sources.payload.manifest,
375
+ }
376
+ : {}),
288
377
  })
289
378
  } catch (err) {
290
379
  _lastFailedAst = astJson
@@ -300,11 +389,12 @@ async function runComposeSchemaPush(
300
389
  }
301
390
 
302
391
  console.log("[supatype] Applying schema via compose schema-engine...")
303
- let push = await runComposeEnginePush(paths, cwd, composeProject, config)
392
+ const sources = writeSchemaSourcePushArtifacts(cwd)
393
+ let push = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
304
394
  // Windows Docker bind mounts can lag briefly after the host write.
305
395
  if (push.status !== 0) {
306
396
  await new Promise((r) => setTimeout(r, 250))
307
- push = await runComposeEnginePush(paths, cwd, composeProject, config)
397
+ push = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
308
398
  }
309
399
  if (push.status !== 0) {
310
400
  _lastFailedAst = astJson
@@ -347,6 +437,7 @@ async function runComposeEnginePush(
347
437
  cwd: string,
348
438
  composeProject: string,
349
439
  config: SupatypeProjectConfig,
440
+ sources?: SchemaSourcePushArtifacts | null,
350
441
  ): Promise<{ status: number; output: string }> {
351
442
  const envFile = resolve(cwd, ".env")
352
443
  const composeArgs = ["compose", "--progress", "quiet"]
@@ -370,6 +461,14 @@ async function runComposeEnginePush(
370
461
  "--force",
371
462
  "--non-interactive",
372
463
  )
464
+ if (sources) {
465
+ composeArgs.push(
466
+ "--schema-sources-gz",
467
+ sources.dockerGzPath,
468
+ "--schema-sources-manifest",
469
+ sources.dockerManifestPath,
470
+ )
471
+ }
373
472
  const pushEnv: NodeJS.ProcessEnv = {
374
473
  ...process.env,
375
474
  COMPOSE_PROGRESS: "quiet",
@@ -399,6 +498,117 @@ async function runComposeEnginePush(
399
498
  return { status: exitStatus, output }
400
499
  }
401
500
 
501
+ async function runComposeEngineDiff(
502
+ paths: SelfHostComposePaths,
503
+ cwd: string,
504
+ composeProject: string,
505
+ config: SupatypeProjectConfig,
506
+ pgSchema: string,
507
+ ): Promise<{ status: number; output: string; diff: DiffResult | null }> {
508
+ const envFile = resolve(cwd, ".env")
509
+ const composeArgs = ["compose", "--progress", "quiet"]
510
+ if (composeProject) composeArgs.push("-p", composeProject)
511
+ composeArgs.push("--project-directory", cwd)
512
+ composeArgs.push("-f", paths.composePath)
513
+ if (existsSync(envFile)) {
514
+ composeArgs.push("--env-file", envFile)
515
+ }
516
+ composeArgs.push(
517
+ "--profile",
518
+ "tools",
519
+ "run",
520
+ "--rm",
521
+ "schema-engine",
522
+ "diff",
523
+ "-i",
524
+ "/project/.supatype/schema.ast.json",
525
+ "--database-url",
526
+ composeDbUrl(),
527
+ "--schema",
528
+ pgSchema,
529
+ )
530
+ const diffEnv: NodeJS.ProcessEnv = {
531
+ ...process.env,
532
+ COMPOSE_PROGRESS: "quiet",
533
+ }
534
+ const engineImage = await schemaEngineImageForPush(config)
535
+ if (engineImage) {
536
+ diffEnv.SUPATYPE_ENGINE_IMAGE = engineImage
537
+ }
538
+ const result = spawnSync("docker", composeArgs, {
539
+ cwd,
540
+ encoding: "utf8",
541
+ maxBuffer: 10 * 1024 * 1024,
542
+ env: diffEnv,
543
+ })
544
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
545
+ const exitStatus = result.status ?? 1
546
+ const diff = parseEngineJsonOutput<DiffResult>(output)
547
+
548
+ return { status: exitStatus, output, diff }
549
+ }
550
+
551
+ /**
552
+ * `supatype diff` when `provider: docker`. Uses in-compose schema-engine unless
553
+ * `overrides.engine` is set — then Postgres is published to the host and diff runs
554
+ * through the local engine binary.
555
+ */
556
+ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfig): Promise<DiffResult> {
557
+ if (resolveRuntimeProvider(config) !== "docker") {
558
+ throw new Error("diffSchemaDocker requires provider: docker")
559
+ }
560
+ const project = composeProjectName(config.project.name)
561
+ const pgSchema = config.schema?.pg_schema ?? "public"
562
+
563
+ if (hasEngineOverride(config)) {
564
+ await ensureDockerDbPublishedForHostEngine(cwd, config)
565
+ const schemaPath = schemaPathFromProject(config, cwd)
566
+ const ast = loadSchemaAst(schemaPath, cwd)
567
+ await ensureEngine()
568
+ return engineRequest<DiffResult>("/diff", {
569
+ ast,
570
+ database_url: hostComposeDbUrl(cwd),
571
+ schema: pgSchema,
572
+ })
573
+ }
574
+
575
+ const kongPort = await resolveKongPort(cwd)
576
+ const now = Math.floor(Date.now() / 1000)
577
+ const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
578
+ const anonKey = signJwt({ ...jwtBase, role: "anon" }, LOCAL_JWT_SECRET)
579
+ const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, LOCAL_JWT_SECRET)
580
+ ensureDevComposeEnv(cwd, config, anonKey, serviceRoleKey, kongPort, undefined)
581
+
582
+ const paths = writeSelfHostCompose(cwd, config, { devLocal: true })
583
+
584
+ const up = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, { quiet: true })
585
+ if (up !== 0) process.exit(up)
586
+ await waitComposeHealthy(paths, cwd, 120_000, project)
587
+
588
+ const schemaPath = schemaPathFromProject(config, cwd)
589
+ const ast = loadSchemaAst(schemaPath, cwd)
590
+
591
+ const supatypeDir = join(cwd, ".supatype")
592
+ mkdirSync(supatypeDir, { recursive: true })
593
+ const astPath = join(supatypeDir, "schema.ast.json")
594
+ writeFileSync(astPath, JSON.stringify(ast))
595
+
596
+ let result = await runComposeEngineDiff(paths, cwd, project, config, pgSchema)
597
+ // Windows Docker bind mounts can lag briefly after the host write.
598
+ if (result.status !== 0) {
599
+ await new Promise((r) => setTimeout(r, 250))
600
+ result = await runComposeEngineDiff(paths, cwd, project, config, pgSchema)
601
+ }
602
+ if (result.status !== 0) {
603
+ const detail = filterComposeNoise(result.output) || result.output
604
+ throw new Error(detail || `Engine schema diff failed (exit ${result.status})`)
605
+ }
606
+ if (!result.diff) {
607
+ throw new Error("Engine diff returned no result")
608
+ }
609
+ return result.diff
610
+ }
611
+
402
612
  /**
403
613
  * `supatype push` when `provider: docker`. Uses in-compose schema-engine unless
404
614
  * `overrides.engine` is set — then Postgres is published to the host and push runs
@@ -426,7 +636,14 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
426
636
  await waitComposeHealthy(paths, cwd, 120_000, project)
427
637
 
428
638
  const schemaPath = schemaPathFromProject(config, cwd)
639
+ const ast = loadSchemaAst(schemaPath, cwd)
429
640
  await runComposeSchemaPush(cwd, config, paths, schemaPath, project)
641
+
642
+ const upGateway = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, { quiet: true })
643
+ if (upGateway !== 0) process.exit(upGateway)
644
+ await waitKongReady(kongPort, 120)
645
+ await provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey)
646
+
430
647
  console.log("[supatype] Schema pushed.")
431
648
  }
432
649
 
@@ -468,6 +685,18 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
468
685
  console.log("[supatype] Waiting for API gateway...")
469
686
  await waitKongReady(kongPort, 120)
470
687
 
688
+ writeLocalEnvironment(cwd, {
689
+ target: "local",
690
+ apiUrl: `http://localhost:${kongPort}`,
691
+ databaseUrl: hasEngineOverride(config) ? hostComposeDbUrl(cwd) : composeDbUrl(),
692
+ projectRef: config.project.name,
693
+ kongPort,
694
+ provider: "docker",
695
+ })
696
+
697
+ const ast = loadSchemaAst(schemaPath, cwd)
698
+ await provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey)
699
+
471
700
  const pidDir = join(homedir(), ".supatype", "projects", config.project.name, "pid")
472
701
  mkdirSync(pidDir, { recursive: true })
473
702
 
@@ -538,9 +767,14 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
538
767
  debounceTimer = setTimeout(() => {
539
768
  debounceTimer = null
540
769
  console.log(`\n[supatype] Change detected in ${filename}, pushing schema...`)
541
- runComposeSchemaPushQueued(cwd, config, paths, schemaPath, project).catch((e: unknown) =>
542
- console.error("[supatype] Schema push failed:", (e as Error).message),
543
- )
770
+ runComposeSchemaPushQueued(cwd, config, paths, schemaPath, project)
771
+ .then(async () => {
772
+ const updatedAst = loadSchemaAst(schemaPath, cwd)
773
+ await provisionDockerStorageBuckets(updatedAst, kongPort, serviceRoleKey)
774
+ })
775
+ .catch((e: unknown) =>
776
+ console.error("[supatype] Schema push failed:", (e as Error).message),
777
+ )
544
778
  }, 300)
545
779
  })
546
780
  }
@@ -1,4 +1,44 @@
1
- import type { DiffResult } from "./engine-client.js"
1
+ import type { DiffResult, Operation } from "./engine-client.js"
2
+
3
+ /** Human-readable label for a single schema operation. */
4
+ export function formatOperation(op: Operation): string {
5
+ if (typeof op.description === "string" && op.description.trim().length > 0) {
6
+ return op.description
7
+ }
8
+
9
+ const kind = typeof op.type === "string" ? op.type : typeof op.kind === "string" ? op.kind : "operation"
10
+ const raw = op as unknown as Record<string, unknown>
11
+ const table = raw["table"]
12
+ const column = raw["column"]
13
+ const index = raw["index"]
14
+ const sql = typeof op.sql === "string" ? op.sql.trim() : ""
15
+
16
+ if (kind === "add_unique_constraint" || kind === "drop_unique_constraint") {
17
+ const constraint = typeof raw["constraint"] === "string" ? raw["constraint"] : null
18
+ if (typeof table === "string" && constraint) return `${kind} ${table}.${constraint}`
19
+ }
20
+
21
+ if (kind === "create_index" || kind === "drop_index" || kind === "add_index") {
22
+ const indexName = typeof index === "string" ? index : typeof raw["name"] === "string" ? raw["name"] : null
23
+ const fields = Array.isArray(raw["fields"]) ? raw["fields"].join(", ") : null
24
+ if (indexName && fields) return `${kind} ${table}.${indexName} (${fields})`
25
+ if (indexName) return `${kind} ${table}.${indexName}`
26
+ }
27
+
28
+ if (typeof table === "string" && typeof column === "string") {
29
+ return `${kind} ${table}.${column}`
30
+ }
31
+ if (typeof table === "string") {
32
+ return `${kind} ${table}`
33
+ }
34
+
35
+ if (sql) {
36
+ const oneLine = sql.replace(/\s+/g, " ").slice(0, 120)
37
+ return `${kind}: ${oneLine}${sql.length > 120 ? "…" : ""}`
38
+ }
39
+
40
+ return kind
41
+ }
2
42
 
3
43
  /** Print engine diff warnings before the operation list. */
4
44
  export function printDiffWarnings(diff: DiffResult): void {
@@ -10,3 +50,41 @@ export function printDiffWarnings(diff: DiffResult): void {
10
50
  }
11
51
  console.log()
12
52
  }
53
+
54
+ const RISK_SYMBOL: Record<NonNullable<DiffResult["operations"][number]["risk"]>, string> = {
55
+ safe: "+",
56
+ warn: "~",
57
+ cautious: "~",
58
+ danger: "!",
59
+ destructive: "!",
60
+ }
61
+
62
+ const RISK_LEGEND: Record<NonNullable<DiffResult["operations"][number]["risk"]>, string> = {
63
+ safe: "safe",
64
+ warn: "caution",
65
+ cautious: "caution",
66
+ danger: "DANGER",
67
+ destructive: "DANGER",
68
+ }
69
+
70
+ /** Print planned schema operations from a diff result. */
71
+ export function printDiffOperations(diff: DiffResult): void {
72
+ const ops = diff.operations ?? []
73
+ if (ops.length === 0) {
74
+ console.log("No changes.")
75
+ return
76
+ }
77
+
78
+ console.log(`\n${ops.length} change(s):\n`)
79
+ for (const op of ops) {
80
+ const r = op.risk ?? "safe"
81
+ const label = op.warning ?? formatOperation(op)
82
+ console.log(` [${RISK_SYMBOL[r]}] ${label} (${RISK_LEGEND[r]})`)
83
+ }
84
+
85
+ const dangerous = ops.filter((o) => o.risk === "danger").length
86
+ if (dangerous > 0) {
87
+ console.log(`\n ${dangerous} dangerous operation(s). Review before pushing.`)
88
+ }
89
+ console.log()
90
+ }