@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
@@ -20,14 +20,17 @@ import { resolveBinary, currentPlatform, cachePath } from "./binary-cache.js"
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
22
  export interface Operation {
23
- kind: "create_table" | "alter_table" | "drop_table" | "create_index" | "drop_index" |
24
- "create_policy" | "drop_policy" | "add_column" | "drop_column" | "alter_column" |
25
- "recreate_column"
26
23
  type?: string
24
+ kind?: string
27
25
  description?: string
28
26
  risk?: "safe" | "warn" | "danger" | "cautious" | "destructive"
29
27
  warning?: string
30
28
  sql?: string
29
+ table?: string
30
+ column?: string
31
+ constraint?: string
32
+ index_name?: string
33
+ index?: { fields?: string[]; name?: string; unique?: boolean }
31
34
  }
32
35
 
33
36
  export interface DiffResult {
@@ -146,24 +149,40 @@ export async function engineRequest<T = unknown>(
146
149
  ): Promise<T> {
147
150
  const bin = await getEngineBin()
148
151
 
149
- // Write request to a temp file.
150
- // For CLI-mode endpoints the engine reads the input file as a raw SchemaAst,
151
- // so we extract the `ast` field when present, otherwise write the full body.
152
152
  const tmpDir = join(tmpdir(), "supatype-engine")
153
153
  mkdirSync(tmpDir, { recursive: true })
154
+ const cleanup: string[] = []
154
155
  const reqFile = join(tmpDir, `req-${Date.now()}.json`)
155
156
  const inputPayload = body["ast"] !== undefined ? body["ast"] : body
156
157
  writeFileSync(reqFile, JSON.stringify(inputPayload))
158
+ cleanup.push(reqFile)
159
+
160
+ let gzPath: string | undefined
161
+ let manifestPath: string | undefined
162
+ if (typeof body["schema_sources_gz_base64"] === "string") {
163
+ gzPath = join(tmpDir, `sources-${Date.now()}.gz`)
164
+ writeFileSync(gzPath, Buffer.from(body["schema_sources_gz_base64"], "base64"))
165
+ cleanup.push(gzPath)
166
+ }
167
+ if (body["schema_sources_manifest"] !== undefined) {
168
+ manifestPath = join(tmpDir, `manifest-${Date.now()}.json`)
169
+ writeFileSync(manifestPath, JSON.stringify(body["schema_sources_manifest"]))
170
+ cleanup.push(manifestPath)
171
+ }
157
172
 
158
- const args = endpointToArgs(endpoint, body, reqFile)
173
+ const args = endpointToArgs(endpoint, body, reqFile, {
174
+ ...(gzPath !== undefined ? { gzPath } : {}),
175
+ ...(manifestPath !== undefined ? { manifestPath } : {}),
176
+ })
159
177
 
160
178
  const result = spawnSync(bin, args, {
161
179
  encoding: "utf8",
162
180
  cwd: process.cwd(),
163
181
  })
164
182
 
165
- // Clean up temp file.
166
- try { unlinkSync(reqFile) } catch { /* ignore */ }
183
+ for (const f of cleanup) {
184
+ try { unlinkSync(f) } catch { /* ignore */ }
185
+ }
167
186
 
168
187
  if (result.status !== 0) {
169
188
  const stderr = result.stderr?.trim() || "(no output)"
@@ -195,6 +214,7 @@ function endpointToArgs(
195
214
  endpoint: string,
196
215
  body: Record<string, unknown>,
197
216
  reqFile: string,
217
+ sources?: { gzPath?: string; manifestPath?: string },
198
218
  ): string[] {
199
219
  const dbUrl = (body["database_url"] as string | undefined) ?? ""
200
220
  const schema = (body["schema"] as string | undefined) ?? "public"
@@ -206,8 +226,26 @@ function endpointToArgs(
206
226
  case "/diff":
207
227
  return ["diff", "--input", reqFile, "--database-url", dbUrl, "--schema", schema]
208
228
 
209
- case "/push":
210
- return ["push", "--input", reqFile, "--database-url", dbUrl, "--schema", schema, ...force, ...nonInteractive]
229
+ case "/push": {
230
+ const sourceArgs: string[] = []
231
+ if (sources?.gzPath) sourceArgs.push("--schema-sources-gz", sources.gzPath)
232
+ if (sources?.manifestPath) sourceArgs.push("--schema-sources-manifest", sources.manifestPath)
233
+ return [
234
+ "push",
235
+ "--input",
236
+ reqFile,
237
+ "--database-url",
238
+ dbUrl,
239
+ "--schema",
240
+ schema,
241
+ ...force,
242
+ ...nonInteractive,
243
+ ...sourceArgs,
244
+ ]
245
+ }
246
+
247
+ case "/rollback":
248
+ return ["rollback", "--database-url", dbUrl, "--schema", schema]
211
249
 
212
250
  case "/parse":
213
251
  return ["parse", "--input", reqFile]
@@ -220,6 +258,18 @@ function endpointToArgs(
220
258
  case "/introspect":
221
259
  return ["introspect", "--database-url", dbUrl, "--schema", schema]
222
260
 
261
+ case "/doctor": {
262
+ const strict = body["strict"] ? ["--strict"] : []
263
+ const noCache = body["no_cache"] ? ["--no-cache"] : []
264
+ return ["doctor", "--input", reqFile, "--database-url", dbUrl, "--schema", schema, ...strict, ...noCache]
265
+ }
266
+
267
+ case "/adopt": {
268
+ const yes = body["yes"] ? ["--yes"] : []
269
+ const noCache = body["no_cache"] ? ["--no-cache"] : []
270
+ return ["adopt", "--input", reqFile, "--database-url", dbUrl, "--schema", schema, ...yes, ...noCache]
271
+ }
272
+
223
273
  case "/validate":
224
274
  return ["validate", "--input", reqFile]
225
275
 
@@ -227,9 +277,16 @@ function endpointToArgs(
227
277
  return ["admin", "--input", reqFile]
228
278
 
229
279
  default:
230
- if (endpoint.startsWith("/migrations")) {
280
+ if (endpoint === "/migrations" || endpoint.startsWith("/migrations")) {
231
281
  const action = (body["action"] as string | undefined) ?? "list"
232
- return ["migrations", action, "--database-url", dbUrl]
282
+ if (action === "rollback") {
283
+ return ["rollback", "--database-url", dbUrl, "--schema", schema]
284
+ }
285
+ const name = body["name"] as string | undefined
286
+ if (name) {
287
+ return ["migrations", "--database-url", dbUrl, "--name", name]
288
+ }
289
+ return ["migrations", "--database-url", dbUrl]
233
290
  }
234
291
  return [endpoint.replace(/^\//, ""), "--input", reqFile]
235
292
  }
@@ -10,7 +10,7 @@ export interface EnginePushResult {
10
10
  }
11
11
 
12
12
  /** Extract the engine JSON object from mixed docker compose stdout/stderr. */
13
- export function parseEnginePushOutput(output: string): EnginePushResult | null {
13
+ export function parseEngineJsonOutput<T>(output: string): T | null {
14
14
  const trimmed = output.trim()
15
15
  if (!trimmed) return null
16
16
 
@@ -18,7 +18,7 @@ export function parseEnginePushOutput(output: string): EnginePushResult | null {
18
18
  const candidate = line.trim()
19
19
  if (!candidate.startsWith("{") || !candidate.endsWith("}")) continue
20
20
  try {
21
- return JSON.parse(candidate) as EnginePushResult
21
+ return JSON.parse(candidate) as T
22
22
  } catch {
23
23
  /* try next line */
24
24
  }
@@ -26,7 +26,7 @@ export function parseEnginePushOutput(output: string): EnginePushResult | null {
26
26
 
27
27
  if (trimmed.startsWith("{")) {
28
28
  try {
29
- return JSON.parse(trimmed) as EnginePushResult
29
+ return JSON.parse(trimmed) as T
30
30
  } catch {
31
31
  return null
32
32
  }
@@ -35,6 +35,10 @@ export function parseEnginePushOutput(output: string): EnginePushResult | null {
35
35
  return null
36
36
  }
37
37
 
38
+ export function parseEnginePushOutput(output: string): EnginePushResult | null {
39
+ return parseEngineJsonOutput<EnginePushResult>(output)
40
+ }
41
+
38
42
  /** Human-readable one-liner for successful push (matches `supatype push` tone). */
39
43
  export function formatEnginePushMessage(result: EnginePushResult): string {
40
44
  if (result.status === "up_to_date") {
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ export const SUPATYPE_GITIGNORE_MARKER = "# Supatype — local runtime (contains secrets in link.json)"
5
+ export const SUPATYPE_GITIGNORE_BLOCK = `${SUPATYPE_GITIGNORE_MARKER}
6
+ .env
7
+ .supatype/
8
+ supatype.local.config.ts
9
+ supatype.local.config.js
10
+ supatype.local.config.mjs
11
+ `
12
+
13
+ export function isSupatypeGitignored(cwd: string): boolean {
14
+ const gitignorePath = resolve(cwd, ".gitignore")
15
+ if (!existsSync(gitignorePath)) return false
16
+ const content = readFileSync(gitignorePath, "utf8")
17
+ return /\.supatype\/?(\/|\s|$)/m.test(content) || content.includes(".supatype/")
18
+ }
19
+
20
+ export function ensureSupatypeGitignore(cwd: string, opts?: { silent?: boolean }): boolean {
21
+ const gitignorePath = resolve(cwd, ".gitignore")
22
+ if (isSupatypeGitignored(cwd)) return true
23
+
24
+ if (existsSync(gitignorePath)) {
25
+ const content = readFileSync(gitignorePath, "utf8")
26
+ const next = content.endsWith("\n") ? content : `${content}\n`
27
+ writeFileSync(gitignorePath, `${next}\n${SUPATYPE_GITIGNORE_BLOCK}`, "utf8")
28
+ } else {
29
+ writeFileSync(
30
+ gitignorePath,
31
+ `.env\nnode_modules/\ndist/\n${SUPATYPE_GITIGNORE_BLOCK}`,
32
+ "utf8",
33
+ )
34
+ }
35
+
36
+ if (!opts?.silent) {
37
+ console.warn("Added .supatype/ to .gitignore (link.json contains secrets — never commit).")
38
+ }
39
+ return true
40
+ }
41
+
42
+ export function warnIfLinkNotGitignored(cwd: string): void {
43
+ if (isSupatypeGitignored(cwd)) return
44
+ console.warn(
45
+ "\n⚠ Warning: .supatype/ is not in .gitignore — link.json contains tokens and must not be committed.",
46
+ )
47
+ console.warn(" Run with link --fix-gitignore to append the Supatype block.\n")
48
+ }
package/src/link.ts ADDED
@@ -0,0 +1,242 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ export const LINK_VERSION = 1 as const
5
+ export const LINK_FILE = ".supatype/link.json"
6
+ export const LEGACY_CLOUD_FILE = ".supatype/cloud.json"
7
+ export const LEGACY_LINKED_FILE = ".supatype/linked.json"
8
+
9
+ export type ProjectLinkKind = "cloud" | "self-host" | "local"
10
+
11
+ export interface EnvironmentTarget {
12
+ name: string
13
+ apiUrl: string
14
+ token?: string
15
+ linkedAt: string
16
+ }
17
+
18
+ export interface ProjectLink {
19
+ version: typeof LINK_VERSION
20
+ kind: ProjectLinkKind
21
+ projectRef: string
22
+ defaultEnvironment: string
23
+ token?: string
24
+ orgId?: string | undefined
25
+ cloudApiUrl?: string
26
+ linkedAt: string
27
+ environments: Record<string, EnvironmentTarget>
28
+ }
29
+
30
+ export interface LocalEnvironment {
31
+ target: "local"
32
+ apiUrl: string
33
+ databaseUrl: string
34
+ projectRef: string
35
+ kongPort: number
36
+ provider: "docker" | "native"
37
+ }
38
+
39
+ export interface BranchContext {
40
+ mode: "branch"
41
+ branchId: string
42
+ apiUrl: string
43
+ token?: string | undefined
44
+ }
45
+
46
+ interface LegacyCloudFile {
47
+ apiUrl?: string
48
+ token?: string
49
+ projectSlug?: string
50
+ orgId?: string
51
+ }
52
+
53
+ interface LegacyLinkedFile {
54
+ ref?: string
55
+ orgId?: string
56
+ }
57
+
58
+ export function writeLocalEnvironment(cwd: string, env: LocalEnvironment): void {
59
+ const dir = resolve(cwd, ".supatype")
60
+ mkdirSync(dir, { recursive: true })
61
+ writeFileSync(resolve(dir, "environment.json"), `${JSON.stringify(env, null, 2)}\n`, "utf8")
62
+ }
63
+
64
+ export function loadLocalEnvironment(cwd: string): LocalEnvironment | null {
65
+ const path = resolve(cwd, ".supatype/environment.json")
66
+ if (!existsSync(path)) return null
67
+ return JSON.parse(readFileSync(path, "utf8")) as LocalEnvironment
68
+ }
69
+
70
+ export function linkPath(cwd: string): string {
71
+ return resolve(cwd, LINK_FILE)
72
+ }
73
+
74
+ export function loadProjectLink(cwd: string): ProjectLink | null {
75
+ migrateLegacyLinkFiles(cwd)
76
+ const path = linkPath(cwd)
77
+ if (!existsSync(path)) return null
78
+ return JSON.parse(readFileSync(path, "utf8")) as ProjectLink
79
+ }
80
+
81
+ export function saveProjectLink(cwd: string, link: ProjectLink): void {
82
+ const dir = resolve(cwd, ".supatype")
83
+ mkdirSync(dir, { recursive: true })
84
+ writeFileSync(linkPath(cwd), `${JSON.stringify(link, null, 2)}\n`, "utf8")
85
+ }
86
+
87
+ export function isProjectLinked(cwd: string): boolean {
88
+ const link = loadProjectLink(cwd)
89
+ return Boolean(link?.projectRef && Object.keys(link.environments).length > 0)
90
+ }
91
+
92
+ export function resolveEnvironmentName(link: ProjectLink, envFlag?: string): string {
93
+ if (envFlag?.trim()) return envFlag.trim()
94
+ return link.defaultEnvironment || "production"
95
+ }
96
+
97
+ export function getEnvironmentTarget(link: ProjectLink, envName: string): EnvironmentTarget | null {
98
+ return link.environments[envName] ?? null
99
+ }
100
+
101
+ export function resolveEnvironmentToken(link: ProjectLink, env: EnvironmentTarget): string | undefined {
102
+ return env.token ?? link.token
103
+ }
104
+
105
+ let migrationWarned = false
106
+
107
+ /** Merge legacy cloud.json + linked.json into link.json once. */
108
+ export function migrateLegacyLinkFiles(cwd: string): void {
109
+ const target = linkPath(cwd)
110
+ if (existsSync(target)) return
111
+
112
+ const cloudPath = resolve(cwd, LEGACY_CLOUD_FILE)
113
+ const linkedPath = resolve(cwd, LEGACY_LINKED_FILE)
114
+ const hasCloud = existsSync(cloudPath)
115
+ const hasLinked = existsSync(linkedPath)
116
+ if (!hasCloud && !hasLinked) return
117
+
118
+ let cloud: LegacyCloudFile | null = null
119
+ let linked: LegacyLinkedFile | null = null
120
+
121
+ if (hasCloud) {
122
+ cloud = JSON.parse(readFileSync(cloudPath, "utf8")) as LegacyCloudFile
123
+ }
124
+ if (hasLinked) {
125
+ linked = JSON.parse(readFileSync(linkedPath, "utf8")) as LegacyLinkedFile
126
+ }
127
+
128
+ const projectRef = cloud?.projectSlug ?? linked?.ref
129
+ if (!projectRef || !cloud?.token) {
130
+ return
131
+ }
132
+
133
+ const now = new Date().toISOString()
134
+ const link: ProjectLink = {
135
+ version: LINK_VERSION,
136
+ kind: "cloud",
137
+ projectRef,
138
+ defaultEnvironment: "production",
139
+ token: cloud.token,
140
+ cloudApiUrl: cloud.apiUrl ?? "https://api.supatype.com",
141
+ linkedAt: now,
142
+ environments: {
143
+ production: {
144
+ name: "production",
145
+ apiUrl: cloud.apiUrl ?? "https://api.supatype.com",
146
+ linkedAt: now,
147
+ },
148
+ },
149
+ ...(cloud.orgId !== undefined
150
+ ? { orgId: cloud.orgId }
151
+ : linked?.orgId !== undefined
152
+ ? { orgId: linked.orgId }
153
+ : {}),
154
+ }
155
+
156
+ saveProjectLink(cwd, link)
157
+
158
+ if (!migrationWarned) {
159
+ migrationWarned = true
160
+ console.warn(
161
+ "Migrated .supatype/cloud.json → .supatype/link.json (legacy files kept; remove manually when ready).",
162
+ )
163
+ }
164
+ }
165
+
166
+ export function createSelfHostLink(params: {
167
+ projectRef: string
168
+ apiUrl: string
169
+ token: string
170
+ envName?: string
171
+ existing?: ProjectLink | null
172
+ }): ProjectLink {
173
+ const envName = params.envName ?? "production"
174
+ const now = new Date().toISOString()
175
+ const env: EnvironmentTarget = {
176
+ name: envName,
177
+ apiUrl: params.apiUrl.replace(/\/$/, ""),
178
+ token: params.token,
179
+ linkedAt: now,
180
+ }
181
+
182
+ if (params.existing) {
183
+ return {
184
+ ...params.existing,
185
+ kind: params.existing.kind === "cloud" ? "self-host" : params.existing.kind,
186
+ projectRef: params.projectRef,
187
+ defaultEnvironment: params.existing.defaultEnvironment || envName,
188
+ linkedAt: now,
189
+ environments: {
190
+ ...params.existing.environments,
191
+ [envName]: env,
192
+ },
193
+ }
194
+ }
195
+
196
+ return {
197
+ version: LINK_VERSION,
198
+ kind: "self-host",
199
+ projectRef: params.projectRef,
200
+ defaultEnvironment: envName,
201
+ token: params.token,
202
+ linkedAt: now,
203
+ environments: { [envName]: env },
204
+ }
205
+ }
206
+
207
+ export function createCloudLink(params: {
208
+ projectRef: string
209
+ cloudApiUrl: string
210
+ token: string
211
+ orgId?: string | undefined
212
+ environments?: Array<{ name: string; apiUrl: string }>
213
+ existing?: ProjectLink | null
214
+ }): ProjectLink {
215
+ const now = new Date().toISOString()
216
+ const envMap: Record<string, EnvironmentTarget> = {}
217
+ const envs = params.environments?.length
218
+ ? params.environments
219
+ : [{ name: "production", apiUrl: params.cloudApiUrl }]
220
+
221
+ for (const e of envs) {
222
+ envMap[e.name] = {
223
+ name: e.name,
224
+ apiUrl: e.apiUrl.replace(/\/$/, ""),
225
+ linkedAt: now,
226
+ }
227
+ }
228
+
229
+ return {
230
+ version: LINK_VERSION,
231
+ kind: "cloud",
232
+ projectRef: params.projectRef,
233
+ defaultEnvironment: "production",
234
+ token: params.token,
235
+ cloudApiUrl: params.cloudApiUrl.replace(/\/$/, ""),
236
+ linkedAt: now,
237
+ environments: params.existing
238
+ ? { ...params.existing.environments, ...envMap }
239
+ : envMap,
240
+ ...(params.orgId !== undefined ? { orgId: params.orgId } : {}),
241
+ }
242
+ }
@@ -32,6 +32,8 @@ export interface ProcessOptions {
32
32
  onLine?: (line: string, stream: "stdout" | "stderr") => void
33
33
  /** Return false to drop a line before logging. */
34
34
  shouldLogLine?: (line: string) => boolean
35
+ /** Called before auto-restart after a non-zero exit (e.g. free a stale port). */
36
+ beforeRestart?: () => void
35
37
  }
36
38
 
37
39
  const RESET = "\x1b[0m"
@@ -164,6 +166,7 @@ export class ProcessManager {
164
166
  process.stderr.write(message)
165
167
  }
166
168
  setTimeout(() => {
169
+ this.opts.beforeRestart?.()
167
170
  this.backoffMs = Math.min(this.backoffMs * 2, this.opts.maxBackoffMs)
168
171
  this.spawn()
169
172
  }, this.backoffMs)
@@ -186,6 +189,7 @@ export class ProcessManager {
186
189
  }
187
190
 
188
191
  setTimeout(() => {
192
+ this.opts.beforeRestart?.()
189
193
  this.backoffMs = Math.min(this.backoffMs * 2, this.opts.maxBackoffMs)
190
194
  this.spawn()
191
195
  }, this.backoffMs)
@@ -201,6 +201,14 @@ export interface SupatypeProjectConfig {
201
201
  /** Custom response headers for the deployed static site. */
202
202
  headers?: Record<string, string>
203
203
  }
204
+ /**
205
+ * Persistent environment defaults for `resolveTarget()` when `--env` is omitted.
206
+ * Ephemeral schema branches (Phase 22) use `.supatype/branch.json`, not this block.
207
+ */
208
+ environments?: {
209
+ default?: string
210
+ branchDefaults?: Record<string, string>
211
+ }
204
212
  /**
205
213
  * Optional Postgres URL for CLI commands that talk to the DB (`push`, `migrate`, …).
206
214
  * When omitted, `DATABASE_URL` from the environment is used, then a local default DSN.