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

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 +104 -71
  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,11 +1,72 @@
1
1
  import type { Command } from "commander"
2
2
  import { createInterface } from "node:readline"
3
+ import { join } from "node:path"
3
4
  import { loadConfig, loadSchemaAst } from "../config.js"
4
- import { connectionString, schemaPathFromProject } from "../project-config.js"
5
+ import { connectionString, projectRootFromConfig, schemaPathFromProject } from "../project-config.js"
5
6
  import { ensureEngine, engineRequest } from "../engine-client.js"
7
+ import { loadProjectLink } from "../link.js"
8
+ import {
9
+ resolveTarget,
10
+ targetSchemaDiff,
11
+ targetSchemaRollback,
12
+ targetListMigrations,
13
+ schemaPgSchema,
14
+ type SchemaRollbackResult,
15
+ } from "../resolve-target.js"
16
+ import {
17
+ restoreSchemaSourcesFromGz,
18
+ findOrphanSchemaFiles,
19
+ type SchemaSourcesManifest,
20
+ } from "../schema-sources.js"
6
21
 
7
22
  export function registerMigrate(program: Command): void {
8
- // migrate apply all pending migrations
23
+ const migrations = program
24
+ .command("migrations")
25
+ .description("Migration history utilities")
26
+
27
+ migrations
28
+ .command("list")
29
+ .description("List applied migrations with schema snapshot metadata")
30
+ .option("--connection <url>", "Database connection URL (overrides config)")
31
+ .option("--env <name>", "Target environment when linked")
32
+ .option("--direct", "Use local engine subprocess (skip control plane)")
33
+ .action(async (opts: { connection?: string; env?: string; direct?: boolean }) => {
34
+ const cwd = process.cwd()
35
+ const link = loadProjectLink(cwd)
36
+ const useDirect = opts.direct || Boolean(opts.connection)
37
+
38
+ let target
39
+ if (link && !useDirect && !opts.connection) {
40
+ target = resolveTarget(cwd, { env: opts.env })
41
+ } else {
42
+ target = resolveTarget(cwd, {
43
+ env: opts.env,
44
+ direct: true,
45
+ connection: opts.connection,
46
+ })
47
+ }
48
+
49
+ const list = await targetListMigrations(target)
50
+ if (list.length === 0) {
51
+ console.log("No migrations applied.")
52
+ return
53
+ }
54
+
55
+ for (const m of list) {
56
+ const manifest = m.schemaSourcesManifest
57
+ const size =
58
+ manifest?.compressedBytes !== undefined
59
+ ? `${(manifest.compressedBytes / 1024).toFixed(1)} KB`
60
+ : "—"
61
+ const files = manifest?.fileCount ?? "—"
62
+ const author = manifest?.pushedBy ?? "—"
63
+ const rolled = m.rolledBack ? " (rolled back)" : ""
64
+ console.log(
65
+ `${m.name}${rolled}\n applied: ${m.appliedAt} status: ${m.status}\n author: ${author} files: ${files} snapshot: ${size}`,
66
+ )
67
+ }
68
+ })
69
+
9
70
  program
10
71
  .command("migrate")
11
72
  .description("Apply pending migrations from the migration history")
@@ -23,25 +84,46 @@ export function registerMigrate(program: Command): void {
23
84
  console.log(result.message ?? "Migrations applied.")
24
85
  })
25
86
 
26
- // rollback — undo the last applied migration
27
87
  program
28
88
  .command("rollback")
29
89
  .description("Roll back the last applied migration")
30
90
  .option("--connection <url>", "Database connection URL (overrides config)")
