@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
@@ -12,11 +12,14 @@
12
12
  */
13
13
 
14
14
  import type { Command } from "commander"
15
- import { existsSync, readdirSync, statSync, createReadStream } from "node:fs"
16
- import { join } from "node:path"
15
+ import { existsSync, readdirSync, statSync, createReadStream, mkdirSync, cpSync } from "node:fs"
16
+ import { join, relative } from "node:path"
17
17
  import { loadConfig, loadSchemaAst } from "../config.js"
18
18
  import { connectionString, schemaPathFromProject } from "../project-config.js"
19
19
  import { deploySchemaToLinkedProject, loadCloudConfig, pushSchemaToLinkedProject } from "./cloud.js"
20
+ import { loadProjectLink } from "../link.js"
21
+ import { resolveTarget } from "../resolve-target.js"
22
+ import { targetFetch } from "../target-client.js"
20
23
  import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
21
24
  import { resolveAppConfig, validateStaticMode, validateBuildOutput, detectPackageManager } from "../app/framework.js"
22
25
  import { TIER_LIMITS, type Tier } from "./deploy-types.js"
@@ -29,7 +32,8 @@ export function registerDeploy(program: Command): void {
29
32
  "Deploy schema and app — Supatype Cloud by default when linked (`supatype link`); pass --local for engine + your database",
30
33
  )
31
34
  .option("--local", "Use local schema engine and database_url from config (skip cloud control plane)")
32
- .option("--environment <name>", "Cloud environment when using linked project", "production")
35
+ .option("--environment <name>", "Target environment when linked", "production")
36
+ .option("--env <name>", "Alias for --environment")
33
37
  .option("--app-only", "Skip schema push, only deploy the static site")
34
38
  .option("--schema-only", "Skip app build, only push schema changes")
35
39
  .option("--skip-build", "Deploy existing build output without building")
