@supatype/cli 0.1.0-alpha.6 → 0.1.0-alpha.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 (309) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +203 -1
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app-config.d.ts +7 -0
  5. package/dist/app-config.d.ts.map +1 -0
  6. package/dist/app-config.js +113 -0
  7. package/dist/app-config.js.map +1 -0
  8. package/dist/augmentation-generator.d.ts +2 -0
  9. package/dist/augmentation-generator.d.ts.map +1 -0
  10. package/dist/augmentation-generator.js +111 -0
  11. package/dist/augmentation-generator.js.map +1 -0
  12. package/dist/binary-cache.d.ts +89 -0
  13. package/dist/binary-cache.d.ts.map +1 -0
  14. package/dist/binary-cache.js +656 -0
  15. package/dist/binary-cache.js.map +1 -0
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +13 -7
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/admin.d.ts.map +1 -1
  20. package/dist/commands/admin.js +4 -3
  21. package/dist/commands/admin.js.map +1 -1
  22. package/dist/commands/app.d.ts.map +1 -1
  23. package/dist/commands/app.js +56 -209
  24. package/dist/commands/app.js.map +1 -1
  25. package/dist/commands/cache.d.ts +6 -0
  26. package/dist/commands/cache.d.ts.map +1 -0
  27. package/dist/commands/cache.js +105 -0
  28. package/dist/commands/cache.js.map +1 -0
  29. package/dist/commands/cloud.d.ts +12 -0
  30. package/dist/commands/cloud.d.ts.map +1 -1
  31. package/dist/commands/cloud.js +36 -46
  32. package/dist/commands/cloud.js.map +1 -1
  33. package/dist/commands/db.d.ts.map +1 -1
  34. package/dist/commands/db.js +47 -54
  35. package/dist/commands/db.js.map +1 -1
  36. package/dist/commands/deploy.d.ts +2 -1
  37. package/dist/commands/deploy.d.ts.map +1 -1
  38. package/dist/commands/deploy.js +92 -51
  39. package/dist/commands/deploy.js.map +1 -1
  40. package/dist/commands/dev.d.ts +11 -0
  41. package/dist/commands/dev.d.ts.map +1 -1
  42. package/dist/commands/dev.js +751 -384
  43. package/dist/commands/dev.js.map +1 -1
  44. package/dist/commands/diff.d.ts.map +1 -1
  45. package/dist/commands/diff.js +20 -15
  46. package/dist/commands/diff.js.map +1 -1
  47. package/dist/commands/engine.d.ts +1 -3
  48. package/dist/commands/engine.d.ts.map +1 -1
  49. package/dist/commands/engine.js +13 -85
  50. package/dist/commands/engine.js.map +1 -1
  51. package/dist/commands/functions.d.ts.map +1 -1
  52. package/dist/commands/functions.js +92 -105
  53. package/dist/commands/functions.js.map +1 -1
  54. package/dist/commands/generate.d.ts.map +1 -1
  55. package/dist/commands/generate.js +22 -12
  56. package/dist/commands/generate.js.map +1 -1
  57. package/dist/commands/init.d.ts +1 -1
  58. package/dist/commands/init.d.ts.map +1 -1
  59. package/dist/commands/init.js +124 -410
  60. package/dist/commands/init.js.map +1 -1
  61. package/dist/commands/migrate-from-v1.d.ts +5 -0
  62. package/dist/commands/migrate-from-v1.d.ts.map +1 -0
  63. package/dist/commands/migrate-from-v1.js +125 -0
  64. package/dist/commands/migrate-from-v1.js.map +1 -0
  65. package/dist/commands/migrate.d.ts.map +1 -1
  66. package/dist/commands/migrate.js +27 -23
  67. package/dist/commands/migrate.js.map +1 -1
  68. package/dist/commands/pg.d.ts +8 -0
  69. package/dist/commands/pg.d.ts.map +1 -0
  70. package/dist/commands/pg.js +102 -0
  71. package/dist/commands/pg.js.map +1 -0
  72. package/dist/commands/pull.d.ts.map +1 -1
  73. package/dist/commands/pull.js +5 -66
  74. package/dist/commands/pull.js.map +1 -1
  75. package/dist/commands/push.d.ts.map +1 -1
  76. package/dist/commands/push.js +99 -39
  77. package/dist/commands/push.js.map +1 -1
  78. package/dist/commands/seed.d.ts +2 -0
  79. package/dist/commands/seed.d.ts.map +1 -1
  80. package/dist/commands/seed.js +44 -11
  81. package/dist/commands/seed.js.map +1 -1
  82. package/dist/commands/self-host.d.ts +7 -1
  83. package/dist/commands/self-host.d.ts.map +1 -1
  84. package/dist/commands/self-host.js +272 -758
  85. package/dist/commands/self-host.js.map +1 -1
  86. package/dist/commands/self-update.d.ts +9 -0
  87. package/dist/commands/self-update.d.ts.map +1 -0
  88. package/dist/commands/self-update.js +33 -0
  89. package/dist/commands/self-update.js.map +1 -0
  90. package/dist/commands/status.d.ts.map +1 -1
  91. package/dist/commands/status.js +4 -3
  92. package/dist/commands/status.js.map +1 -1
  93. package/dist/commands/types.d.ts +3 -0
  94. package/dist/commands/types.d.ts.map +1 -0
  95. package/dist/commands/types.js +62 -0
  96. package/dist/commands/types.js.map +1 -0
  97. package/dist/commands/update.d.ts +7 -0
  98. package/dist/commands/update.d.ts.map +1 -0
  99. package/dist/commands/update.js +77 -0
  100. package/dist/commands/update.js.map +1 -0
  101. package/dist/components.d.ts +5 -0
  102. package/dist/components.d.ts.map +1 -0
  103. package/dist/components.js +3 -0
  104. package/dist/components.js.map +1 -0
  105. package/dist/config.d.ts +10 -51
  106. package/dist/config.d.ts.map +1 -1
  107. package/dist/config.js +101 -33
  108. package/dist/config.js.map +1 -1
  109. package/dist/docker-postgres.d.ts +39 -0
  110. package/dist/docker-postgres.d.ts.map +1 -0
  111. package/dist/docker-postgres.js +96 -0
  112. package/dist/docker-postgres.js.map +1 -0
  113. package/dist/engine-client.d.ts +67 -0
  114. package/dist/engine-client.d.ts.map +1 -0
  115. package/dist/engine-client.js +156 -0
  116. package/dist/engine-client.js.map +1 -0
  117. package/dist/ensure-binary.d.ts +7 -0
  118. package/dist/ensure-binary.d.ts.map +1 -0
  119. package/dist/ensure-binary.js +17 -0
  120. package/dist/ensure-binary.js.map +1 -0
  121. package/dist/functions-router-gen.d.ts +14 -0
  122. package/dist/functions-router-gen.d.ts.map +1 -0
  123. package/dist/functions-router-gen.js +199 -0
  124. package/dist/functions-router-gen.js.map +1 -0
  125. package/dist/index.d.ts +4 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +2 -3
  128. package/dist/index.js.map +1 -1
  129. package/dist/kong-config.d.ts +21 -0
  130. package/dist/kong-config.d.ts.map +1 -0
  131. package/dist/kong-config.js +60 -0
  132. package/dist/kong-config.js.map +1 -0
  133. package/dist/local-gateway.d.ts +7 -0
  134. package/dist/local-gateway.d.ts.map +1 -0
  135. package/dist/local-gateway.js +9 -0
  136. package/dist/local-gateway.js.map +1 -0
  137. package/dist/local-storage.d.ts +8 -0
  138. package/dist/local-storage.d.ts.map +1 -0
  139. package/dist/local-storage.js +14 -0
  140. package/dist/local-storage.js.map +1 -0
  141. package/dist/pgbouncer-userlist.d.ts +5 -0
  142. package/dist/pgbouncer-userlist.d.ts.map +1 -0
  143. package/dist/pgbouncer-userlist.js +14 -0
  144. package/dist/pgbouncer-userlist.js.map +1 -0
  145. package/dist/postgres-ctl.d.ts +44 -0
  146. package/dist/postgres-ctl.d.ts.map +1 -0
  147. package/dist/postgres-ctl.js +137 -0
  148. package/dist/postgres-ctl.js.map +1 -0
  149. package/dist/process-manager.d.ts +41 -0
  150. package/dist/process-manager.d.ts.map +1 -0
  151. package/dist/process-manager.js +120 -0
  152. package/dist/process-manager.js.map +1 -0
  153. package/dist/project-config.d.ts +215 -0
  154. package/dist/project-config.d.ts.map +1 -0
  155. package/dist/project-config.js +145 -0
  156. package/dist/project-config.js.map +1 -0
  157. package/dist/pull-utils.d.ts +15 -0
  158. package/dist/pull-utils.d.ts.map +1 -1
  159. package/dist/pull-utils.js +12 -0
  160. package/dist/pull-utils.js.map +1 -1
  161. package/dist/release-pins.d.ts +7 -0
  162. package/dist/release-pins.d.ts.map +1 -0
  163. package/dist/release-pins.js +27 -0
  164. package/dist/release-pins.js.map +1 -0
  165. package/dist/release-public-key.d.ts +8 -0
  166. package/dist/release-public-key.d.ts.map +1 -0
  167. package/dist/release-public-key.js +13 -0
  168. package/dist/release-public-key.js.map +1 -0
  169. package/dist/runtime-routes.d.ts +25 -0
  170. package/dist/runtime-routes.d.ts.map +1 -0
  171. package/dist/runtime-routes.js +189 -0
  172. package/dist/runtime-routes.js.map +1 -0
  173. package/dist/scripts/postinstall.d.ts +5 -6
  174. package/dist/scripts/postinstall.d.ts.map +1 -1
  175. package/dist/scripts/postinstall.js +36 -20
  176. package/dist/scripts/postinstall.js.map +1 -1
  177. package/dist/self-host-compose.d.ts +14 -0
  178. package/dist/self-host-compose.d.ts.map +1 -0
  179. package/dist/self-host-compose.js +236 -0
  180. package/dist/self-host-compose.js.map +1 -0
  181. package/dist/storage-provision.d.ts +24 -0
  182. package/dist/storage-provision.d.ts.map +1 -0
  183. package/dist/storage-provision.js +44 -0
  184. package/dist/storage-provision.js.map +1 -0
  185. package/dist/systemd.d.ts +26 -0
  186. package/dist/systemd.d.ts.map +1 -0
  187. package/dist/systemd.js +102 -0
  188. package/dist/systemd.js.map +1 -0
  189. package/dist/tsx-runner.d.ts.map +1 -1
  190. package/dist/tsx-runner.js +9 -2
  191. package/dist/tsx-runner.js.map +1 -1
  192. package/dist/type-extractor.d.ts +31 -0
  193. package/dist/type-extractor.d.ts.map +1 -0
  194. package/dist/type-extractor.js +876 -0
  195. package/dist/type-extractor.js.map +1 -0
  196. package/package.json +4 -3
  197. package/releases/deno/VERSION +1 -0
  198. package/scripts/mirror-deno-release.sh +76 -0
  199. package/src/app-config.ts +128 -0
  200. package/src/augmentation-generator.ts +126 -0
  201. package/src/binary-cache.ts +802 -0
  202. package/src/cli.ts +13 -8
  203. package/src/commands/admin.ts +4 -3
  204. package/src/commands/app.ts +67 -231
  205. package/src/commands/cache.ts +117 -0
  206. package/src/commands/cloud.ts +46 -57
  207. package/src/commands/db.ts +54 -63
  208. package/src/commands/deploy.ts +110 -61
  209. package/src/commands/dev.ts +930 -405
  210. package/src/commands/diff.ts +21 -29
  211. package/src/commands/engine.ts +13 -116
  212. package/src/commands/functions.ts +97 -115
  213. package/src/commands/generate.ts +23 -10
  214. package/src/commands/init.ts +136 -414
  215. package/src/commands/migrate-from-v1.ts +131 -0
  216. package/src/commands/migrate.ts +27 -23
  217. package/src/commands/pg.ts +133 -0
  218. package/src/commands/pull.ts +6 -85
  219. package/src/commands/push.ts +128 -59
  220. package/src/commands/seed.ts +54 -12
  221. package/src/commands/self-host.ts +312 -880
  222. package/src/commands/self-update.ts +45 -0
  223. package/src/commands/status.ts +4 -3
  224. package/src/commands/types.ts +76 -0
  225. package/src/commands/update.ts +92 -0
  226. package/src/components.ts +6 -0
  227. package/src/config.ts +127 -94
  228. package/src/docker-postgres.ts +138 -0
  229. package/src/engine-client.ts +231 -0
  230. package/src/ensure-binary.ts +28 -0
  231. package/src/functions-router-gen.ts +224 -0
  232. package/src/index.ts +4 -12
  233. package/src/kong-config.ts +78 -0
  234. package/src/local-gateway.ts +9 -0
  235. package/src/local-storage.ts +14 -0
  236. package/src/pgbouncer-userlist.ts +15 -0
  237. package/src/postgres-ctl.ts +171 -0
  238. package/src/process-manager.ts +151 -0
  239. package/src/project-config.ts +353 -0
  240. package/src/pull-utils.ts +24 -0
  241. package/src/release-pins.ts +31 -0
  242. package/src/release-public-key.ts +12 -0
  243. package/src/runtime-routes.ts +216 -0
  244. package/src/scripts/postinstall.ts +36 -25
  245. package/src/self-host-compose.ts +257 -0
  246. package/src/storage-provision.ts +58 -0
  247. package/src/systemd.ts +137 -0
  248. package/src/tsx-runner.ts +11 -1
  249. package/src/type-extractor.ts +1016 -0
  250. package/tests/app-command.test.ts +54 -0
  251. package/tests/augmentation-generator.test.ts +59 -0
  252. package/tests/binary-cache-cloud-overrides.test.ts +123 -0
  253. package/tests/cached-artifact-format.test.ts +84 -0
  254. package/tests/cli-help.test.ts +40 -14
  255. package/tests/config.test.ts +140 -37
  256. package/tests/engine-distribution.test.ts +3 -3
  257. package/tests/ensure-binary.test.ts +59 -0
  258. package/tests/init.test.ts +28 -86
  259. package/tests/migrate-from-v1.test.ts +29 -0
  260. package/tests/pg-spawn-env.test.ts +18 -0
  261. package/tests/postgres-archive-tag.test.ts +9 -0
  262. package/tests/pull-utils.test.ts +36 -1
  263. package/tests/release-pins.test.ts +28 -0
  264. package/tests/runtime-contract.test.ts +236 -0
  265. package/tests/seed-discover.test.ts +31 -0
  266. package/tests/tsconfig.json +9 -0
  267. package/tests/type-extractor.test.ts +401 -0
  268. package/tsconfig.tsbuildinfo +1 -1
  269. package/vitest.config.ts +12 -0
  270. package/dist/engine/cache.d.ts +0 -37
  271. package/dist/engine/cache.d.ts.map +0 -1
  272. package/dist/engine/cache.js +0 -121
  273. package/dist/engine/cache.js.map +0 -1
  274. package/dist/engine/download.d.ts +0 -19
  275. package/dist/engine/download.d.ts.map +0 -1
  276. package/dist/engine/download.js +0 -108
  277. package/dist/engine/download.js.map +0 -1
  278. package/dist/engine/platform.d.ts +0 -24
  279. package/dist/engine/platform.d.ts.map +0 -1
  280. package/dist/engine/platform.js +0 -50
  281. package/dist/engine/platform.js.map +0 -1
  282. package/dist/engine/resolve.d.ts +0 -37
  283. package/dist/engine/resolve.d.ts.map +0 -1
  284. package/dist/engine/resolve.js +0 -133
  285. package/dist/engine/resolve.js.map +0 -1
  286. package/dist/engine/update-notify.d.ts +0 -11
  287. package/dist/engine/update-notify.d.ts.map +0 -1
  288. package/dist/engine/update-notify.js +0 -43
  289. package/dist/engine/update-notify.js.map +0 -1
  290. package/dist/engine/verify.d.ts +0 -50
  291. package/dist/engine/verify.d.ts.map +0 -1
  292. package/dist/engine/verify.js +0 -161
  293. package/dist/engine/verify.js.map +0 -1
  294. package/dist/engine-version.d.ts +0 -35
  295. package/dist/engine-version.d.ts.map +0 -1
  296. package/dist/engine-version.js +0 -35
  297. package/dist/engine-version.js.map +0 -1
  298. package/dist/engine.d.ts +0 -34
  299. package/dist/engine.d.ts.map +0 -1
  300. package/dist/engine.js +0 -76
  301. package/dist/engine.js.map +0 -1
  302. package/src/engine/cache.ts +0 -135
  303. package/src/engine/download.ts +0 -143
  304. package/src/engine/platform.ts +0 -66
  305. package/src/engine/resolve.ts +0 -197
  306. package/src/engine/update-notify.ts +0 -50
  307. package/src/engine/verify.ts +0 -206
  308. package/src/engine-version.ts +0 -39
  309. package/src/engine.ts +0 -99
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
+ import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import { spawnSync } from "node:child_process"
6
+ import { resolve, dirname } from "node:path"
7
+ import { fileURLToPath } from "node:url"
8
+ import { scaffold } from "../src/commands/init.js"
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const CLI_BIN = resolve(__dirname, "../bin/supatype.js")
12
+
13
+ function runCli(cwd: string, args: string[]): { stdout: string; stderr: string; exitCode: number } {
14
+ const result = spawnSync(process.execPath, [CLI_BIN, ...args], {
15
+ encoding: "utf8",
16
+ cwd,
17
+ timeout: 10_000,
18
+ })
19
+ return {
20
+ stdout: String(result.stdout ?? ""),
21
+ stderr: String(result.stderr ?? ""),
22
+ exitCode: result.status ?? 1,
23
+ }
24
+ }
25
+
26
+ let tmpRoot: string
27
+
28
+ beforeEach(() => {
29
+ tmpRoot = join(tmpdir(), `dt-app-test-${Date.now()}`)
30
+ mkdirSync(tmpRoot, { recursive: true })
31
+ scaffold(tmpRoot, "app-test")
32
+ })
33
+
34
+ afterEach(() => {
35
+ rmSync(tmpRoot, { recursive: true, force: true })
36
+ })
37
+
38
+ describe("supatype app add --static", () => {
39
+ it("sets app.mode=static and static_dir in supatype.config.ts", () => {
40
+ const { exitCode } = runCli(tmpRoot, ["app", "add", "--static", "./public"])
41
+ expect(exitCode).toBe(0)
42
+ const config = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
43
+ expect(config).toContain('mode: "static"')
44
+ expect(config).toContain('static_dir: "./public"')
45
+ })
46
+
47
+ it("creates the static directory when missing", () => {
48
+ const siteDir = join(tmpRoot, "site")
49
+ expect(existsSync(siteDir)).toBe(false)
50
+ const { exitCode } = runCli(tmpRoot, ["app", "add", "--static", "./site"])
51
+ expect(exitCode).toBe(0)
52
+ expect(existsSync(siteDir)).toBe(true)
53
+ })
54
+ })
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { generateClientAugmentation } from "../src/augmentation-generator.js"
3
+
4
+ describe("generateClientAugmentation", () => {
5
+ it("emits deterministic output independent of model order", () => {
6
+ const astA = {
7
+ models: [
8
+ { tableName: "post", fields: { title: { kind: "text", required: true } } },
9
+ { tableName: "comment", fields: { body: { kind: "text", required: true } } },
10
+ ],
11
+ }
12
+ const astB = {
13
+ models: [
14
+ { tableName: "comment", fields: { body: { kind: "text", required: true } } },
15
+ { tableName: "post", fields: { title: { kind: "text", required: true } } },
16
+ ],
17
+ }
18
+
19
+ expect(generateClientAugmentation(astA)).toEqual(generateClientAugmentation(astB))
20
+ })
21
+
22
+ it("marks Insert fields optional when serverGenerated or default is set", () => {
23
+ const ast = {
24
+ models: [
25
+ {
26
+ tableName: "widget",
27
+ fields: {
28
+ id: { kind: "uuid", pgType: "UUID", required: true, primaryKey: true, default: { kind: "genRandomUuid" } },
29
+ name: { kind: "text", pgType: "TEXT", required: true },
30
+ created_at: { kind: "text", pgType: "TEXT", required: true, serverGenerated: true },
31
+ },
32
+ },
33
+ ],
34
+ }
35
+ const out = generateClientAugmentation(ast)
36
+ const insertOnly = out.split("Update:")[0] ?? out
37
+ expect(insertOnly).toContain("id?:")
38
+ expect(insertOnly).toContain("created_at?:")
39
+ expect(insertOnly).toContain("name: string")
40
+ expect(insertOnly).not.toContain("name?:")
41
+ })
42
+
43
+ it("types richText fields as SerializedEditorState", () => {
44
+ const ast = {
45
+ models: [
46
+ {
47
+ tableName: "note",
48
+ fields: {
49
+ id: { kind: "uuid", pgType: "UUID", required: true, primaryKey: true, default: { kind: "genRandomUuid" } },
50
+ body: { kind: "richText", pgType: "JSONB", required: true },
51
+ },
52
+ },
53
+ ],
54
+ }
55
+ const out = generateClientAugmentation(ast)
56
+ expect(out).toContain('import("@supatype/types/lexical").SerializedEditorState')
57
+ expect(out).not.toMatch(/body: Record<string, unknown>/)
58
+ })
59
+ })
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import {
6
+ hasMeaningfulOverrides,
7
+ isLinkedToCloudProject,
8
+ resolveBinary,
9
+ VERSION_PIN_LOCAL,
10
+ describeActiveOverrides,
11
+ } from "../src/binary-cache.js"
12
+ import type { SupatypeProjectConfig } from "../src/project-config.js"
13
+
14
+ function baseConfig(overrides?: SupatypeProjectConfig["overrides"]): SupatypeProjectConfig {
15
+ return {
16
+ project: { name: "t" },
17
+ database: { provider: "native" },
18
+ server: { mode: "dev" },
19
+ app: { mode: "none" },
20
+ versions: { engine: "0.1.0", server: "0.1.0", postgres: "17", deno: "2.0.0" },
21
+ ...(overrides !== undefined ? { overrides } : {}),
22
+ }
23
+ }
24
+
25
+ let tmp: string
26
+ beforeEach(() => {
27
+ tmp = join(tmpdir(), `dt-bc-${Date.now()}`)
28
+ mkdirSync(join(tmp, ".supatype"), { recursive: true })
29
+ })
30
+ afterEach(() => {
31
+ rmSync(tmp, { recursive: true, force: true })
32
+ })
33
+
34
+ describe("hasMeaningfulOverrides", () => {
35
+ it("is false without overrides", () => {
36
+ expect(hasMeaningfulOverrides(baseConfig())).toBe(false)
37
+ })
38
+ it("is false for empty strings only", () => {
39
+ expect(hasMeaningfulOverrides(baseConfig({ engine: " " }))).toBe(false)
40
+ })
41
+ it("is true when any path is non-empty", () => {
42
+ expect(hasMeaningfulOverrides(baseConfig({ engine: "/x/y" }))).toBe(true)
43
+ })
44
+ })
45
+
46
+ describe("isLinkedToCloudProject", () => {
47
+ it("is true when project.ref is set", () => {
48
+ const cfg = baseConfig()
49
+ cfg.project.ref = "my-slug"
50
+ expect(isLinkedToCloudProject(tmp, cfg)).toBe(true)
51
+ })
52
+ it("is true when .supatype/cloud.json has projectSlug", () => {
53
+ writeFileSync(
54
+ join(tmp, ".supatype", "cloud.json"),
55
+ JSON.stringify({ apiUrl: "x", token: "t", projectSlug: "p" }),
56
+ )
57
+ expect(isLinkedToCloudProject(tmp, baseConfig())).toBe(true)
58
+ })
59
+ it("is true when .supatype/linked.json has ref", () => {
60
+ writeFileSync(join(tmp, ".supatype", "linked.json"), JSON.stringify({ ref: "r1" }))
61
+ expect(isLinkedToCloudProject(tmp, baseConfig())).toBe(true)
62
+ })
63
+ it("is false with token-only cloud.json", () => {
64
+ writeFileSync(join(tmp, ".supatype", "cloud.json"), JSON.stringify({ apiUrl: "x", token: "t" }))
65
+ expect(isLinkedToCloudProject(tmp, baseConfig())).toBe(false)
66
+ })
67
+ })
68
+
69
+ describe("resolveBinary + cloud link", () => {
70
+ const prevCwd = process.cwd()
71
+
72
+ beforeEach(() => {
73
+ process.chdir(tmp)
74
+ })
75
+ afterEach(() => {
76
+ process.chdir(prevCwd)
77
+ })
78
+
79
+ it("rejects any meaningful overrides when cloud.json is linked", async () => {
80
+ writeFileSync(
81
+ join(tmp, ".supatype", "cloud.json"),
82
+ JSON.stringify({ apiUrl: "x", token: "t", projectSlug: "p" }),
83
+ )
84
+ const cfg = baseConfig({ studio: "/does/not/need/to/exist/for/early/throw" })
85
+ await expect(resolveBinary("engine", cfg)).rejects.toThrow(/linked to Supatype Cloud/)
86
+ })
87
+ })
88
+
89
+ describe("VERSION_PIN_LOCAL + resolveBinary", () => {
90
+ const prevCwd = process.cwd()
91
+
92
+ beforeEach(() => {
93
+ process.chdir(tmp)
94
+ })
95
+ afterEach(() => {
96
+ process.chdir(prevCwd)
97
+ })
98
+
99
+ it("throws when versions.engine is local without overrides.engine", async () => {
100
+ const cfg = baseConfig()
101
+ cfg.versions.engine = VERSION_PIN_LOCAL
102
+ await expect(resolveBinary("engine", cfg)).rejects.toThrow(/overrides\.engine/)
103
+ })
104
+
105
+ it("uses overrides.engine when versions.engine is local", async () => {
106
+ const fakeBin = join(tmp, "supatype-engine-local")
107
+ writeFileSync(fakeBin, "#")
108
+ const cfg = baseConfig({ engine: fakeBin })
109
+ cfg.versions.engine = VERSION_PIN_LOCAL
110
+ const p = await resolveBinary("engine", cfg)
111
+ expect(p.replace(/\\/g, "/")).toContain("supatype-engine-local")
112
+ })
113
+ })
114
+
115
+ describe("describeActiveOverrides", () => {
116
+ it("lists non-empty override paths", () => {
117
+ const lines = describeActiveOverrides(
118
+ baseConfig({ engine: "/a/b", server: "/c/d" }),
119
+ )
120
+ expect(lines.some((l) => l.includes("engine"))).toBe(true)
121
+ expect(lines.some((l) => l.includes("/a/b"))).toBe(true)
122
+ })
123
+ })
@@ -0,0 +1,84 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import {
6
+ validateArtifactFormat,
7
+ type Component,
8
+ type PlatformId,
9
+ } from "../src/binary-cache.js"
10
+
11
+ function writeMagic(name: string, bytes: number[]): string {
12
+ const dir = mkdtempSync(join(tmpdir(), "supatype-artifact-"))
13
+ const path = join(dir, name)
14
+ writeFileSync(path, Buffer.from(bytes))
15
+ return path
16
+ }
17
+
18
+ function expectValid(component: Component, path: string, platform: PlatformId): void {
19
+ expect(() => validateArtifactFormat(component, path, platform)).not.toThrow()
20
+ }
21
+
22
+ function expectInvalid(component: Component, path: string, platform: PlatformId): void {
23
+ expect(() => validateArtifactFormat(component, path, platform)).toThrow()
24
+ }
25
+
26
+ describe("validateArtifactFormat", () => {
27
+ const paths: string[] = []
28
+
29
+ afterEach(() => {
30
+ for (const p of paths) {
31
+ try {
32
+ rmSync(join(p, ".."), { recursive: true, force: true })
33
+ } catch {
34
+ /* ignore */
35
+ }
36
+ }
37
+ paths.length = 0
38
+ })
39
+
40
+ it("accepts ELF for engine/server/deno on linux", () => {
41
+ const linux: PlatformId = { os: "linux", arch: "amd64" }
42
+ for (const component of ["engine", "server", "deno"] as const) {
43
+ const p = writeMagic(component, [0x7f, 0x45, 0x4c, 0x46, 0, 0, 0, 0])
44
+ paths.push(p)
45
+ expectValid(component, p, linux)
46
+ }
47
+ })
48
+
49
+ it("accepts Mach-O for engine on darwin", () => {
50
+ const p = writeMagic("engine", [0xcf, 0xfa, 0xed, 0xfe, 0, 0, 0, 0])
51
+ paths.push(p)
52
+ expectValid("engine", p, { os: "darwin", arch: "arm64" })
53
+ })
54
+
55
+ it("accepts PE for deno on windows", () => {
56
+ const p = writeMagic("deno.exe", [0x4d, 0x5a, 0x90, 0x00, 0, 0, 0, 0])
57
+ paths.push(p)
58
+ expectValid("deno", p, { os: "windows", arch: "amd64" })
59
+ })
60
+
61
+ it("rejects Go c-archive for server on linux", () => {
62
+ const p = writeMagic("server", [0x21, 0x3c, 0x61, 0x72, 0, 0, 0, 0])
63
+ paths.push(p)
64
+ expectInvalid("server", p, { os: "linux", arch: "amd64" })
65
+ })
66
+
67
+ it("accepts gzip for postgres on linux", () => {
68
+ const p = writeMagic("pg.tar.gz", [0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0])
69
+ paths.push(p)
70
+ expectValid("postgres", p, { os: "linux", arch: "amd64" })
71
+ })
72
+
73
+ it("accepts zip for postgres on windows", () => {
74
+ const p = writeMagic("pg.zip", [0x50, 0x4b, 0x03, 0x04, 0, 0, 0, 0])
75
+ paths.push(p)
76
+ expectValid("postgres", p, { os: "windows", arch: "amd64" })
77
+ })
78
+
79
+ it("rejects gzip postgres artifact on windows", () => {
80
+ const p = writeMagic("pg.tar.gz", [0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0])
81
+ paths.push(p)
82
+ expectInvalid("postgres", p, { os: "windows", arch: "amd64" })
83
+ })
84
+ })
@@ -27,7 +27,22 @@ describe("CLI binary (requires built dist/)", () => {
27
27
  it("--help lists all top-level commands", () => {
28
28
  const { stdout, exitCode } = runCli(["--help"])
29
29
  expect(exitCode).toBe(0)
30
- const commands = ["init", "dev", "push", "diff", "pull", "generate", "migrate", "rollback", "reset", "seed", "keys", "app", "self-host"]
30
+ const commands = [
31
+ "init",
32
+ "dev",
33
+ "push",
34
+ "diff",
35
+ "pull",
36
+ "generate",
37
+ "migrate",
38
+ "rollback",
39
+ "reset",
40
+ "seed",
41
+ "keys",
42
+ "app",
43
+ "self-host",
44
+ "self-update",
45
+ ]
31
46
  for (const cmd of commands) {
32
47
  expect(stdout, `Expected '${cmd}' in --help output`).toContain(cmd)
33
48
  }
@@ -39,6 +54,12 @@ describe("CLI binary (requires built dist/)", () => {
39
54
  expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/)
40
55
  })
41
56
 
57
+ it("self-update --help describes the command", () => {
58
+ const { stdout, exitCode } = runCli(["self-update", "--help"])
59
+ expect(exitCode).toBe(0)
60
+ expect(stdout.toLowerCase()).toMatch(/npm|update/)
61
+ })
62
+
42
63
  it("init --help describes the init command", () => {
43
64
  const { stdout, exitCode } = runCli(["init", "--help"])
44
65
  expect(exitCode).toBe(0)
@@ -58,10 +79,10 @@ describe("CLI binary (requires built dist/)", () => {
58
79
  expect(stdout).toContain("dry run")
59
80
  })
60
81
 
61
- it("pull --help shows --output option", () => {
82
+ it("pull --help describes deprecated pull command", () => {
62
83
  const { stdout, exitCode } = runCli(["pull", "--help"])
63
84
  expect(exitCode).toBe(0)
64
- expect(stdout).toContain("--output")
85
+ expect(stdout).toContain("deprecated")
65
86
  })
66
87
 
67
88
  it("reset --help shows --yes flag", () => {
@@ -77,27 +98,32 @@ describe("CLI binary (requires built dist/)", () => {
77
98
  expect(stdout).toContain("remove")
78
99
  })
79
100
 
80
- it("app add --help shows --dockerfile and --port options", () => {
101
+ it("app add --help shows --static and --port options", () => {
81
102
  const { stdout, exitCode } = runCli(["app", "add", "--help"])
82
103
  expect(exitCode).toBe(0)
83
- expect(stdout).toContain("--dockerfile")
104
+ expect(stdout).toContain("--static")
84
105
  expect(stdout).toContain("--port")
85
106
  })
86
107
 
87
- it("self-host --help describes the self-host command", () => {
108
+ it("self-host --help shows compose only (legacy commands hidden)", () => {
88
109
  const { stdout, exitCode } = runCli(["self-host", "--help"])
89
110
  expect(exitCode).toBe(0)
90
- expect(stdout).toContain("setup")
91
- expect(stdout).toContain("status")
92
- expect(stdout).toContain("logs")
93
- expect(stdout).toContain("backup")
94
- expect(stdout).toContain("update")
111
+ const commandsSection = stdout.split("Commands:")[1] ?? ""
112
+ expect(commandsSection).toContain("compose")
113
+ expect(commandsSection).not.toContain("install-service")
114
+ expect(commandsSection).not.toContain("native")
115
+ expect(commandsSection).not.toContain("backup")
116
+ expect(stdout).not.toContain("--output-dir")
95
117
  })
96
118
 
97
- it("self-host setup --help shows --domain option", () => {
98
- const { stdout, exitCode } = runCli(["self-host", "setup", "--help"])
119
+ it("self-host compose --help shows compose subcommands", () => {
120
+ const { stdout, exitCode } = runCli(["self-host", "compose", "--help"])
99
121
  expect(exitCode).toBe(0)
100
- expect(stdout).toContain("--domain")
122
+ expect(stdout).toContain("render")
123
+ expect(stdout).toContain("up")
124
+ expect(stdout).toContain("down")
125
+ expect(stdout).toContain("status")
126
+ expect(stdout).toContain("logs")
101
127
  })
102
128
 
103
129
  it("unknown command exits non-zero", () => {
@@ -3,13 +3,26 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { tmpdir } from "node:os"
5
5
  import { defineConfig, loadConfig } from "../src/config.js"
6
- import type { DefinatypeConfig } from "../src/config.js"
6
+ import { mergeProjectConfig, type SupatypeProjectConfig } from "../src/project-config.js"
7
+ import { DENO_RELEASE_PIN } from "../src/release-pins.js"
7
8
 
8
9
  let counter = 0
9
10
  let tmpDir: string
10
11
 
12
+ const minimalProject = (name: string): SupatypeProjectConfig => ({
13
+ project: { name },
14
+ database: { provider: "docker" },
15
+ server: { mode: "dev" },
16
+ app: { mode: "none" },
17
+ versions: {
18
+ engine: "0.4.2",
19
+ server: "0.1.0",
20
+ postgres: "17.2",
21
+ deno: DENO_RELEASE_PIN,
22
+ },
23
+ })
24
+
11
25
  beforeEach(() => {
12
- // Counter prevents timestamp collisions when tests run in rapid succession
13
26
  tmpDir = join(tmpdir(), `dt-config-test-${Date.now()}-${++counter}`)
14
27
  mkdirSync(tmpDir, { recursive: true })
15
28
  })
@@ -20,17 +33,18 @@ afterEach(() => {
20
33
 
21
34
  describe("defineConfig()", () => {
22
35
  it("returns its argument unchanged (identity helper for type inference)", () => {
23
- const cfg: DefinatypeConfig = {
36
+ const cfg = defineConfig({
37
+ ...minimalProject("test"),
24
38
  connection: "postgresql://localhost/test",
25
- schema: "./schema/index.ts",
26
- }
39
+ schema: { path: "./schema/index.ts" },
40
+ })
27
41
  expect(defineConfig(cfg)).toBe(cfg)
28
42
  })
29
43
 
30
44
  it("preserves optional output field", () => {
31
45
  const cfg = defineConfig({
32
- connection: "postgresql://localhost/test",
33
- schema: "./schema/index.ts",
46
+ ...minimalProject("test"),
47
+ schema: { path: "./schema/index.ts" },
34
48
  output: { types: "./src/types.d.ts", client: "./src/client.ts" },
35
49
  })
36
50
  expect(cfg.output?.types).toBe("./src/types.d.ts")
@@ -40,62 +54,83 @@ describe("defineConfig()", () => {
40
54
 
41
55
  describe("loadConfig()", () => {
42
56
  it("throws when no config file exists in the directory", () => {
43
- expect(() => loadConfig(tmpDir)).toThrow(/No supatype.config.ts found/)
57
+ expect(() => loadConfig(tmpDir)).toThrow(/No supatype.config.ts/)
44
58
  })
45
59
 
46
60
  it("loads a valid supatype.config.ts (plain export default, no package imports)", () => {
47
- // Note: avoid importing @supatype/cli inside the temp config —
48
- // the temp dir is outside the monorepo and can't resolve workspace packages.
61
+ const body = {
62
+ ...minimalProject("mydb"),
63
+ connection: "postgresql://localhost/mydb",
64
+ schema: { path: "./schema/index.ts" },
65
+ }
49
66
  writeFileSync(
50
67
  join(tmpDir, "supatype.config.ts"),
51
- `export default {
52
- connection: "postgresql://localhost/mydb",
53
- schema: "./schema/index.ts",
54
- }
68
+ `export default ${JSON.stringify(body)}
55
69
  `,
56
70
  )
57
71
  const cfg = loadConfig(tmpDir)
58
72
  expect(cfg.connection).toBe("postgresql://localhost/mydb")
59
- expect(cfg.schema).toBe("./schema/index.ts")
73
+ expect(cfg.schema?.path).toBe("./schema/index.ts")
60
74
  })
61
75
 
62
- it("loads a supatype.config.js (plain ESM)", () => {
76
+ it("normalizes shorthand schema string to { path, pg_schema }", () => {
63
77
  writeFileSync(
64
- join(tmpDir, "supatype.config.js"),
65
- `export default {
66
- connection: "postgresql://localhost/jsdb",
67
- schema: "./schema/index.ts",
68
- }
78
+ join(tmpDir, "supatype.config.ts"),
79
+ `export default ${JSON.stringify({
80
+ ...minimalProject("x"),
81
+ schema: "./schema/index.ts",
82
+ })}
69
83
  `,
70
84
  )
71
85
  const cfg = loadConfig(tmpDir)
72
- expect(cfg.connection).toBe("postgresql://localhost/jsdb")
86
+ expect(cfg.schema?.path).toBe("./schema/index.ts")
87
+ expect(cfg.schema?.pg_schema).toBe("public")
73
88
  })
74
89
 
75
- it("throws if config is missing the schema field", () => {
90
+ it("loads a supatype.config.js (plain ESM)", () => {
76
91
  writeFileSync(
77
- join(tmpDir, "supatype.config.ts"),
78
- `export default { connection: "postgresql://localhost/test" }`,
92
+ join(tmpDir, "supatype.config.js"),
93
+ `export default ${JSON.stringify({
94
+ ...minimalProject("jsdb"),
95
+ connection: "postgresql://localhost/jsdb",
96
+ schema: { path: "./schema/index.ts" },
97
+ })}
98
+ `,
79
99
  )
80
- expect(() => loadConfig(tmpDir)).toThrow(/must export/)
100
+ const cfg = loadConfig(tmpDir)
101
+ expect(cfg.connection).toBe("postgresql://localhost/jsdb")
81
102
  })
82
103
 
83
- it("throws if config is missing the connection field", () => {
104
+ it("throws if config is missing the versions section", () => {
84
105
  writeFileSync(
85
106
  join(tmpDir, "supatype.config.ts"),
86
- `export default { schema: "./schema/index.ts" }`,
107
+ `export default {
108
+ project: { name: "t" },
109
+ database: { provider: "docker" },
110
+ server: { mode: "dev" },
111
+ app: { mode: "none" },
112
+ schema: { path: "./schema/index.ts" },
113
+ }`,
87
114
  )
88
- expect(() => loadConfig(tmpDir)).toThrow(/must export/)
115
+ expect(() => loadConfig(tmpDir)).toThrow(/versions/)
89
116
  })
90
117
 
91
118
  it("prefers supatype.config.ts over supatype.config.js when both exist", () => {
92
119
  writeFileSync(
93
120
  join(tmpDir, "supatype.config.ts"),
94
- `export default { connection: "postgresql://localhost/from-ts", schema: "./schema/index.ts" }`,
121
+ `export default ${JSON.stringify({
122
+ ...minimalProject("from-ts"),
123
+ connection: "postgresql://localhost/from-ts",
124
+ schema: { path: "./schema/index.ts" },
125
+ })}`,
95
126
  )
96
127
  writeFileSync(
97
128
  join(tmpDir, "supatype.config.js"),
98
- `export default { connection: "postgresql://localhost/from-js", schema: "./schema/index.ts" }`,
129
+ `export default ${JSON.stringify({
130
+ ...minimalProject("from-js"),
131
+ connection: "postgresql://localhost/from-js",
132
+ schema: { path: "./schema/index.ts" },
133
+ })}`,
99
134
  )
100
135
  const cfg = loadConfig(tmpDir)
101
136
  expect(cfg.connection).toContain("from-ts")
@@ -104,14 +139,82 @@ describe("loadConfig()", () => {
104
139
  it("supports optional output field in config", () => {
105
140
  writeFileSync(
106
141
  join(tmpDir, "supatype.config.ts"),
107
- `export default {
108
- connection: "postgresql://localhost/test",
109
- schema: "./schema/index.ts",
110
- output: { types: "./src/types.d.ts" },
111
- }
112
- `,
142
+ `export default ${JSON.stringify({
143
+ ...minimalProject("t"),
144
+ schema: { path: "./schema/index.ts" },
145
+ output: { types: "./src/types.d.ts" },
146
+ })}`,
113
147
  )
114
148
  const cfg = loadConfig(tmpDir)
115
149
  expect(cfg.output?.types).toBe("./src/types.d.ts")
116
150
  })
151
+
152
+ it("merges supatype.local.config.ts over base", () => {
153
+ writeFileSync(
154
+ join(tmpDir, "supatype.config.ts"),
155
+ `export default ${JSON.stringify({
156
+ ...minimalProject("base"),
157
+ schema: { path: "./a.ts" },
158
+ versions: {
159
+ engine: "0.4.0",
160
+ server: "0.1.0",
161
+ postgres: "17",
162
+ deno: "2.2.0",
163
+ },
164
+ })}`,
165
+ )
166
+ writeFileSync(
167
+ join(tmpDir, "supatype.local.config.ts"),
168
+ `export default ${JSON.stringify({
169
+ versions: {
170
+ engine: "0.4.2",
171
+ server: "0.1.0",
172
+ postgres: "17",
173
+ deno: "2.2.0",
174
+ },
175
+ })}`,
176
+ )
177
+ const cfg = loadConfig(tmpDir)
178
+ expect(cfg.versions.engine).toBe("0.4.2")
179
+ expect(cfg.schema?.path).toBe("./a.ts")
180
+ })
181
+ })
182
+
183
+ describe("mergeProjectConfig()", () => {
184
+ it("deep-merges email.smtp between base and local", () => {
185
+ const base = defineConfig({
186
+ ...minimalProject("p"),
187
+ email: { provider: "smtp", smtp: { host: "h1", port: 587, user: "u0" } },
188
+ })
189
+ const merged = mergeProjectConfig(base, { email: { smtp: { host: "h2", pass: "x" } } })
190
+ expect(merged.email?.provider).toBe("smtp")
191
+ expect(merged.email?.smtp?.host).toBe("h2")
192
+ expect(merged.email?.smtp?.port).toBe(587)
193
+ expect(merged.email?.smtp?.user).toBe("u0")
194
+ expect(merged.email?.smtp?.pass).toBe("x")
195
+ })
196
+
197
+ it("merges send_email_hook fields from local", () => {
198
+ const base = defineConfig({
199
+ ...minimalProject("p"),
200
+ email: { provider: "console", send_email_hook: true, send_email_hook_uri: "http://old/hook" },
201
+ })
202
+ const merged = mergeProjectConfig(base, {
203
+ email: { send_email_hook_secrets: "v1,whsec_customsecret0000000000000000" },
204
+ })
205
+ expect(merged.email?.send_email_hook).toBe(true)
206
+ expect(merged.email?.send_email_hook_uri).toBe("http://old/hook")
207
+ expect(merged.email?.send_email_hook_secrets).toBe("v1,whsec_customsecret0000000000000000")
208
+ })
209
+
210
+ it("overrides app.vite_dev_url from local", () => {
211
+ const base = defineConfig({
212
+ ...minimalProject("p"),
213
+ app: { mode: "static", static_dir: "./dist", vite_dev_url: "http://127.0.0.1:1111" },
214
+ })
215
+ const merged = mergeProjectConfig(base, { app: { vite_dev_url: "http://127.0.0.1:5173" } })
216
+ expect(merged.app.vite_dev_url).toBe("http://127.0.0.1:5173")
217
+ expect(merged.app.mode).toBe("static")
218
+ expect(merged.app.static_dir).toBe("./dist")
219
+ })
117
220
  })