31
- .action(async (opts: { connection?: string }) => {
32
- const config = loadConfig()
33
- const connection = opts.connection ?? connectionString(config)
91
+ .option("--env <name>", "Target environment when linked")
92
+ .option("--direct", "Use local engine subprocess (skip control plane)")
93
+ .option("--sync-schema", "Restore schema source files from DB snapshot without prompting")
94
+ .option("--no-sync-schema", "Revert database only; do not restore schema files")
95
+ .action(async (opts: {
96
+ connection?: string
97
+ env?: string
98
+ direct?: boolean
99
+ syncSchema?: boolean
100
+ noSyncSchema?: boolean
101
+ }) => {
102
+ const cwd = process.cwd()
103
+ const config = loadConfig(cwd)
104
+ const pgSchema = schemaPgSchema(cwd)
105
+ const link = loadProjectLink(cwd)
106
+ const useDirect = opts.direct || Boolean(opts.connection)
34
107
 
35
- await ensureEngine()
36
- const result = await engineRequest<{ message?: string }>("/migrations", {
37
- database_url: connection,
38
- schema: "public",
39
- action: "rollback",
40
- })
108
+ let target
109
+ if (link && !useDirect && !opts.connection) {
110
+ target = resolveTarget(cwd, { env: opts.env })
111
+ } else {
112
+ target = resolveTarget(cwd, {
113
+ env: opts.env,
114
+ direct: true,
115
+ connection: opts.connection,
116
+ })
117
+ }
118
+
119
+ const result = await targetSchemaRollback(target, { schema: pgSchema })
41
120
  console.log(result.message ?? "Rolled back.")
121
+
122
+ if (!opts.noSyncSchema) {
123
+ await offerSchemaRestore(cwd, config, target, result, pgSchema, opts.syncSchema ?? false)
124
+ }
42
125
  })
43
126
 
44
- // reset — drop all tables and re-apply from scratch
45
127
  program
46
128
  .command("reset")
47
129
  .description(
@@ -76,12 +158,78 @@ export function registerMigrate(program: Command): void {
76
158
  })
77
159
  }
78
160
 
161
+ async function offerSchemaRestore(
162
+ cwd: string,
163
+ config: ReturnType<typeof loadConfig>,
164
+ target: ReturnType<typeof resolveTarget>,
165
+ result: SchemaRollbackResult,
166
+ pgSchema: string,
167
+ autoSync: boolean,
168
+ ): Promise<void> {
169
+ const manifest = result.schemaSourcesManifest as SchemaSourcesManifest | undefined
170
+ const gzB64 = result.schemaSourcesBase64
171
+
172
+ if (!gzB64 || !manifest) {
173
+ console.warn(
174
+ "No schema source snapshot on the restored migration (legacy push). Run `supatype pull` to draft from DB if needed.",
175
+ )
176
+ return
177
+ }
178
+
179
+ const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
180
+ const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
181
+ const drift = (diff.operations ?? []).length > 0
182
+
183
+ if (!drift && !autoSync) {
184
+ console.log("Schema files match reverted database (no restore needed).")
185
+ return
186
+ }
187
+
188
+ const fileList = manifest.files.map((f) => f.path).join(", ")
189
+ const sizeKb = (manifest.compressedBytes / 1024).toFixed(1)
190
+ const label = result.restoredMigrationName ?? result.name
191
+
192
+ let proceed = autoSync
193
+ if (!proceed) {
194
+ proceed = await confirm(
195
+ `\nRolled back migration ${result.name}.\nRestore ${manifest.fileCount} schema files from database snapshot (${sizeKb} KB)?\n ${fileList}\n [Y/n] `,
196
+ )
197
+ }
198
+
199
+ if (!proceed) {
200
+ console.log("Skipped schema file restore. Run `supatype diff` to review drift.")
201
+ return
202
+ }
203
+
204
+ const root = projectRootFromConfig(config, cwd)
205
+ const backupDir = join(cwd, ".supatype", "schema-backups", `${Date.now()}`)
206
+ const gz = Buffer.from(gzB64, "base64")
207
+ restoreSchemaSourcesFromGz(gz, manifest, root, { backupDir })
208
+
209
+ const manifestPaths = new Set(manifest.files.map((f) => f.path))
210
+ const orphans = findOrphanSchemaFiles(root, manifest.entryPoint, manifestPaths)
211
+ for (const orphan of orphans) {
212
+ console.warn(`Warning: ${orphan} not in snapshot — review manually`)
213
+ }
214
+
215
+ console.log(`Restored schema files from migration ${label}.`)
216
+ console.log(`Backup saved to ${backupDir}`)
217
+
218
+ const postDiff = await targetSchemaDiff(target, ast, { schema: pgSchema })
219
+ if ((postDiff.operations ?? []).length === 0) {
220
+ console.log("Schema matches database after restore.")
221
+ } else {
222
+ console.log("Run `supatype diff` — schema may still differ from database.")
223
+ }
224
+ }
225
+
79
226
  async function confirm(prompt: string): Promise<boolean> {
80
227
  const rl = createInterface({ input: process.stdin, output: process.stdout })
81
- return new Promise((resolve) => {
228
+ return new Promise((resolveConfirm) => {
82
229
  rl.question(prompt, (answer) => {
83
230
  rl.close()
84
- resolve(answer.toLowerCase() === "y")
231
+ const trimmed = answer.trim().toLowerCase()
232
+ resolveConfirm(trimmed === "" || trimmed === "y" || trimmed === "yes")
85
233
  })
86
234
  })
87
235
  }
@@ -1,17 +1,45 @@
1
1
  import type { Command } from "commander"
2
- import { ensureEngine } from "../engine-client.js"
2
+ import { writeFileSync } from "node:fs"
3
+ import { loadConfig } from "../config.js"
4
+ import { schemaPathFromProject } from "../project-config.js"
5
+ import { ensureEngine, engineRequest } from "../engine-client.js"
6
+ import { resolveHostEngineDatabaseUrl } from "../dev-compose.js"
7
+ import { databaseStateToSchemaScaffold, type DatabaseStateJson } from "../pull-utils.js"
3
8
 
4
9
  export function registerPull(program: Command): void {
5
10
  program
6
11
  .command("pull")
7
- .description(
8
- "Introspect an existing Postgres database (deprecated in type-first mode)",
9
- )
10
- .action(async () => {
12
+ .description("Scaffold schema/index.ts from live database introspection (draft — review before push)")
13
+ .option("--connection <url>", "Database connection URL (overrides config)")
14
+ .option("--out <path>", "Write scaffold to file (default: stdout)")
15
+ .option("--dry-run", "Print scaffold to stdout without writing files")
16
+ .action(async (opts: { connection?: string; out?: string; dryRun?: boolean }) => {
17
+ const cwd = process.cwd()
18
+ const config = loadConfig(cwd)
19
+ const connection = await resolveHostEngineDatabaseUrl(cwd, config, opts.connection)
20
+ const pgSchema = config.schema?.pg_schema ?? "public"
21
+
11
22
  await ensureEngine()
12
- throw new Error(
13
- "The legacy `supatype pull` schema generator has been removed.\n" +
14
- "Use type-based models with @supatype/types and run `supatype generate`.",
15
- )
23
+
24
+ console.error("Introspecting database...")
25
+ const state = await engineRequest<DatabaseStateJson>("/introspect", {
26
+ database_url: connection,
27
+ schema: pgSchema,
28
+ })
29
+
30
+ const scaffold = databaseStateToSchemaScaffold(state)
31
+ const defaultOut = schemaPathFromProject(config, cwd)
32
+
33
+ if (opts.dryRun || !opts.out) {
34
+ console.log(scaffold)
35
+ if (!opts.dryRun && !opts.out) {
36
+ console.error("\n(draft printed to stdout — use --out to write a file)")
37
+ }
38
+ return
39
+ }
40
+
41
+ writeFileSync(opts.out ?? defaultOut, scaffold, "utf8")
42
+ console.log(`Wrote draft schema to ${opts.out ?? defaultOut}`)
43
+ console.log("Review access rules and relations, then run `supatype generate`.")
16
44
  })
17
45
  }
@@ -3,16 +3,29 @@ import { mkdirSync, writeFileSync } from "node:fs"
3
3
  import { createInterface } from "node:readline"
4
4
  import { join } from "node:path"
5
5
  import { loadConfig, loadSchemaAst } from "../config.js"
6
- import { connectionString, resolveRuntimeProvider, schemaPathFromProject, serverBaseUrl } from "../project-config.js"
7
- import { isCloudLinked, pushSchemaToLinkedProject } from "./cloud.js"
8
- import { ensureEngine, engineRequest, type DiffResult, type Operation } from "../engine-client.js"
9
- import { printDiffWarnings } from "../diff-output.js"
6
+ import { resolveRuntimeProvider, schemaPathFromProject, serverBaseUrl } from "../project-config.js"
7
+ import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
8
+ import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
10
9
  import { signJwt } from "../jwt.js"
11
- import { provisionBuckets } from "../storage-provision.js"
10
+ import { provisionBucketsFromAst } from "../storage-provision.js"
11
+ import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
12
12
  import { promptFirstAdminUser } from "./admin.js"
13
13
  import { withAdminRoles } from "../studio-admin-roles.js"
14
14
  import { restoreSystemRelationTargets } from "../restore-system-relation-targets.js"
15
15
  import type { SupatypeProjectConfig } from "../project-config.js"
16
+ import {
17
+ resolveTarget,
18
+ targetSchemaDiff,
19
+ targetSchemaPush,
20
+ schemaPgSchema,
21
+ type DeployTarget,
22
+ } from "../resolve-target.js"
23
+ import { loadProjectLink } from "../link.js"
24
+ import {
25
+ buildSchemaSourcesPayload,
26
+ cacheSchemaSourcesLocally,
27
+ resolvePushedBy,
28
+ } from "../schema-sources.js"
16
29
 
17
30
  const DEV_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
18
31
 
@@ -24,202 +37,155 @@ export function registerPush(program: Command): void {
24
37
  )
25
38
  .option("--yes", "Skip confirmation prompts for destructive changes")
26
39
  .option("--connection <url>", "Database connection URL (overrides config)")
27
- .action(async (opts: { yes?: boolean; connection?: string }) => {
40
+ .option("--env <name>", "Target environment when linked")
41
+ .option("--direct", "Use local engine subprocess (skip control plane)")
42
+ .option("--local", "Alias for --direct")
43
+ .action(async (opts: {
44
+ yes?: boolean
45
+ connection?: string
46
+ env?: string
47
+ direct?: boolean
48
+ local?: boolean
49
+ }) => {
28
50
  const cwd = process.cwd()
51
+ const config = loadConfig(cwd)
52
+ const pgSchema = schemaPgSchema(cwd)
53
+ const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
29
54
 
30
- if (isCloudLinked(cwd)) {
31
- if (opts.connection) {
32
- console.error("--connection is not allowed when linked to a cloud project (credentials stay server-side).")
33
- process.exit(1)
34
- }
35
- await pushSchemaToLinkedProject(cwd, { force: opts.yes ?? true })
55
+ const linked = loadProjectLink(cwd)
56
+ const useDirect = opts.direct || opts.local || Boolean(opts.connection)
57
+
58
+ if (linked && !useDirect && !opts.connection) {
59
+ const target = resolveTarget(cwd, { env: opts.env })
60
+ await pushViaTarget(cwd, config, target, ast, pgSchema, opts.yes ?? false)
36
61
  return
37
62
  }
38
63
 
39
- const config = loadConfig(cwd)
40
-
41
- // Docker provider: the compose Postgres isn't published to the host, so
42
- // apply the schema through the in-compose schema-engine (unless the user
43
- // gave an explicit --connection to a reachable database).
44
- if (!opts.connection && resolveRuntimeProvider(config) === "docker") {
64
+ if (!opts.connection && !useDirect && resolveRuntimeProvider(config) === "docker") {
65
+ const localTarget = resolveTarget(cwd, { env: opts.env })
66
+ if (localTarget.mode === "local" && localTarget.token) {
67
+ await pushViaTarget(cwd, config, localTarget, ast, pgSchema, opts.yes ?? false)
68
+ return
69
+ }
45
70
  const { pushSchemaDocker } = await import("../dev-compose.js")
46
71
  await pushSchemaDocker(cwd, config)
47
72
  return
48
73
  }
49
74
 
50
- const connection = opts.connection ?? connectionString(config)
51
-
52
- await ensureEngine()
53
-
54
- console.log("Loading schema...")
55
- const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
56
-
57
- console.log("Diffing against database...")
58
- const diff = await engineRequest<DiffResult>("/diff", {
59
- ast,
60
- database_url: connection,
61
- schema: "public",
75
+ const target = resolveTarget(cwd, {
76
+ env: opts.env,
77
+ direct: true,
78
+ connection: opts.connection,
62
79
  })
63
-
64
- const ops = diff.operations ?? []
65
- printDiffWarnings(diff)
66
-
67
- if (ops.length === 0) {
68
- console.log(
69
- "Schema matches the database (no DDL). Syncing Studio metadata...",
70
- )
71
- } else {
72
- printDiff(ops)
73
-
74
- const risky = ops.filter(
75
- (o) => o.risk === "cautious" || o.risk === "destructive" || o.risk === "warn" || o.risk === "danger",
76
- )
77
- if (risky.length > 0 && !opts.yes) {
78
- const confirmed = await confirm(
79
- `\n${risky.length} risky operation(s) above (type changes or data loss). Proceed? [y/N] `,
80
- )
81
- if (!confirmed) {
82
- console.log("Aborted.")
83
- return
84
- }
85
- }
86
- }
87
-
88
- console.log(ops.length > 0 ? "\nApplying migration..." : "\nSyncing with engine...")
89
- const pushResult = await engineRequest<{
90
- message?: string
91
- status?: string
92
- admin_refreshed?: boolean
93
- }>("/push", {
94
- ast,
95
- database_url: connection,
96
- schema: "public",
97
- force: true,
98
- })
99
- if (pushResult.status === "up_to_date") {
100
- console.log(
101
- pushResult.admin_refreshed
102
- ? "Database schema unchanged — Studio metadata synced."
103
- : "Schema is up to date.",
104
- )
105
- } else {
106
- console.log(pushResult.message ?? "Migration applied.")
107
- }
108
-
109
- await writeLocalAdminConfig(ast, config)
110
-
111
- // After a DDL migration, check if this is the first push and offer to create an
112
- // admin user if none exist (Gap Appendices task 48).
113
- if (ops.length > 0) {
114
- await promptFirstAdminUser(connection)
115
- }
116
-
117
- // Provision storage buckets declared in the schema.
118
- const baseUrl = serverBaseUrl(config)
119
- const serviceRoleKey =
120
- process.env["SUPATYPE_SERVICE_ROLE_KEY"] ??
121
- (config.server.mode === "dev"
122
- ? signJwt({ role: "service_role", iss: "supatype", iat: Math.floor(Date.now() / 1000) }, DEV_JWT_SECRET)
123
- : undefined)
124
-
125
- if (baseUrl && serviceRoleKey) {
126
- const parsedAst = await engineRequest<{
127
- storageBuckets?: Array<{
128
- id: string
129
- public: boolean
130
- accessMode?: "public" | "private" | "custom"
131
- allowedMimeTypes?: string[]
132
- fileSizeLimit?: number
133
- s3BucketPolicy?: string
134
- }>
135
- }>("/parse", { ast })
136
- const buckets = (parsedAst.storageBuckets ?? []).map((b) => ({
137
- id: b.id,
138
- public: b.public,
139
- ...(b.accessMode !== undefined && { access_mode: b.accessMode }),
140
- ...(b.allowedMimeTypes != null && { allowed_mime_types: b.allowedMimeTypes }),
141
- ...(b.fileSizeLimit != null && { file_size_limit: b.fileSizeLimit }),
142
- ...(b.s3BucketPolicy != null &&
143
- b.s3BucketPolicy !== "" && { s3_bucket_policy: b.s3BucketPolicy }),
144
- }))
145
- if (buckets.length > 0) {
146
- console.log("Provisioning storage buckets...")
147
- await provisionBuckets(`${baseUrl}/storage/v1`, serviceRoleKey, buckets)
148
- }
149
- }
150
-
151
- if (config.output?.types ?? config.output?.client) {
152
- console.log("Generating types...")
153
- const genBody: Record<string, unknown> = { ast, lang: "typescript" }
154
- if (config.output?.types) genBody["types_path"] = config.output.types
155
- if (config.output?.client) genBody["client_path"] = config.output.client
156
-
157
- const genResult = await engineRequest<{ code?: string; message?: string }>("/generate", genBody)
158
- console.log(genResult.message ?? "Types generated.")
159
- }
160
-
161
- const studioBase = baseUrl?.replace(/\/$/, "") ?? ""
162
- if (studioBase) {
163
- console.log(`\nStudio: ${studioBase}/studio/ — sign in with the admin user you created.`)
164
- } else {
165
- console.log("\nDone.")
166
- }
80
+ await pushViaTarget(cwd, config, target, ast, pgSchema, opts.yes ?? false)
167
81
  })
168
82
  }
169
83
 
170
- function printDiff(ops: Operation[]): void {
171
- const symbol: Record<string, string> = {
172
- safe: "+",
173
- warn: "~",
174
- cautious: "~",
175
- danger: "!",
176
- destructive: "!",
177
- }
178
- console.log(`\n${ops.length} change(s) planned:\n`)
179
- for (const op of ops) {
180
- const riskKey = op.risk ?? "safe"
181
- const s = symbol[riskKey] ?? "?"
182
- const label = op.warning ?? op.description ?? formatOperation(op)
183
- console.log(` [${s}] ${label}`)
84
+ async function pushViaTarget(
85
+ cwd: string,
86
+ config: SupatypeProjectConfig,
87
+ target: DeployTarget,
88
+ ast: unknown,
89
+ pgSchema: string,
90
+ skipConfirm: boolean,
91
+ ): Promise<void> {
92
+ console.log("Diffing against database...")
93
+ const diff = await targetSchemaDiff(target, ast, { schema: pgSchema })
94
+ const ops = diff.operations ?? []
95
+ printDiffWarnings(diff)
96
+
97
+ if (ops.length === 0) {
98
+ console.log("Schema matches the database (no DDL). Syncing Studio metadata...")
99
+ } else {
100
+ printDiffOperations({ operations: ops })
101
+ const risky = ops.filter(
102
+ (o) => o.risk === "cautious" || o.risk === "destructive" || o.risk === "warn" || o.risk === "danger",
103
+ )
104
+ if (risky.length > 0 && !skipConfirm) {
105
+ const confirmed = await confirm(
106
+ `\n${risky.length} risky operation(s) above. Proceed? [y/N] `,
107
+ )
108
+ if (!confirmed) {
109
+ console.log("Aborted.")
110
+ return
111
+ }
112
+ }
184
113
  }
185
- }
186
114
 
187
- function formatOperation(op: Operation): string {
188
- if (typeof op.description === "string" && op.description.trim().length > 0) {
189
- return op.description
115
+ console.log(ops.length > 0 ? "\nApplying migration..." : "\nSyncing with engine...")
116
+ const schemaSources = buildSchemaSourcesPayload(cwd, resolvePushedBy())
117
+ const pushResult = await targetSchemaPush(target, ast, {
118
+ force: true,
119
+ schema: pgSchema,
120
+ schemaSources,
121
+ })
122
+ if ((pushResult as { status?: string }).status === "up_to_date") {
123
+ console.log("Schema is up to date.")
124
+ } else {
125
+ console.log((pushResult as { message?: string }).message ?? "Migration applied.")
126
+ const migrationName = (pushResult as { name?: string }).name
127
+ if (migrationName && schemaSources) {
128
+ cacheSchemaSourcesLocally(cwd, migrationName, schemaSources.gz)
129
+ }
190
130
  }
191
131
 
192
- const kind = typeof op.kind === "string" ? op.kind : "operation"
193
- const raw = op as unknown as Record<string, unknown>
194
- const table = raw["table"]
195
- const column = raw["column"]
196
-
197
- if (typeof table === "string" && typeof column === "string") {
198
- return `${kind} ${table}.${column}`
132
+ if (target.mode === "direct" || target.mode === "local") {
133
+ await writeLocalAdminConfig(ast, config)
134
+ if (ops.length > 0 && target.databaseUrl) {
135
+ await promptFirstAdminUser(target.databaseUrl)
136
+ }
137
+ await generateTypesLocal(ast, config)
138
+ await provisionLocalStorage(ast, config)
139
+ } else {
140
+ console.log(`Pushed to ${target.mode} (${target.environment}).`)
199
141
  }
200
- if (typeof table === "string") {
201
- return `${kind} ${table}`
142
+
143
+ const baseUrl = (serverBaseUrl(config) ?? "").replace(/\/$/, "")
144
+ if (baseUrl) {
145
+ console.log(`\nStudio: ${baseUrl}/studio/`)
202
146
  }
147
+ }
148
+
149
+ async function generateTypesLocal(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
150
+ if (!config.output?.types && !config.output?.client) return
151
+ console.log("Generating types...")
152
+ await ensureEngine()
153
+ const genBody: Record<string, unknown> = { ast, lang: "typescript" }
154
+ if (config.output?.types) genBody["types_path"] = config.output.types
155
+ if (config.output?.client) genBody["client_path"] = config.output.client
156
+ const genResult = await engineRequest<{ message?: string }>("/generate", genBody)
157
+ console.log(genResult.message ?? "Types generated.")
158
+ }
203
159
 
204
- // Last resort: show operation kind with compact payload.
205
- return `${kind} ${JSON.stringify(op)}`
160
+ async function provisionLocalStorage(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
161
+ const baseUrl = serverBaseUrl(config)
162
+ const serviceRoleKey =
163
+ process.env["SUPATYPE_SERVICE_ROLE_KEY"] ??
164
+ process.env["SERVICE_ROLE_KEY"] ??
165
+ (config.server.mode === "dev"
166
+ ? signJwt({ role: "service_role", iss: "supatype", iat: Math.floor(Date.now() / 1000) }, DEV_JWT_SECRET)
167
+ : undefined)
168
+ if (!baseUrl || !serviceRoleKey) return
169
+ await ensureEngine()
170
+ const parsedAst = await engineRequest<Pick<ExtractedSchemaAstV2, "storageBuckets">>("/parse", { ast })
171
+ await provisionBucketsFromAst(parsedAst, `${baseUrl}/storage/v1`, serviceRoleKey)
206
172
  }
207
173
 
208
174
  async function confirm(prompt: string): Promise<boolean> {
209
175
  const rl = createInterface({ input: process.stdin, output: process.stdout })
210
- return new Promise((resolve) => {
176
+ return new Promise((resolveConfirm) => {
211
177
  rl.question(prompt, (answer) => {
212
178
  rl.close()
213
- resolve(answer.toLowerCase() === "y")
179
+ resolveConfirm(answer.toLowerCase() === "y")
214
180
  })
215
181
  })
216
182
  }
217
183
 
218
- /** Write `.supatype/admin-config.json` for local Studio (same layout as `supatype dev`). */
219
184
  async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
220
185
  const cwd = process.cwd()
221
186
  const dir = join(cwd, ".supatype")
222
187
  mkdirSync(dir, { recursive: true })
188
+ await ensureEngine()
223
189
  const admin = withAdminRoles(await engineRequest<unknown>("/admin", { ast }), config)
224
190
  restoreSystemRelationTargets(admin, ast)
225
191
  writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)