@supatype/cli 0.1.5 → 0.1.7

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 (247) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +143 -93
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/deno.d.ts +52 -0
  5. package/dist/augmentation-generator.d.ts +22 -0
  6. package/dist/augmentation-generator.d.ts.map +1 -1
  7. package/dist/augmentation-generator.js +43 -7
  8. package/dist/augmentation-generator.js.map +1 -1
  9. package/dist/binary-cache.d.ts +14 -0
  10. package/dist/binary-cache.d.ts.map +1 -1
  11. package/dist/binary-cache.js +33 -13
  12. package/dist/binary-cache.js.map +1 -1
  13. package/dist/cache-clean.d.ts +15 -0
  14. package/dist/cache-clean.d.ts.map +1 -0
  15. package/dist/cache-clean.js +76 -0
  16. package/dist/cache-clean.js.map +1 -0
  17. package/dist/cache-pins.d.ts +51 -0
  18. package/dist/cache-pins.d.ts.map +1 -0
  19. package/dist/cache-pins.js +184 -0
  20. package/dist/cache-pins.js.map +1 -0
  21. package/dist/cli-install-method.d.ts +19 -0
  22. package/dist/cli-install-method.d.ts.map +1 -0
  23. package/dist/cli-install-method.js +60 -0
  24. package/dist/cli-install-method.js.map +1 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-credentials.d.ts +34 -0
  29. package/dist/cloud-credentials.d.ts.map +1 -0
  30. package/dist/cloud-credentials.js +139 -0
  31. package/dist/cloud-credentials.js.map +1 -0
  32. package/dist/commands/admin.d.ts +11 -0
  33. package/dist/commands/admin.d.ts.map +1 -1
  34. package/dist/commands/admin.js +131 -22
  35. package/dist/commands/admin.js.map +1 -1
  36. package/dist/commands/cache.d.ts.map +1 -1
  37. package/dist/commands/cache.js +67 -71
  38. package/dist/commands/cache.js.map +1 -1
  39. package/dist/commands/cloud.d.ts.map +1 -1
  40. package/dist/commands/cloud.js +32 -8
  41. package/dist/commands/cloud.js.map +1 -1
  42. package/dist/commands/db.d.ts +1 -0
  43. package/dist/commands/db.d.ts.map +1 -1
  44. package/dist/commands/db.js +147 -1
  45. package/dist/commands/db.js.map +1 -1
  46. package/dist/commands/dev.d.ts.map +1 -1
  47. package/dist/commands/dev.js +50 -10
  48. package/dist/commands/dev.js.map +1 -1
  49. package/dist/commands/doctor.d.ts +28 -0
  50. package/dist/commands/doctor.d.ts.map +1 -1
  51. package/dist/commands/doctor.js +60 -3
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/functions.d.ts +7 -0
  54. package/dist/commands/functions.d.ts.map +1 -1
  55. package/dist/commands/functions.js +40 -6
  56. package/dist/commands/functions.js.map +1 -1
  57. package/dist/commands/generate.d.ts.map +1 -1
  58. package/dist/commands/generate.js +5 -1
  59. package/dist/commands/generate.js.map +1 -1
  60. package/dist/commands/hooks.d.ts +12 -0
  61. package/dist/commands/hooks.d.ts.map +1 -0
  62. package/dist/commands/hooks.js +118 -0
  63. package/dist/commands/hooks.js.map +1 -0
  64. package/dist/commands/init.d.ts.map +1 -1
  65. package/dist/commands/init.js +41 -7
  66. package/dist/commands/init.js.map +1 -1
  67. package/dist/commands/internal.d.ts +6 -0
  68. package/dist/commands/internal.d.ts.map +1 -0
  69. package/dist/commands/internal.js +22 -0
  70. package/dist/commands/internal.js.map +1 -0
  71. package/dist/commands/keys.d.ts.map +1 -1
  72. package/dist/commands/keys.js +2 -0
  73. package/dist/commands/keys.js.map +1 -1
  74. package/dist/commands/link-helpers.d.ts.map +1 -1
  75. package/dist/commands/link-helpers.js +8 -2
  76. package/dist/commands/link-helpers.js.map +1 -1
  77. package/dist/commands/login.d.ts +7 -0
  78. package/dist/commands/login.d.ts.map +1 -0
  79. package/dist/commands/login.js +110 -0
  80. package/dist/commands/login.js.map +1 -0
  81. package/dist/commands/push.d.ts.map +1 -1
  82. package/dist/commands/push.js +110 -7
  83. package/dist/commands/push.js.map +1 -1
  84. package/dist/commands/self-update.d.ts +7 -0
  85. package/dist/commands/self-update.d.ts.map +1 -1
  86. package/dist/commands/self-update.js +18 -11
  87. package/dist/commands/self-update.js.map +1 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +18 -3
  90. package/dist/config.js.map +1 -1
  91. package/dist/db-preflight.d.ts +78 -0
  92. package/dist/db-preflight.d.ts.map +1 -0
  93. package/dist/db-preflight.js +452 -0
  94. package/dist/db-preflight.js.map +1 -0
  95. package/dist/dev-compose.d.ts +1 -1
  96. package/dist/dev-compose.d.ts.map +1 -1
  97. package/dist/dev-compose.js +129 -72
  98. package/dist/dev-compose.js.map +1 -1
  99. package/dist/env-file.d.ts +40 -2
  100. package/dist/env-file.d.ts.map +1 -1
  101. package/dist/env-file.js +132 -11
  102. package/dist/env-file.js.map +1 -1
  103. package/dist/field-masking-tier.d.ts +50 -0
  104. package/dist/field-masking-tier.d.ts.map +1 -0
  105. package/dist/field-masking-tier.js +91 -0
  106. package/dist/field-masking-tier.js.map +1 -0
  107. package/dist/functions-deno-types.d.ts +24 -0
  108. package/dist/functions-deno-types.d.ts.map +1 -0
  109. package/dist/functions-deno-types.js +155 -0
  110. package/dist/functions-deno-types.js.map +1 -0
  111. package/dist/hook-upload.d.ts +30 -0
  112. package/dist/hook-upload.d.ts.map +1 -0
  113. package/dist/hook-upload.js +86 -0
  114. package/dist/hook-upload.js.map +1 -0
  115. package/dist/hooks-generator.d.ts +8 -0
  116. package/dist/hooks-generator.d.ts.map +1 -0
  117. package/dist/hooks-generator.js +332 -0
  118. package/dist/hooks-generator.js.map +1 -0
  119. package/dist/link.d.ts +4 -0
  120. package/dist/link.d.ts.map +1 -1
  121. package/dist/link.js +2 -0
  122. package/dist/link.js.map +1 -1
  123. package/dist/local-secrets.d.ts +71 -0
  124. package/dist/local-secrets.d.ts.map +1 -0
  125. package/dist/local-secrets.js +100 -0
  126. package/dist/local-secrets.js.map +1 -0
  127. package/dist/model-hooks.d.ts +73 -0
  128. package/dist/model-hooks.d.ts.map +1 -0
  129. package/dist/model-hooks.js +211 -0
  130. package/dist/model-hooks.js.map +1 -0
  131. package/dist/postgres-ctl.d.ts +11 -0
  132. package/dist/postgres-ctl.d.ts.map +1 -1
  133. package/dist/postgres-ctl.js +25 -2
  134. package/dist/postgres-ctl.js.map +1 -1
  135. package/dist/project-config.d.ts +121 -1
  136. package/dist/project-config.d.ts.map +1 -1
  137. package/dist/project-config.js +150 -1
  138. package/dist/project-config.js.map +1 -1
  139. package/dist/resolve-target.d.ts +4 -0
  140. package/dist/resolve-target.d.ts.map +1 -1
  141. package/dist/resolve-target.js +69 -73
  142. package/dist/resolve-target.js.map +1 -1
  143. package/dist/schema-ast-v2.d.ts +2 -1
  144. package/dist/schema-ast-v2.d.ts.map +1 -1
  145. package/dist/schema-ast-v2.js +4 -2
  146. package/dist/schema-ast-v2.js.map +1 -1
  147. package/dist/schema-push-lock.d.ts.map +1 -1
  148. package/dist/schema-push-lock.js +43 -16
  149. package/dist/schema-push-lock.js.map +1 -1
  150. package/dist/self-host-compose.d.ts +24 -0
  151. package/dist/self-host-compose.d.ts.map +1 -1
  152. package/dist/self-host-compose.js +301 -69
  153. package/dist/self-host-compose.js.map +1 -1
  154. package/dist/service-role-check.d.ts +21 -0
  155. package/dist/service-role-check.d.ts.map +1 -0
  156. package/dist/service-role-check.js +85 -0
  157. package/dist/service-role-check.js.map +1 -0
  158. package/dist/target-client.d.ts +17 -0
  159. package/dist/target-client.d.ts.map +1 -1
  160. package/dist/target-client.js +69 -17
  161. package/dist/target-client.js.map +1 -1
  162. package/dist/type-extractor.d.ts.map +1 -1
  163. package/dist/type-extractor.js +736 -32
  164. package/dist/type-extractor.js.map +1 -1
  165. package/dist/ui/runtime/command-chrome.js +1 -1
  166. package/dist/ui/runtime/command-chrome.js.map +1 -1
  167. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
  168. package/dist/ui/runtime/stdin-after-ink.js +3 -1
  169. package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
  170. package/package.json +4 -1
  171. package/scripts/generate-homebrew-formula.mjs +121 -0
  172. package/src/augmentation-generator.ts +46 -9
  173. package/src/binary-cache.ts +47 -15
  174. package/src/cache-clean.ts +81 -0
  175. package/src/cache-pins.ts +243 -0
  176. package/src/cli-install-method.ts +67 -0
  177. package/src/cli.ts +6 -0
  178. package/src/cloud-credentials.ts +183 -0
  179. package/src/commands/admin.ts +153 -28
  180. package/src/commands/cache.ts +90 -75
  181. package/src/commands/cloud.ts +32 -8
  182. package/src/commands/db.ts +162 -1
  183. package/src/commands/dev.ts +57 -8
  184. package/src/commands/doctor.ts +69 -3
  185. package/src/commands/functions.ts +42 -5
  186. package/src/commands/generate.ts +5 -1
  187. package/src/commands/hooks.ts +138 -0
  188. package/src/commands/init.ts +42 -8
  189. package/src/commands/internal.ts +23 -0
  190. package/src/commands/keys.ts +2 -0
  191. package/src/commands/link-helpers.ts +9 -2
  192. package/src/commands/login.ts +127 -0
  193. package/src/commands/push.ts +124 -7
  194. package/src/commands/self-update.ts +85 -13
  195. package/src/config.ts +22 -3
  196. package/src/db-preflight.ts +575 -0
  197. package/src/dev-compose.ts +144 -73
  198. package/src/env-file.ts +157 -11
  199. package/src/field-masking-tier.ts +107 -0
  200. package/src/functions-deno-types.ts +188 -0
  201. package/src/hook-upload.ts +107 -0
  202. package/src/hooks-generator.ts +352 -0
  203. package/src/link.ts +7 -0
  204. package/src/local-secrets.ts +104 -0
  205. package/src/model-hooks.ts +260 -0
  206. package/src/postgres-ctl.ts +29 -2
  207. package/src/project-config.ts +236 -2
  208. package/src/resolve-target.ts +113 -68
  209. package/src/schema-ast-v2.ts +5 -2
  210. package/src/schema-push-lock.ts +56 -17
  211. package/src/self-host-compose.ts +344 -68
  212. package/src/service-role-check.ts +111 -0
  213. package/src/target-client.ts +105 -18
  214. package/src/type-extractor.ts +883 -28
  215. package/src/ui/runtime/command-chrome.ts +1 -1
  216. package/src/ui/runtime/stdin-after-ink.ts +3 -1
  217. package/tests/access-composition.test.ts +667 -0
  218. package/tests/admin-studio-membership.test.ts +121 -0
  219. package/tests/api-schemas.test.ts +83 -0
  220. package/tests/archive-name-alias.test.ts +42 -0
  221. package/tests/augmentation-generator.test.ts +72 -4
  222. package/tests/cache-clean.test.ts +206 -0
  223. package/tests/cli-help.test.ts +3 -0
  224. package/tests/cli-install-method.test.ts +66 -0
  225. package/tests/command-chrome-coverage.test.ts +31 -26
  226. package/tests/command-chrome-wrap.test.ts +1 -0
  227. package/tests/db-preflight.test.ts +72 -0
  228. package/tests/doctor-render.test.ts +62 -0
  229. package/tests/doctor-service-role.test.ts +55 -0
  230. package/tests/env-file.test.ts +121 -0
  231. package/tests/external-database-compose.test.ts +184 -0
  232. package/tests/external-database-config.test.ts +154 -0
  233. package/tests/field-masking-tier.test.ts +184 -0
  234. package/tests/functions-deno-types.test.ts +197 -0
  235. package/tests/homebrew-formula.test.ts +58 -0
  236. package/tests/hook-upload.test.ts +125 -0
  237. package/tests/hooks-generator.test.ts +212 -0
  238. package/tests/init.test.ts +3 -0
  239. package/tests/local-secrets.test.ts +92 -0
  240. package/tests/model-hooks.test.ts +322 -0
  241. package/tests/runtime-contract.test.ts +113 -1
  242. package/tests/service-role-check.test.ts +109 -0
  243. package/tests/service-role-routes.test.ts +51 -0
  244. package/tests/target-client-refresh.test.ts +94 -0
  245. package/tests/type-extractor.test.ts +187 -0
  246. package/tsconfig.tsbuildinfo +1 -1
  247. package/tests/engine-distribution.test.ts +0 -418