@@ -38,6 +42,7 @@ export function registerDeploy(program: Command): void {
38
42
  .action(async (opts: {
39
43
  local?: boolean
40
44
  environment?: string
45
+ env?: string
41
46
  appOnly?: boolean
42
47
  schemaOnly?: boolean
43
48
  skipBuild?: boolean
@@ -46,18 +51,19 @@ export function registerDeploy(program: Command): void {
46
51
  }) => {
47
52
  const cwd = process.cwd()
48
53
  const config = loadConfig(cwd)
54
+ const link = loadProjectLink(cwd)
49
55
  const cloudCfg = loadCloudConfig(cwd)
56
+ const envName = opts.env ?? opts.environment ?? "production"
50
57
 
51
58
  let schemaDone = false
52
59
 
53
- // Default: cloud — .supatype/cloud.json → control plane schema deploy
54
60
  if (
55
61
  !opts.local &&
56
- cloudCfg?.projectSlug &&
62
+ link &&
57
63
  !opts.appOnly &&
58
64
  !opts.skipBuild
59
65
  ) {
60
- await deploySchemaToLinkedProject(cwd, opts.environment ?? "production")
66
+ await deploySchemaToLinkedProject(cwd, envName)
61
67
  schemaDone = true
62
68
  if (opts.schemaOnly) {
63
69
  return
@@ -92,9 +98,9 @@ export function registerDeploy(program: Command): void {
92
98
  } else {
93
99
  console.log("Schema is up to date.")
94
100
  }
95
- } else if (cloudCfg?.projectSlug) {
96
- console.log("=== Schema Push (cloud) ===")
97
- await pushSchemaToLinkedProject(cwd, { force: opts.yes ?? true })
101
+ } else if (link) {
102
+ console.log("=== Schema Push (linked) ===")
103
+ await pushSchemaToLinkedProject(cwd, { force: opts.yes ?? true, env: envName })
98
104
  } else {
99
105
  console.error(
100
106
  "Not linked to Supatype Cloud. Run: supatype link\n" +
@@ -184,21 +190,21 @@ export function registerDeploy(program: Command): void {
184
190
  }
185
191
 
186
192
  // Deploy (--local never uploads to cloud, even if linked)
187
- if (cloudCfg?.projectSlug && !opts.local) {
188
- await deployToCloud(
189
- { projectRef: cloudCfg.projectSlug, apiUrl: cloudCfg.apiUrl, accessToken: cloudCfg.token },
190
- appConfig.outputDirectory,
191
- opts.preview ?? false,
192
- )
193
+ if (link && !opts.local) {
194
+ await deployStaticSite(cwd, appConfig.outputDirectory, {
195
+ preview: opts.preview ?? false,
196
+ env: envName,
197
+ })
193
198
  } else {
194
199
  deploySelfHost(appConfig.outputDirectory, cwd)
195
200
  }
196
201
 
197
202
  console.log("\nDeployment complete!")
198
- if (cloudCfg?.projectSlug && !opts.local) {
203
+ if (link && !opts.local) {
204
+ const target = resolveTarget(cwd, { env: envName })
199
205
  const url = opts.preview
200
- ? `https://preview-${Date.now().toString(36)}.${cloudCfg.projectSlug}.supatype.dev`
201
- : `https://${cloudCfg.projectSlug}.supatype.dev`
206
+ ? `${target.apiBaseUrl}/preview`
207
+ : target.apiBaseUrl
202
208
  console.log(`URL: ${url}`)
203
209
  }
204
210
  }
@@ -208,123 +214,174 @@ export function registerDeploy(program: Command): void {
208
214
  deploy
209
215
  .command("rollback")
210
216
  .description("Roll back to the previous static site deployment")
211
- .action(async () => {
212
- const cloudCfg = loadCloudConfig(process.cwd())
213
- if (!cloudCfg?.projectSlug) {
214
- console.error("Not linked to a cloud project. Rollback is only available for cloud deployments.")
215
- process.exit(1)
216
- }
217
-
218
- const apiUrl = cloudCfg.apiUrl || "https://api.supatype.com"
219
- const token = cloudCfg.token || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
220
-
221
- const res = await fetch(`${apiUrl}/platform/v1/projects/${cloudCfg.projectSlug}/deployments/rollback`, {
222
- method: "POST",
223
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
224
- })
225
-
226
- if (!res.ok) {
227
- const body = await res.text()
228
- console.error(`Rollback failed: ${res.status} ${body}`)
217
+ .option("--env <name>", "Target environment when linked")
218
+ .option("--to <id>", "Roll back to a specific deployment id or version")
219
+ .action(async (opts: { env?: string; to?: string }) => {
220
+ const cwd = process.cwd()
221
+ const link = loadProjectLink(cwd)
222
+ if (!link) {
223
+ console.error("Not linked to a project. Rollback requires a linked target.")
229
224
  process.exit(1)
230
225
  }
231
226
 
232
- const { data } = await res.json() as { data: { version: string; message: string } }
233
- console.log(`Rolled back to deployment ${data.version}.`)
227
+ const target = resolveTarget(cwd, { env: opts.env })
228
+ const body = opts.to ? { to: opts.to } : undefined
229
+ const data = await targetFetch<{ version: string; message: string }>(
230
+ target.apiBaseUrl,
231
+ target.apiPrefix,
232
+ {
233
+ method: "POST",
234
+ path: `/projects/${target.projectRef}/deployments/rollback`,
235
+ body,
236
+ token: target.token!,
237
+ orgId: target.orgId,
238
+ environment: target.mode === "cloud" ? target.environment : undefined,
239
+ },
240
+ )
241
+
242
+ console.log(`Rolled back to deployment ${data.version ?? "previous"}.`)
234
243
  })
235
244
 
236
245
  // supatype deploy status
237
246
  deploy
238
247
  .command("status")
239
248
  .description("Show current deployment status")
240
- .action(async () => {
241
- const cloudCfg = loadCloudConfig(process.cwd())
242
- if (!cloudCfg?.projectSlug) {
243
- console.error("Not linked to a cloud project.")
249
+ .option("--env <name>", "Target environment when linked")
250
+ .action(async (opts: { env?: string }) => {
251
+ const cwd = process.cwd()
252
+ const link = loadProjectLink(cwd)
253
+ if (!link) {
254
+ console.error("Not linked to a project.")
244
255
  process.exit(1)
245
256
  }
246
257
 
247
- const apiUrl = cloudCfg.apiUrl || "https://api.supatype.com"
248
- const token = cloudCfg.token || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
249
-
250
- const res = await fetch(`${apiUrl}/platform/v1/projects/${cloudCfg.projectSlug}/deployments/current`, {
251
- headers: { Authorization: `Bearer ${token}` },
252
- })
253
-
254
- if (!res.ok) {
255
- console.error("No active deployment found.")
258
+ const target = resolveTarget(cwd, { env: opts.env })
259
+ const data = await targetFetch<{
260
+ version?: string
261
+ id?: string
262
+ timestamp?: string
263
+ createdAt?: string
264
+ size?: number
265
+ buildDuration?: number
266
+ url?: string
267
+ status?: string
268
+ } | null>(
269
+ target.apiBaseUrl,
270
+ target.apiPrefix,
271
+ {
272
+ method: "GET",
273
+ path: `/projects/${target.projectRef}/deployments/current`,
274
+ token: target.token!,
275
+ orgId: target.orgId,
276
+ environment: target.mode === "cloud" ? target.environment : undefined,
277
+ },
278
+ )
279
+
280
+ if (!data) {
281
+ console.log("No active deployment found.")
256
282
  return
257
283
  }
258
284
 
259
- const { data } = await res.json() as { data: {
260
- version: string
261
- timestamp: string
262
- size: number
263
- buildDuration: number
264
- url: string
265
- status: string
266
- }}
267
-
268
- console.log(`Deployment: ${data.version}`)
269
- console.log(`Status: ${data.status}`)
270
- console.log(`Deployed: ${data.timestamp}`)
271
- console.log(`Size: ${(data.size / (1024 * 1024)).toFixed(1)}MB`)
272
- console.log(`Build duration: ${data.buildDuration}s`)
273
- console.log(`URL: ${data.url}`)
285
+ console.log(`Deployment: ${data.version ?? data.id ?? "unknown"}`)
286
+ console.log(`Status: ${data.status ?? "live"}`)
287
+ if (data.timestamp ?? data.createdAt) {
288
+ console.log(`Deployed: ${data.timestamp ?? data.createdAt}`)
289
+ }
290
+ if (data.size) console.log(`Size: ${(data.size / (1024 * 1024)).toFixed(1)}MB`)
291
+ if (data.buildDuration) console.log(`Build duration: ${data.buildDuration}s`)
292
+ if (data.url) console.log(`URL: ${data.url}`)
274
293
  })
275
294
 
276
295
  // supatype deploy logs <version>
277
296
  deploy
278
297
  .command("logs [version]")
279
298
  .description("Show build logs for a deployment")
280
- .action(async (version?: string) => {
281
- const cloudCfg = loadCloudConfig(process.cwd())
282
- if (!cloudCfg?.projectSlug) {
283
- console.error("Not linked to a cloud project.")
299
+ .option("--env <name>", "Target environment when linked")
300
+ .action(async (version: string | undefined, opts: { env?: string }) => {
301
+ const cwd = process.cwd()
302
+ const link = loadProjectLink(cwd)
303
+ if (!link) {
304
+ console.error("Not linked to a project.")
284
305
  process.exit(1)
285
306
  }
286
307
 
287
- const apiUrl = cloudCfg.apiUrl || "https://api.supatype.com"
288
- const token = cloudCfg.token || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
289
-
308
+ const target = resolveTarget(cwd, { env: opts.env })
290
309
  const versionPath = version ? `/${version}` : "/current"
291
- const res = await fetch(`${apiUrl}/platform/v1/projects/${cloudCfg.projectSlug}/deployments${versionPath}/logs`, {
292
- headers: { Authorization: `Bearer ${token}` },
293
- })
294
-
295
- if (!res.ok) {
296
- console.error("Logs not found.")
297
- process.exit(1)
298
- }
299
-
300
- const { data } = await res.json() as { data: { logs: string } }
301
- console.log(data.logs)
310
+ const data = await targetFetch<{ logs: string }>(
311
+ target.apiBaseUrl,
312
+ target.apiPrefix,
313
+ {
314
+ method: "GET",
315
+ path: `/projects/${target.projectRef}/deployments${versionPath}/logs`,
316
+ token: target.token!,
317
+ orgId: target.orgId,
318
+ environment: target.mode === "cloud" ? target.environment : undefined,
319
+ },
320
+ )
321
+
322
+ console.log(data.logs ?? "(no logs)")
302
323
  })
303
324
  }
304
325
 
305
- async function deployToCloud(
306
- config: { projectRef?: string; apiUrl?: string; accessToken?: string },
326
+ async function deployStaticSite(
327
+ cwd: string,
307
328
  outputDir: string,
308
- isPreview: boolean,
329
+ opts: { preview?: boolean; env?: string },
309
330
  ): Promise<void> {
310
- const apiUrl = config.apiUrl || "https://api.supatype.com"
311
- const token = config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
331
+ const target = resolveTarget(cwd, { env: opts.env })
332
+ const token = target.token
333
+ if (!token) {
334
+ throw new Error("No token for linked target. Re-run supatype link --token ...")
335
+ }
312
336
 
313
337
  console.log("Uploading build artifacts...")
314
-
315
- // Collect files from output directory
316
338
  const files = collectFiles(outputDir, outputDir)
317
339
  console.log(`${files.length} files to upload (${formatSize(files.reduce((s, f) => s + f.size, 0))})`)
318
340
 
319
- // Create deployment
320
- const createRes = await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments`, {
341
+ const { readFileSync } = await import("node:fs")
342
+
343
+ if (target.apiPrefix === "/platform/v1") {
344
+ const filePayload = files.map((f) => ({
345
+ path: f.relativePath,
346
+ content: readFileSync(f.absolutePath).toString("base64"),
347
+ encoding: "base64",
348
+ }))
349
+
350
+ const created = await targetFetch<{ id: string }>(
351
+ target.apiBaseUrl,
352
+ target.apiPrefix,
353
+ {
354
+ method: "POST",
355
+ path: `/projects/${target.projectRef}/deployments`,
356
+ body: { preview: opts.preview ?? false, files: filePayload },
357
+ token,
358
+ orgId: target.orgId,
359
+ },
360
+ )
361
+
362
+ await targetFetch(
363
+ target.apiBaseUrl,
364
+ target.apiPrefix,
365
+ {
366
+ method: "POST",
367
+ path: `/projects/${target.projectRef}/deployments/${created.id}/finalize`,
368
+ token,
369
+ orgId: target.orgId,
370
+ },
371
+ )
372
+ return
373
+ }
374
+
375
+ const createRes = await fetch(`${target.apiBaseUrl}/api/v1/projects/${target.projectRef}/deployments`, {
321
376
  method: "POST",
322
377
  headers: {
323
378
  Authorization: `Bearer ${token}`,
324
379
  "Content-Type": "application/json",
380
+ ...(target.orgId ? { "X-Org-Id": target.orgId } : {}),
381
+ ...(target.environment ? { "X-Supatype-Environment": target.environment } : {}),
325
382
  },
326
383
  body: JSON.stringify({
327
- preview: isPreview,
384
+ preview: opts.preview ?? false,
328
385
  fileCount: files.length,
329
386
  totalSize: files.reduce((s, f) => s + f.size, 0),
330
387
  files: files.map((f) => ({ path: f.relativePath, size: f.size })),
@@ -338,9 +395,8 @@ async function deployToCloud(
338
395
 
339
396
  const { data } = await createRes.json() as { data: { deploymentId: string; uploadUrl: string } }
340
397
 
341
- // Upload files (simplified — in production, use multipart or presigned URLs)
342
398
  for (const file of files) {
343
- const content = require("node:fs").readFileSync(file.absolutePath)
399
+ const content = readFileSync(file.absolutePath)
344
400
  await fetch(`${data.uploadUrl}/${file.relativePath}`, {
345
401
  method: "PUT",
346
402
  headers: {
@@ -352,16 +408,20 @@ async function deployToCloud(
352
408
  })
353
409
  }
354
410
 
355
- // Finalize deployment
356
- await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments/${data.deploymentId}/finalize`, {
357
- method: "POST",
358
- headers: { Authorization: `Bearer ${token}` },
359
- })
411
+ await fetch(
412
+ `${target.apiBaseUrl}/api/v1/projects/${target.projectRef}/deployments/${data.deploymentId}/finalize`,
413
+ {
414
+ method: "POST",
415
+ headers: {
416
+ Authorization: `Bearer ${token}`,
417
+ ...(target.orgId ? { "X-Org-Id": target.orgId } : {}),
418
+ },
419
+ },
420
+ )
360
421
  }
361
422
 
362
423
  function deploySelfHost(outputDir: string, cwd: string): void {
363
424
  const servingDir = join(cwd, ".supatype", "static")
364
- const { mkdirSync, cpSync } = require("node:fs") as typeof import("node:fs")
365
425
  mkdirSync(servingDir, { recursive: true })
366
426
  cpSync(outputDir, servingDir, { recursive: true })
367
427
  console.log(`Static files deployed to ${servingDir}`)
@@ -375,8 +435,6 @@ interface FileEntry {
375
435
 
376
436
  function collectFiles(dir: string, baseDir: string): FileEntry[] {
377
437
  const files: FileEntry[] = []
378
- const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs")
379
- const { relative } = require("node:path") as typeof import("node:path")
380
438
 
381
439
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
382
440
  const fullPath = join(dir, entry.name)
@@ -16,6 +16,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
16
16
  import { homedir } from "node:os"
17
17
  import { isAbsolute, join, relative, resolve } from "node:path"
18
18
  import { loadConfig } from "../config.js"
19
+ import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
19
20
  import {
20
21
  functionsPathCandidatesFromProject,
21
22
  resolveRuntimeProvider,
@@ -572,14 +573,27 @@ async function runSchemaPush(
572
573
 
573
574
  // Push schema.
574
575
  console.log("[supatype] Applying schema...")
575
- const pushResult = spawnSync(
576
- engineBin,
577
- ["push", "-i", astPath, "--database-url", dbURL, "--force", "--non-interactive"],
578
- { cwd, stdio: "inherit", encoding: "utf8" },
579
- )
580
- if (pushResult.status !== 0) {
576
+ const { ensureEngine, engineRequest } = await import("../engine-client.js")
577
+ const { writeSchemaSourcePushArtifacts } = await import("../schema-sources.js")
578
+ await ensureEngine()
579
+ const pgSchema = config?.schema?.pg_schema ?? "public"
580
+ const sources = writeSchemaSourcePushArtifacts(cwd)
581
+ try {
582
+ await engineRequest("/push", {
583
+ ast,
584
+ database_url: dbURL,
585
+ schema: pgSchema,
586
+ force: true,
587
+ ...(sources
588
+ ? {
589
+ schema_sources_gz_base64: sources.payload.dataBase64,
590
+ schema_sources_manifest: sources.payload.manifest,
591
+ }
592
+ : {}),
593
+ })
594
+ } catch (err) {
581
595
  _lastFailedAst = astJson
582
- throw new Error(`Engine schema push failed (exit ${pushResult.status})`)
596
+ throw err
583
597
  }
584
598
  _lastPushedAst = astJson
585
599
  _lastFailedAst = null
@@ -919,9 +933,9 @@ const EXTENSION_FIELDS: Record<string, { ext: string; fallback: AstField }> = {
919
933
  }
920
934
 
921
935
  function adaptUnsupportedKinds(
922
- ast: unknown,
936
+ ast: ExtractedSchemaAstV2,
923
937
  skipKinds: ReadonlySet<string>,
924
- ): { filtered: unknown; adapted: string[] } {
938
+ ): { filtered: ExtractedSchemaAstV2; adapted: string[] } {
925
939
  const adapted: string[] = []
926
940
  if (!ast || typeof ast !== "object") return { filtered: ast, adapted }
927
941
  const schema = ast as AstSchema
@@ -941,7 +955,7 @@ function adaptUnsupportedKinds(
941
955
  return { ...model, fields }
942
956
  })
943
957
 
944
- return { filtered: { ...schema, models }, adapted }
958
+ return { filtered: { ...schema, models } as ExtractedSchemaAstV2, adapted }
945
959
  }
946
960
 
947
961
  // ---------------------------------------------------------------------------
@@ -1,63 +1,62 @@
1
1
  import type { Command } from "commander"
2
2
  import { loadConfig, loadSchemaAst } from "../config.js"
3
- import { connectionString, schemaPathFromProject } from "../project-config.js"
4
- import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
5
- import { printDiffWarnings } from "../diff-output.js"
3
+ import { resolveRuntimeProvider, schemaPathFromProject } from "../project-config.js"
4
+ import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
5
+ import { resolveTarget, targetSchemaDiff, schemaPgSchema } from "../resolve-target.js"
6
+ import { loadProjectLink } from "../link.js"
6
7
 
7
8
  export function registerDiff(program: Command): void {
8
9
  program
9
10
  .command("diff")
10
11
  .description("Show planned schema changes without applying them (dry run)")
11
12
  .option("--connection <url>", "Database connection URL (overrides config)")
12
- .action(async (opts: { connection?: string }) => {
13
+ .option("--env <name>", "Target environment when linked")
14
+ .option("--direct", "Use local engine subprocess")
15
+ .action(async (opts: { connection?: string; env?: string; direct?: boolean }) => {
13
16
  const cwd = process.cwd()
14
17
  const config = loadConfig(cwd)
15
- const connection = opts.connection ?? connectionString(config)
16
-
17
- await ensureEngine()
18
+ const pgSchema = schemaPgSchema(cwd)
18
19
 
19
20
  console.log("Loading schema...")
20
21
  const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
21
22
 
22
- const diff = await engineRequest<DiffResult>("/diff", {
23
- ast,
24
- database_url: connection,
25
- schema: "public",
26
- })
27
-
28
- const ops = diff.operations ?? []
29
- printDiffWarnings(diff)
23
+ const linked = loadProjectLink(cwd)
24
+ const useDirect = opts.direct || Boolean(opts.connection)
30
25
 
31
- if (ops.length === 0) {
32
- console.log("No changes.")
26
+ if (linked && !useDirect && !opts.connection) {
27
+ const target = resolveTarget(cwd, { env: opts.env })
28
+ const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
29
+ printDiffWarnings(diff)
30
+ printDiffOperations(diff)
33
31
  return
34
32
  }
35
33
 
36
- const symbol: Record<NonNullable<DiffResult["operations"][number]["risk"]>, string> = {
37
- safe: "+",
38
- warn: "~",
39
- cautious: "~",
40
- danger: "!",
41
- destructive: "!",
42
- }
43
- const legend: typeof symbol = {
44
- safe: "safe",
45
- warn: "caution",
46
- cautious: "caution",
47
- danger: "DANGER",
48
- destructive: "DANGER",
49
- }
50
-
51
- console.log(`\n${ops.length} change(s):\n`)
52
- for (const op of ops) {
53
- const r = op.risk ?? "safe"
54
- console.log(` [${symbol[r]}] ${op.description} (${legend[r]})`)
34
+ if (
35
+ !opts.connection &&
36
+ !useDirect &&
37
+ resolveRuntimeProvider(config) === "docker"
38
+ ) {
39
+ const localTarget = resolveTarget(cwd, { env: opts.env })
40
+ if (localTarget.mode === "local" && localTarget.token) {
41
+ const diff = await targetSchemaDiff(localTarget, ast, { schema: pgSchema })
42
+ printDiffWarnings(diff)
43
+ printDiffOperations(diff)
44
+ return
45
+ }
46
+ const { diffSchemaDocker } = await import("../dev-compose.js")
47
+ const diff = await diffSchemaDocker(cwd, config)
48
+ printDiffWarnings(diff)
49
+ printDiffOperations(diff)
50
+ return
55
51
  }
56
52
 
57
- const dangerous = ops.filter((o) => o.risk === "danger").length
58
- if (dangerous > 0) {
59
- console.log(`\n ${dangerous} dangerous operation(s). Review before pushing.`)
60
- }
61
- console.log()
53
+ const target = resolveTarget(cwd, {
54
+ env: opts.env,
55
+ direct: true,
56
+ connection: opts.connection,
57
+ })
58
+ const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
59
+ printDiffWarnings(diff)
60
+ printDiffOperations(diff)
62
61
  })
63
62
  }
@@ -0,0 +1,102 @@
1
+ import type { Command } from "commander"
2
+ import { loadConfig, loadSchemaAst } from "../config.js"
3
+ import { schemaPathFromProject } from "../project-config.js"
4
+ import { resolveTarget, targetSchemaDoctor, schemaPgSchema } from "../resolve-target.js"
5
+ import { loadProjectLink } from "../link.js"
6
+ import { resolveHostEngineDatabaseUrl } from "../dev-compose.js"
7
+
8
+ interface DoctorItem {
9
+ kind: string
10
+ table: string
11
+ name: string
12
+ fields: string[]
13
+ message: string
14
+ }
15
+
16
+ interface DoctorReport {
17
+ missing: DoctorItem[]
18
+ staleManaged: DoctorItem[]
19
+ unmanagedDrift: DoctorItem[]
20
+ }
21
+
22
+ function printSection(title: string, items: DoctorItem[]): void {
23
+ if (items.length === 0) return
24
+ console.log(`\n${title} (${items.length}):\n`)
25
+ for (const item of items) {
26
+ const fields = item.fields.length > 0 ? ` (${item.fields.join(", ")})` : ""
27
+ console.log(` • ${item.table}.${item.name}${fields}`)
28
+ console.log(` ${item.message}`)
29
+ }
30
+ }
31
+
32
+ export function registerDoctor(program: Command): void {
33
+ program
34
+ .command("doctor")
35
+ .description("Report schema drift between schema/index.ts and the live database")
36
+ .option("--connection <url>", "Database connection URL (overrides config)")
37
+ .option("--env <name>", "Target environment when linked")
38
+ .option("--strict", "Exit non-zero when missing or stale managed drift exists")
39
+ .option("--no-cache", "Force full database introspection")
40
+ .option("--direct", "Use local engine subprocess")
41
+ .action(async (opts: {
42
+ connection?: string
43
+ env?: string
44
+ strict?: boolean
45
+ noCache?: boolean
46
+ direct?: boolean
47
+ }) => {
48
+ const cwd = process.cwd()
49
+ const config = loadConfig(cwd)
50
+ const pgSchema = schemaPgSchema(cwd)
51
+
52
+ console.log("Loading schema...")
53
+ const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
54
+
55
+ let report: DoctorReport
56
+
57
+ const linked = loadProjectLink(cwd)
58
+ if (linked && !opts.direct && !opts.connection) {
59
+ const target = resolveTarget(cwd, { env: opts.env })
60
+ report = (await targetSchemaDoctor(target, ast, {
61
+ noCache: opts.noCache,
62
+ schema: pgSchema,
63
+ })) as DoctorReport
64
+ } else if (!opts.direct && !opts.connection) {
65
+ const connection = await resolveHostEngineDatabaseUrl(cwd, config, opts.connection)
66
+ const target = resolveTarget(cwd, { direct: true, connection })
67
+ report = (await targetSchemaDoctor(target, ast, {
68
+ noCache: opts.noCache,
69
+ schema: pgSchema,
70
+ })) as DoctorReport
71
+ void connection
72
+ } else {
73
+ const target = resolveTarget(cwd, {
74
+ env: opts.env,
75
+ direct: true,
76
+ connection: opts.connection,
77
+ })
78
+ report = (await targetSchemaDoctor(target, ast, {
79
+ noCache: opts.noCache,
80
+ schema: pgSchema,
81
+ })) as DoctorReport
82
+ }
83
+
84
+ printSection("Missing (in AST, not in DB)", report.missing ?? [])
85
+ printSection("Stale managed (stamped, not in AST)", report.staleManaged ?? [])
86
+ printSection("Unmanaged drift (manual decision)", report.unmanagedDrift ?? [])
87
+
88
+ const missing = report.missing?.length ?? 0
89
+ const stale = report.staleManaged?.length ?? 0
90
+ const unmanaged = report.unmanagedDrift?.length ?? 0
91
+
92
+ if (missing + stale + unmanaged === 0) {
93
+ console.log("\nNo drift detected.")
94
+ } else {
95
+ console.log(`\nSummary: ${missing} missing, ${stale} stale managed, ${unmanaged} unmanaged`)
96
+ }
97
+
98
+ if (opts.strict && (missing > 0 || stale > 0)) {
99
+ process.exit(1)
100
+ }
101
+ })
102
+ }