@@ -1,8 +1,17 @@
1
1
  import type { Command } from "commander"
2
- import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { loadConfig, loadSchemaAst } from "../config.js"
5
- import { resolveRuntimeProvider, schemaPathFromProject, serverBaseUrl } from "../project-config.js"
5
+ import { syncManifestHooks, validateModelHooks, writeHooksModule } from "../model-hooks.js"
6
+ import { adapterEntry, readHookUpload } from "../hook-upload.js"
7
+ import { checkServiceRoleRoutes, serviceRoleProblemLines } from "../service-role-check.js"
8
+ import { fatalError } from "../ui/fatal.js"
9
+ import {
10
+ hooksPathFromProject,
11
+ resolveRuntimeProvider,
12
+ schemaPathFromProject,
13
+ serverBaseUrl,
14
+ } from "../project-config.js"
6
15
  import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
7
16
  import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
8
17
  import { signJwt } from "../jwt.js"
@@ -20,6 +29,7 @@ import {
20
29
  type DeployTarget,
21
30
  } from "../resolve-target.js"
22
31
  import { loadProjectLink } from "../link.js"
32
+ import { targetFetch } from "../target-client.js"
23
33
  import {
24
34
  buildSchemaSourcesPayload,
25
35
  cacheSchemaSourcesLocally,
@@ -54,6 +64,8 @@ export function registerPush(program: Command): void {
54
64
  const config = loadConfig(cwd)
55
65
  const pgSchema = schemaPgSchema(cwd)
56
66
  const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
67
+ assertModelHooksResolve(cwd, config, ast)
68
+ assertServiceRoleGrantsResolve(cwd, config)
57
69
 
58
70
  const linked = loadProjectLink(cwd)
59
71
  const useDirect = opts.direct || opts.local || Boolean(opts.connection)
@@ -143,6 +155,10 @@ async function pushViaTarget(
143
155
  }
144
156
  }
145
157
 
158
+ if (target.mode === "cloud" || target.mode === "self-host") {
159
+ await deployHooksToTarget(cwd, config, target, ast)
160
+ }
161
+
146
162
  if (target.mode === "direct" || target.mode === "local") {
147
163
  await writeLocalAdminConfig(ast, config)
148
164
  if (target.databaseUrl) {
@@ -150,17 +166,86 @@ async function pushViaTarget(
150
166
  }
151
167
  await generateTypesLocal(ast, config)
152
168
  await provisionLocalStorage(ast, config)
153
- } else {
154
- info(`Pushed to ${target.mode} (${target.environment}).`)
169
+
170
+ // Local Studio only — a cloud/self-host push must not advertise the local
171
+ // gateway URL from config, which may not even be running.
172
+ const baseUrl = (serverBaseUrl(config) ?? "").replace(/\/$/, "")
173
+ if (baseUrl) {
174
+ plain(`\nStudio: ${baseUrl}/studio/`)
175
+ }
176
+ return
155
177
  }
156
178
 
157
- const baseUrl = (serverBaseUrl(config) ?? "").replace(/\/$/, "")
158
- if (baseUrl) {
159
- plain(`\nStudio: ${baseUrl}/studio/`)
179
+ info(`Pushed to ${target.mode} (${target.environment}).`)
180
+ const envUrl = target.link?.environments?.[target.environment]?.apiUrl?.replace(/\/$/, "")
181
+ if (envUrl) {
182
+ plain(`\nProject API: ${envUrl}`)
160
183
  }
161
184
  }
162
185
 
186
+ /**
187
+ * Upload the hooks this schema names to a managed stack.
188
+ *
189
+ * On `push` rather than `functions deploy`, because the hook *map* comes from the schema: the two have
190
+ * to move together, and a map naming a handler that was never uploaded turns every write to that table
191
+ * into a 503.
192
+ *
193
+ * The handler types are written locally as well. A project pushing to cloud still edits its hooks on
194
+ * this machine, and `hooks/_supatype/hooks.ts` is what makes them typed — it is generated, never
195
+ * committed, so a fresh clone that has only ever pushed to cloud would otherwise have no types at all.
196
+ */
197
+ async function deployHooksToTarget(
198
+ cwd: string,
199
+ config: SupatypeProjectConfig,
200
+ target: DeployTarget,
201
+ ast: unknown,
202
+ ): Promise<void> {
203
+ const hooksDir = hooksPathFromProject(config, cwd)
204
+ const hooksPath = writeHooksModule(cwd, hooksDir, ast)
205
+ if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
206
+
207
+ let upload: ReturnType<typeof readHookUpload>
208
+ try {
209
+ upload = readHookUpload(cwd, hooksDir, ast)
210
+ } catch (err) {
211
+ // Refused rather than partially uploaded: the schema is already applied, so the honest thing is to
212
+ // say the hooks did not deploy and why, leaving the previous ones in place.
213
+ fatalError(err instanceof Error ? err.message : String(err))
214
+ return
215
+ }
216
+ if (upload === null) return
217
+
218
+ const adapter = readGeneratedAdapter(hooksDir)
219
+ const handlers = adapter === null ? upload.handlers : [...upload.handlers, adapter]
220
+
221
+ await withSpinner(`Deploying ${upload.handlers.length} hook(s)`, () =>
222
+ targetFetch(target.apiBaseUrl, target.apiPrefix, {
223
+ method: "POST",
224
+ path: `/projects/${target.projectRef}/hooks/deploy`,
225
+ body: { handlers, map: upload.map },
226
+ token: target.token!,
227
+ orgId: target.orgId,
228
+ environment: target.mode === "cloud" ? target.environment : undefined,
229
+ }),
230
+ ).then(() => info(`Deployed ${upload.handlers.length} hook(s)`))
231
+ }
232
+
233
+ /** The generated adapter, flattened to the name handlers were rewritten to import. */
234
+ function readGeneratedAdapter(hooksDir: string): { name: string; source: string } | null {
235
+ const file = join(hooksDir, "_supatype", "hooks.ts")
236
+ if (!existsSync(file)) return null
237
+ return adapterEntry(readFileSync(file, "utf8"))
238
+ }
239
+
163
240
  async function generateTypesLocal(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
241
+ // Independent of `output`: a project with hooks needs its handler types whether or not it asked
242
+ // for client types, and the module is written next to the functions that import it.
243
+ const cwd = process.cwd()
244
+ const hooksPath = writeHooksModule(cwd, hooksPathFromProject(config, cwd), ast)
245
+ if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
246
+ // The server watches this file, so a changed hook takes effect without a restart.
247
+ if (syncManifestHooks(cwd, ast)) info("Hook map written to .supatype/manifest.json")
248
+
164
249
  if (!config.output?.types && !config.output?.client) return
165
250
  await withSpinner("Generating types", async () => {
166
251
  await ensureEngine()
@@ -195,3 +280,35 @@ async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig
195
280
  restoreSystemRelationTargets(admin, ast)
196
281
  writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
197
282
  }
283
+
284
+ /**
285
+ * Stop the push when a declared hook names a function that is not there.
286
+ *
287
+ * A hook is only enforcement if it runs. A typo'd name would extract cleanly, reach the manifest, and
288
+ * then never fire — so the write it was meant to validate would succeed and look fine. Cheaper to
289
+ * fail here, naming the directory searched.
290
+ */
291
+ /**
292
+ * Refuse a push whose `functions.serviceRole` names a function that does not exist.
293
+ *
294
+ * The grant fails closed, which is the safe direction and the invisible one: the function reads no key
295
+ * at runtime, in a deploy that reported success. A warning is printed for an entry that is merely
296
+ * redundant, since a reader would reasonably assume the line is what does the granting.
297
+ */
298
+ function assertServiceRoleGrantsResolve(cwd: string, config: SupatypeProjectConfig): void {
299
+ const problems = checkServiceRoleRoutes(config, cwd)
300
+ for (const warning of problems.warnings) plain(warning)
301
+ const lines = serviceRoleProblemLines(problems)
302
+ if (lines.length === 0) return
303
+ fatalError("functions.serviceRole names a function that does not exist.", lines, {
304
+ brand: { intro: "Push" },
305
+ })
306
+ }
307
+
308
+ function assertModelHooksResolve(cwd: string, config: SupatypeProjectConfig, ast: unknown): void {
309
+ const problems = validateModelHooks(ast, hooksPathFromProject(config, cwd), cwd)
310
+ if (problems.length === 0) return
311
+ fatalError("A model declares a hook whose function does not exist.", problems, {
312
+ brand: { intro: "Push" },
313
+ })
314
+ }
@@ -1,67 +1,139 @@
1
1
  /**
2
+
2
3
  * supatype self-update — Phase 10.6C8.
4
+
3
5
  *
6
+
4
7
  * npm-installed CLI: instruct to use `npm update -g`.
8
+
9
+ * Homebrew: instruct to use `brew upgrade supatype`.
10
+
5
11
  * Standalone curl|sh installs: replace binary from releases.supatype.com/cli/.
12
+
6
13
  */
7
14
 
15
+
16
+
8
17
  import type { Command } from "commander"
18
+
9
19
  import { copyFileSync, renameSync } from "node:fs"
10
- import { sep } from "node:path"
20
+
21
+ import { detectCliInstallMethod } from "../cli-install-method.js"
22
+
11
23
  import { downloadStandaloneCli, fetchStandaloneCliLatestVersion } from "../cli-standalone.js"
24
+
12
25
  import { error, info, plain } from "../ui/messages.js"
13
26
 
14
- function looksLikeNpmOrWorkspaceCLI(): boolean {
15
- const main = process.argv[1] ?? ""
16
- return (
17
- main.includes("node_modules") ||
18
- main.includes(`${sep}dist${sep}cli`) ||
19
- main.includes(`${sep}bin${sep}supatype.js`) ||
20
- Boolean(process.env["npm_execpath"]) ||
21
- Boolean(process.env["npm_lifecycle_event"])
22
- )
23
- }
27
+
24
28
 
25
29
  export function registerSelfUpdate(program: Command): void {
30
+
26
31
  program
32
+
27
33
  .command("self-update")
34
+
28
35
  .description("Update the supatype CLI (npm or standalone CDN binary)")
36
+
29
37
  .action(async () => {
30
- if (looksLikeNpmOrWorkspaceCLI()) {
38
+
39
+ const method = detectCliInstallMethod()
40
+
41
+
42
+
43
+ if (method === "npm") {
44
+
31
45
  plain(
46
+
32
47
  "This CLI was installed via npm (or runs from the package workspace).\n" +
48
+
33
49
  "Update with:\n\n npm update -g @supatype/cli\n\n" +
50
+
34
51
  "To bump engine/server/postgres/deno/realtime pinned in supatype.config.ts, use:\n\n supatype update\n",
52
+
53
+ )
54
+
55
+ return
56
+
57
+ }
58
+
59
+
60
+
61
+ if (method === "homebrew") {
62
+
63
+ plain(
64
+
65
+ "This CLI was installed via Homebrew.\n" +
66
+
67
+ "Update with:\n\n brew update && brew upgrade supatype\n\n" +
68
+
69
+ "To bump component binaries pinned in supatype.config.ts, use:\n\n supatype update\n",
70
+
35
71
  )
72
+
36
73
  return
74
+
37
75
  }
38
76
 
77
+
78
+
39
79
  const currentPath = process.argv[1]
80
+
40
81
  if (!currentPath) {
82
+
41
83
  error("Could not determine current CLI path.")
84
+
42
85
  process.exit(1)
86
+
43
87
  }
44
88
 
89
+
90
+
45
91
  try {
92
+
46
93
  const latest = await fetchStandaloneCliLatestVersion()
94
+
47
95
  info(`Downloading supatype CLI v${latest}...`)
96
+
48
97
  const downloaded = await downloadStandaloneCli(latest)
98
+
49
99
  const backup = `${currentPath}.bak`
100
+
50
101
  copyFileSync(currentPath, backup)
102
+
51
103
  try {
104
+
52
105
  renameSync(downloaded, currentPath)
53
- } catch (err) {
106
+
107
+ } catch {
108
+
54
109
  copyFileSync(downloaded, currentPath)
110
+
55
111
  }
112
+
56
113
  info(`Updated to v${latest}.`)
114
+
57
115
  plain("Run `supatype --version` to verify.")
116
+
58
117
  } catch (err) {
118
+
59
119
  error(
120
+
60
121
  `Standalone CLI self-update failed: ${(err as Error).message}\n\n` +
122
+
61
123
  "Install or upgrade via npm:\n npm install -g @supatype/cli\n\n" +
124
+
125
+ "Or Homebrew:\n brew upgrade supatype\n\n" +
126
+
62
127
  "Component binaries (engine, server, postgres, deno, realtime):\n supatype update\n",
128
+
63
129
  )
130
+
64
131
  process.exit(1)
132
+
65
133
  }
134
+
66
135
  })
136
+
67
137
  }
138
+
139
+
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@ import { resolve } from "node:path"
3
3
  import { tmpdir } from "node:os"
4
4
  import { join } from "node:path"
5
5
  import { evalTsSnippet } from "./tsx-runner.js"
6
+ import { readEnvFile } from "./env-file.js"
6
7
  import {
7
8
  mergeProjectConfig,
8
9
  validateProjectConfig,
@@ -135,7 +136,25 @@ export function loadConfig(cwd: string = process.cwd()): SupatypeProjectConfig {
135
136
  if (localRaw === null) return base
136
137
 
137
138
  const localNorm = normalizeProjectJson(localRaw) as Partial<SupatypeProjectConfig>
138
- return mergeProjectConfig(base, localNorm)
139
+ // Re-validated after merging: the local override can reintroduce a combination the base file was
140
+ // rejected for — `database.provider` beside an inherited `database.external`, say — and merging
141
+ // has no opinion about that.
142
+ return validateProjectConfig(
143
+ mergeProjectConfig(base, localNorm),
144
+ "supatype.local.config.ts (merged with supatype.config.ts)",
145
+ )
146
+ }
147
+
148
+ /**
149
+ * Environment for the child process that imports the config module.
150
+ *
151
+ * `supatype.config.ts` is TypeScript, so the natural way to keep a database password out of version
152
+ * control is `process.env.DATABASE_URL` — which only works if the project's `.env` is visible to the
153
+ * process doing the import. A real environment variable still wins over the file, matching how every
154
+ * other dotenv reader behaves and how Compose itself resolves the same names.
155
+ */
156
+ function configLoadEnv(cwd: string): NodeJS.ProcessEnv {
157
+ return { ...readEnvFile(cwd), ...process.env }
139
158
  }
140
159
 
141
160
  function loadFirstTsConfig(
@@ -152,7 +171,7 @@ const mod = await import(${JSON.stringify(urlPath)})
152
171
  const config = mod.default ?? mod
153
172
  process.stdout.write(JSON.stringify(config))
154
173
  `
155
- const result = evalTsSnippet(snippet, { cwd })
174
+ const result = evalTsSnippet(snippet, { cwd, env: configLoadEnv(cwd) })
156
175
  if (result.exitCode === 0) {
157
176
  return JSON.parse(result.stdout) as Record<string, unknown>
158
177
  }
@@ -203,7 +222,7 @@ const mod = await import(${JSON.stringify(urlPath)})
203
222
  const config = mod.default ?? mod
204
223
  process.stdout.write(JSON.stringify(config))
205
224
  `
206
- const result = evalTsSnippet(snippet, { cwd })
225
+ const result = evalTsSnippet(snippet, { cwd, env: configLoadEnv(cwd) })
207
226
  if (result.exitCode !== 0) return null
208
227
  return JSON.parse(result.stdout) as Record<string, unknown>
209
228
  } finally {