@supatype/cli 0.1.0-alpha.6

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 (200) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +7 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/bin/dev-entry.ts +2 -0
  5. package/bin/supatype.js +5 -0
  6. package/dist/app/framework.d.ts +44 -0
  7. package/dist/app/framework.d.ts.map +1 -0
  8. package/dist/app/framework.js +200 -0
  9. package/dist/app/framework.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +55 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/admin.d.ts +4 -0
  15. package/dist/commands/admin.d.ts.map +1 -0
  16. package/dist/commands/admin.js +270 -0
  17. package/dist/commands/admin.js.map +1 -0
  18. package/dist/commands/app.d.ts +3 -0
  19. package/dist/commands/app.d.ts.map +1 -0
  20. package/dist/commands/app.js +235 -0
  21. package/dist/commands/app.js.map +1 -0
  22. package/dist/commands/cloud.d.ts +3 -0
  23. package/dist/commands/cloud.d.ts.map +1 -0
  24. package/dist/commands/cloud.js +256 -0
  25. package/dist/commands/cloud.js.map +1 -0
  26. package/dist/commands/db.d.ts +8 -0
  27. package/dist/commands/db.d.ts.map +1 -0
  28. package/dist/commands/db.js +123 -0
  29. package/dist/commands/db.js.map +1 -0
  30. package/dist/commands/deploy-types.d.ts +14 -0
  31. package/dist/commands/deploy-types.d.ts.map +1 -0
  32. package/dist/commands/deploy-types.js +38 -0
  33. package/dist/commands/deploy-types.js.map +1 -0
  34. package/dist/commands/deploy.d.ts +14 -0
  35. package/dist/commands/deploy.d.ts.map +1 -0
  36. package/dist/commands/deploy.js +295 -0
  37. package/dist/commands/deploy.js.map +1 -0
  38. package/dist/commands/dev.d.ts +3 -0
  39. package/dist/commands/dev.d.ts.map +1 -0
  40. package/dist/commands/dev.js +428 -0
  41. package/dist/commands/dev.js.map +1 -0
  42. package/dist/commands/diff.d.ts +3 -0
  43. package/dist/commands/diff.d.ts.map +1 -0
  44. package/dist/commands/diff.js +39 -0
  45. package/dist/commands/diff.js.map +1 -0
  46. package/dist/commands/engine.d.ts +9 -0
  47. package/dist/commands/engine.d.ts.map +1 -0
  48. package/dist/commands/engine.js +99 -0
  49. package/dist/commands/engine.js.map +1 -0
  50. package/dist/commands/functions.d.ts +3 -0
  51. package/dist/commands/functions.d.ts.map +1 -0
  52. package/dist/commands/functions.js +762 -0
  53. package/dist/commands/functions.js.map +1 -0
  54. package/dist/commands/generate.d.ts +3 -0
  55. package/dist/commands/generate.d.ts.map +1 -0
  56. package/dist/commands/generate.js +28 -0
  57. package/dist/commands/generate.js.map +1 -0
  58. package/dist/commands/init.d.ts +7 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +515 -0
  61. package/dist/commands/init.js.map +1 -0
  62. package/dist/commands/keys.d.ts +4 -0
  63. package/dist/commands/keys.d.ts.map +1 -0
  64. package/dist/commands/keys.js +57 -0
  65. package/dist/commands/keys.js.map +1 -0
  66. package/dist/commands/logs.d.ts +6 -0
  67. package/dist/commands/logs.d.ts.map +1 -0
  68. package/dist/commands/logs.js +52 -0
  69. package/dist/commands/logs.js.map +1 -0
  70. package/dist/commands/migrate.d.ts +3 -0
  71. package/dist/commands/migrate.d.ts.map +1 -0
  72. package/dist/commands/migrate.js +71 -0
  73. package/dist/commands/migrate.js.map +1 -0
  74. package/dist/commands/plugins.d.ts +3 -0
  75. package/dist/commands/plugins.d.ts.map +1 -0
  76. package/dist/commands/plugins.js +431 -0
  77. package/dist/commands/plugins.js.map +1 -0
  78. package/dist/commands/pull.d.ts +3 -0
  79. package/dist/commands/pull.d.ts.map +1 -0
  80. package/dist/commands/pull.js +73 -0
  81. package/dist/commands/pull.js.map +1 -0
  82. package/dist/commands/push.d.ts +3 -0
  83. package/dist/commands/push.d.ts.map +1 -0
  84. package/dist/commands/push.js +87 -0
  85. package/dist/commands/push.js.map +1 -0
  86. package/dist/commands/seed.d.ts +3 -0
  87. package/dist/commands/seed.d.ts.map +1 -0
  88. package/dist/commands/seed.js +22 -0
  89. package/dist/commands/seed.js.map +1 -0
  90. package/dist/commands/self-host.d.ts +3 -0
  91. package/dist/commands/self-host.d.ts.map +1 -0
  92. package/dist/commands/self-host.js +796 -0
  93. package/dist/commands/self-host.js.map +1 -0
  94. package/dist/commands/status.d.ts +6 -0
  95. package/dist/commands/status.d.ts.map +1 -0
  96. package/dist/commands/status.js +69 -0
  97. package/dist/commands/status.js.map +1 -0
  98. package/dist/config.d.ts +106 -0
  99. package/dist/config.d.ts.map +1 -0
  100. package/dist/config.js +66 -0
  101. package/dist/config.js.map +1 -0
  102. package/dist/engine/cache.d.ts +37 -0
  103. package/dist/engine/cache.d.ts.map +1 -0
  104. package/dist/engine/cache.js +121 -0
  105. package/dist/engine/cache.js.map +1 -0
  106. package/dist/engine/download.d.ts +19 -0
  107. package/dist/engine/download.d.ts.map +1 -0
  108. package/dist/engine/download.js +108 -0
  109. package/dist/engine/download.js.map +1 -0
  110. package/dist/engine/platform.d.ts +24 -0
  111. package/dist/engine/platform.d.ts.map +1 -0
  112. package/dist/engine/platform.js +50 -0
  113. package/dist/engine/platform.js.map +1 -0
  114. package/dist/engine/resolve.d.ts +37 -0
  115. package/dist/engine/resolve.d.ts.map +1 -0
  116. package/dist/engine/resolve.js +133 -0
  117. package/dist/engine/resolve.js.map +1 -0
  118. package/dist/engine/update-notify.d.ts +11 -0
  119. package/dist/engine/update-notify.d.ts.map +1 -0
  120. package/dist/engine/update-notify.js +43 -0
  121. package/dist/engine/update-notify.js.map +1 -0
  122. package/dist/engine/verify.d.ts +50 -0
  123. package/dist/engine/verify.d.ts.map +1 -0
  124. package/dist/engine/verify.js +161 -0
  125. package/dist/engine/verify.js.map +1 -0
  126. package/dist/engine-version.d.ts +35 -0
  127. package/dist/engine-version.d.ts.map +1 -0
  128. package/dist/engine-version.js +35 -0
  129. package/dist/engine-version.js.map +1 -0
  130. package/dist/engine.d.ts +34 -0
  131. package/dist/engine.d.ts.map +1 -0
  132. package/dist/engine.js +76 -0
  133. package/dist/engine.js.map +1 -0
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +10 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/jwt.d.ts +3 -0
  139. package/dist/jwt.d.ts.map +1 -0
  140. package/dist/jwt.js +13 -0
  141. package/dist/jwt.js.map +1 -0
  142. package/dist/pull-utils.d.ts +16 -0
  143. package/dist/pull-utils.d.ts.map +1 -0
  144. package/dist/pull-utils.js +65 -0
  145. package/dist/pull-utils.js.map +1 -0
  146. package/dist/scripts/postinstall.d.ts +12 -0
  147. package/dist/scripts/postinstall.d.ts.map +1 -0
  148. package/dist/scripts/postinstall.js +31 -0
  149. package/dist/scripts/postinstall.js.map +1 -0
  150. package/dist/tsx-runner.d.ts +18 -0
  151. package/dist/tsx-runner.d.ts.map +1 -0
  152. package/dist/tsx-runner.js +62 -0
  153. package/dist/tsx-runner.js.map +1 -0
  154. package/package.json +36 -0
  155. package/src/app/framework.ts +249 -0
  156. package/src/cli.ts +58 -0
  157. package/src/commands/admin.ts +371 -0
  158. package/src/commands/app.ts +261 -0
  159. package/src/commands/cloud.ts +326 -0
  160. package/src/commands/db.ts +145 -0
  161. package/src/commands/deploy-types.ts +49 -0
  162. package/src/commands/deploy.ts +366 -0
  163. package/src/commands/dev.ts +477 -0
  164. package/src/commands/diff.ts +61 -0
  165. package/src/commands/engine.ts +133 -0
  166. package/src/commands/functions.ts +919 -0
  167. package/src/commands/generate.ts +31 -0
  168. package/src/commands/init.ts +532 -0
  169. package/src/commands/keys.ts +66 -0
  170. package/src/commands/logs.ts +58 -0
  171. package/src/commands/migrate.ts +83 -0
  172. package/src/commands/plugins.ts +508 -0
  173. package/src/commands/pull.ts +96 -0
  174. package/src/commands/push.ts +119 -0
  175. package/src/commands/seed.ts +26 -0
  176. package/src/commands/self-host.ts +932 -0
  177. package/src/commands/status.ts +83 -0
  178. package/src/config.ts +190 -0
  179. package/src/engine/cache.ts +135 -0
  180. package/src/engine/download.ts +143 -0
  181. package/src/engine/platform.ts +66 -0
  182. package/src/engine/resolve.ts +197 -0
  183. package/src/engine/update-notify.ts +50 -0
  184. package/src/engine/verify.ts +206 -0
  185. package/src/engine-version.ts +39 -0
  186. package/src/engine.ts +99 -0
  187. package/src/index.ts +19 -0
  188. package/src/jwt.ts +14 -0
  189. package/src/pull-utils.ts +57 -0
  190. package/src/scripts/postinstall.ts +40 -0
  191. package/src/tsx-runner.ts +79 -0
  192. package/tests/cli-help.test.ts +107 -0
  193. package/tests/config.test.ts +117 -0
  194. package/tests/engine-distribution.test.ts +418 -0
  195. package/tests/init.test.ts +184 -0
  196. package/tests/keys.test.ts +160 -0
  197. package/tests/pull-utils.test.ts +115 -0
  198. package/tests/tsx-runner.test.ts +66 -0
  199. package/tsconfig.json +10 -0
  200. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Integration tests for engine binary distribution (Phase 0.5).
3
+ *
4
+ * Tests cover:
5
+ * - Platform detection
6
+ * - Cache management
7
+ * - Download, checksum, and signature verification
8
+ * - Version compatibility
9
+ * - Offline mode
10
+ * - Error handling
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
14
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, chmodSync } from "node:fs"
15
+ import { join } from "node:path"
16
+ import { tmpdir, homedir } from "node:os"
17
+ import { createHash } from "node:crypto"
18
+
19
+ // ── Platform detection ───────────────────────────────────────────────
20
+
21
+ describe("Platform detection", () => {
22
+ it("detects supported platforms", async () => {
23
+ const { detectPlatform } = await import("../src/engine/platform.js")
24
+ // This test runs on the current platform — just verify it doesn't throw
25
+ const platform = detectPlatform()
26
+ expect(platform.os).toMatch(/^(linux|darwin|win)$/)
27
+ expect(platform.arch).toMatch(/^(x64|arm64)$/)
28
+ expect(platform.binaryName).toContain("supatype-engine")
29
+ })
30
+
31
+ it("builds correct artifact names", async () => {
32
+ const { getArtifactName } = await import("../src/engine/platform.js")
33
+
34
+ expect(
35
+ getArtifactName("0.1.0-alpha.1", { os: "linux", arch: "x64", binaryName: "supatype-engine", ext: "" }),
36
+ ).toBe("supatype-engine-0.1.0-alpha.1-linux-x64")
37
+
38
+ expect(
39
+ getArtifactName("0.1.0-alpha.1", { os: "darwin", arch: "arm64", binaryName: "supatype-engine", ext: "" }),
40
+ ).toBe("supatype-engine-0.1.0-alpha.1-darwin-arm64")
41
+
42
+ expect(
43
+ getArtifactName("0.1.0-alpha.1", { os: "win", arch: "x64", binaryName: "supatype-engine.exe", ext: ".exe" }),
44
+ ).toBe("supatype-engine-0.1.0-alpha.1-win-x64.exe")
45
+ })
46
+
47
+ it("rejects unsupported platforms", async () => {
48
+ const platformModule = await import("../src/engine/platform.js")
49
+ // We can't easily test this without mocking process.platform/arch
50
+ // but the logic is covered by the PLATFORM_MAP check
51
+ expect(typeof platformModule.detectPlatform).toBe("function")
52
+ })
53
+ })
54
+
55
+ // ── CDN URL construction ─────────────────────────────────────────────
56
+
57
+ describe("CDN URL construction", () => {
58
+ it("builds correct CDN URLs", async () => {
59
+ const { getCdnUrl } = await import("../src/engine/platform.js")
60
+
61
+ const url = getCdnUrl(
62
+ "https://releases.supatype.io/engine",
63
+ "0.1.0-alpha.1",
64
+ "supatype-engine-0.1.0-alpha.1-linux-x64",
65
+ )
66
+ expect(url).toBe(
67
+ "https://releases.supatype.io/engine/v0.1.0-alpha.1/supatype-engine-0.1.0-alpha.1-linux-x64",
68
+ )
69
+ })
70
+ })
71
+
72
+ // ── Cache management ─────────────────────────────────────────────────
73
+
74
+ describe("Cache management", () => {
75
+ const testCacheDir = join(tmpdir(), `supatype-test-cache-${Date.now()}`)
76
+
77
+ // We test the pure functions, not the ones using homedir()
78
+ afterEach(() => {
79
+ if (existsSync(testCacheDir)) {
80
+ rmSync(testCacheDir, { recursive: true, force: true })
81
+ }
82
+ })
83
+
84
+ it("lists cached versions from empty cache", async () => {
85
+ const { listCachedVersions } = await import("../src/engine/cache.js")
86
+ // May or may not have cached versions on this machine
87
+ const versions = listCachedVersions()
88
+ expect(Array.isArray(versions)).toBe(true)
89
+ })
90
+
91
+ it("pruneCacheExcept keeps specified version", async () => {
92
+ const { pruneCacheExcept, getCacheDir } = await import("../src/engine/cache.js")
93
+ const cacheDir = getCacheDir()
94
+
95
+ // Create test versions
96
+ const v1Dir = join(cacheDir, "test-0.0.1")
97
+ const v2Dir = join(cacheDir, "test-0.0.2")
98
+ mkdirSync(v1Dir, { recursive: true })
99
+ mkdirSync(v2Dir, { recursive: true })
100
+ writeFileSync(join(v1Dir, "supatype-engine"), "binary1")
101
+ writeFileSync(join(v2Dir, "supatype-engine"), "binary2")
102
+
103
+ const result = pruneCacheExcept("test-0.0.2")
104
+ expect(result.removed).toContain("test-0.0.1")
105
+ expect(result.removed).not.toContain("test-0.0.2")
106
+
107
+ // Cleanup
108
+ if (existsSync(v2Dir)) rmSync(v2Dir, { recursive: true, force: true })
109
+ })
110
+ })
111
+
112
+ // ── Checksum verification ────────────────────────────────────────────
113
+
114
+ describe("Checksum verification", () => {
115
+ const testDir = join(tmpdir(), `supatype-test-checksum-${Date.now()}`)
116
+
117
+ beforeEach(() => {
118
+ mkdirSync(testDir, { recursive: true })
119
+ })
120
+
121
+ afterEach(() => {
122
+ if (existsSync(testDir)) {
123
+ rmSync(testDir, { recursive: true, force: true })
124
+ }
125
+ })
126
+
127
+ it("verifies correct checksum", async () => {
128
+ const { verifyChecksum } = await import("../src/engine/verify.js")
129
+
130
+ const binaryContent = Buffer.from("test binary content")
131
+ const binaryPath = join(testDir, "supatype-engine")
132
+ writeFileSync(binaryPath, binaryContent)
133
+
134
+ const hash = createHash("sha256").update(binaryContent).digest("hex")
135
+ const checksumPath = join(testDir, "checksums.sha256")
136
+ writeFileSync(checksumPath, `${hash} supatype-engine-0.1.0-linux-x64\n`)
137
+
138
+ const result = await verifyChecksum(
139
+ binaryPath,
140
+ checksumPath,
141
+ "supatype-engine-0.1.0-linux-x64",
142
+ )
143
+ expect(result).toBe(true)
144
+ })
145
+
146
+ it("rejects mismatched checksum", async () => {
147
+ const { verifyChecksum } = await import("../src/engine/verify.js")
148
+
149
+ const binaryPath = join(testDir, "supatype-engine")
150
+ writeFileSync(binaryPath, "actual content")
151
+
152
+ const checksumPath = join(testDir, "checksums.sha256")
153
+ writeFileSync(checksumPath, `${"a".repeat(64)} supatype-engine-0.1.0-linux-x64\n`)
154
+
155
+ const result = await verifyChecksum(
156
+ binaryPath,
157
+ checksumPath,
158
+ "supatype-engine-0.1.0-linux-x64",
159
+ )
160
+ expect(result).toBe(false)
161
+ })
162
+
163
+ it("rejects corrupt cached binary (flipped byte)", async () => {
164
+ const { verifyChecksum } = await import("../src/engine/verify.js")
165
+
166
+ const originalContent = Buffer.from("original binary content here")
167
+ const hash = createHash("sha256").update(originalContent).digest("hex")
168
+
169
+ // Corrupt the binary (flip a byte)
170
+ const corruptContent = Buffer.from(originalContent)
171
+ corruptContent[0] = corruptContent[0]! ^ 0xff
172
+
173
+ const binaryPath = join(testDir, "supatype-engine")
174
+ writeFileSync(binaryPath, corruptContent)
175
+
176
+ const checksumPath = join(testDir, "checksums.sha256")
177
+ writeFileSync(checksumPath, `${hash} supatype-engine-0.1.0-linux-x64\n`)
178
+
179
+ const result = await verifyChecksum(
180
+ binaryPath,
181
+ checksumPath,
182
+ "supatype-engine-0.1.0-linux-x64",
183
+ )
184
+ expect(result).toBe(false)
185
+ })
186
+
187
+ it("throws when filename not found in checksum file", async () => {
188
+ const { verifyChecksum } = await import("../src/engine/verify.js")
189
+
190
+ const binaryPath = join(testDir, "supatype-engine")
191
+ writeFileSync(binaryPath, "content")
192
+
193
+ const checksumPath = join(testDir, "checksums.sha256")
194
+ writeFileSync(checksumPath, `${"a".repeat(64)} other-file\n`)
195
+
196
+ await expect(
197
+ verifyChecksum(binaryPath, checksumPath, "supatype-engine-0.1.0-linux-x64"),
198
+ ).rejects.toThrow("No checksum found")
199
+ })
200
+ })
201
+
202
+ // ── Signature verification ───────────────────────────────────────────
203
+
204
+ describe("Signature verification", () => {
205
+ const testDir = join(tmpdir(), `supatype-test-sig-${Date.now()}`)
206
+
207
+ beforeEach(() => {
208
+ mkdirSync(testDir, { recursive: true })
209
+ })
210
+
211
+ afterEach(() => {
212
+ if (existsSync(testDir)) {
213
+ rmSync(testDir, { recursive: true, force: true })
214
+ }
215
+ })
216
+
217
+ it("rejects tampered checksum file (invalid signature)", async () => {
218
+ const { verifySignature } = await import("../src/engine/verify.js")
219
+
220
+ const checksumPath = join(testDir, "checksums.sha256")
221
+ writeFileSync(checksumPath, `${"a".repeat(64)} supatype-engine-0.1.0-linux-x64\n`)
222
+
223
+ // Create a fake .minisig with garbage signature
224
+ const sigPath = join(testDir, "checksums.sha256.minisig")
225
+ writeFileSync(
226
+ sigPath,
227
+ `untrusted comment: fake\n${Buffer.alloc(74).toString("base64")}\n`,
228
+ )
229
+
230
+ const result = await verifySignature(checksumPath, sigPath)
231
+ expect(result).toBe(false)
232
+ })
233
+
234
+ it("rejects missing .minisig file", async () => {
235
+ const { verifySignature } = await import("../src/engine/verify.js")
236
+
237
+ const checksumPath = join(testDir, "checksums.sha256")
238
+ writeFileSync(checksumPath, "some content")
239
+
240
+ const result = await verifySignature(checksumPath, join(testDir, "nonexistent.minisig"))
241
+ expect(result).toBe(false)
242
+ })
243
+
244
+ it("rejects forged signature with wrong key", async () => {
245
+ const { verifySignature } = await import("../src/engine/verify.js")
246
+
247
+ const checksumPath = join(testDir, "checksums.sha256")
248
+ writeFileSync(checksumPath, `${"a".repeat(64)} supatype-engine\n`)
249
+
250
+ // Forged signature (valid format but wrong key)
251
+ const sigPath = join(testDir, "checksums.sha256.minisig")
252
+ const fakeSignature = Buffer.alloc(74)
253
+ fakeSignature.writeUInt16LE(0x4564, 0) // Ed algorithm bytes
254
+ writeFileSync(
255
+ sigPath,
256
+ `untrusted comment: forged\n${fakeSignature.toString("base64")}\n`,
257
+ )
258
+
259
+ const result = await verifySignature(checksumPath, sigPath)
260
+ expect(result).toBe(false)
261
+ })
262
+ })
263
+
264
+ // ── Version compatibility ────────────────────────────────────────────
265
+
266
+ describe("Version compatibility", () => {
267
+ it("accepts same major version", async () => {
268
+ const { checkVersionCompatibility } = await import("../src/engine/resolve.js")
269
+
270
+ expect(checkVersionCompatibility("1.2.3", "1.0.0").compatible).toBe(true)
271
+ expect(checkVersionCompatibility("0.1.0", "0.2.0").compatible).toBe(true)
272
+ })
273
+
274
+ it("rejects different major versions", async () => {
275
+ const { checkVersionCompatibility } = await import("../src/engine/resolve.js")
276
+
277
+ const result = checkVersionCompatibility("2.1.0", "1.3.0")
278
+ expect(result.compatible).toBe(false)
279
+ expect(result.message).toContain("not compatible")
280
+ expect(result.message).toContain("npm update @supatype/cli")
281
+ })
282
+ })
283
+
284
+ // ── Update check throttling ─────────────────────────────────────────
285
+
286
+ describe("Update check throttling", () => {
287
+ it("returns true when no check file exists", async () => {
288
+ const { shouldCheckForUpdates } = await import("../src/engine/cache.js")
289
+ // On fresh machine, should want to check
290
+ const result = await shouldCheckForUpdates()
291
+ expect(typeof result).toBe("boolean")
292
+ })
293
+
294
+ it("skips check in CI environments", async () => {
295
+ const origCI = process.env.CI
296
+ process.env.CI = "true"
297
+
298
+ const { shouldCheckForUpdates } = await import("../src/engine/cache.js")
299
+ const result = await shouldCheckForUpdates()
300
+ expect(result).toBe(false)
301
+
302
+ if (origCI !== undefined) {
303
+ process.env.CI = origCI
304
+ } else {
305
+ delete process.env.CI
306
+ }
307
+ })
308
+ })
309
+
310
+ // ── Download retry ──────────────────────────────────────────────────
311
+
312
+ describe("Download retry", () => {
313
+ it("fetchJson returns undefined on network error", async () => {
314
+ const { fetchJson } = await import("../src/engine/download.js")
315
+ const result = await fetchJson("http://localhost:1/nonexistent")
316
+ expect(result).toBeUndefined()
317
+ })
318
+ })
319
+
320
+ // ── Engine version constants ─────────────────────────────────────────
321
+
322
+ describe("Engine version constants", () => {
323
+ it("exports valid version and URLs", async () => {
324
+ const {
325
+ ENGINE_VERSION,
326
+ CDN_BASE_URL,
327
+ ENGINE_RELEASES_REPO,
328
+ GITHUB_RELEASES_FALLBACK_URL,
329
+ } = await import("../src/engine-version.js")
330
+
331
+ expect(ENGINE_VERSION).toMatch(/^\d+\.\d+\.\d+/)
332
+ expect(CDN_BASE_URL).toBe("https://releases.supatype.io/engine")
333
+ expect(ENGINE_RELEASES_REPO).toBe("supatype/engine-releases")
334
+ expect(GITHUB_RELEASES_FALLBACK_URL).toContain("github.com")
335
+ })
336
+ })
337
+
338
+ // ── Full binary verification pipeline ────────────────────────────────
339
+
340
+ describe("Binary verification pipeline", () => {
341
+ const testDir = join(tmpdir(), `supatype-test-pipeline-${Date.now()}`)
342
+
343
+ beforeEach(() => {
344
+ mkdirSync(testDir, { recursive: true })
345
+ })
346
+
347
+ afterEach(() => {
348
+ if (existsSync(testDir)) {
349
+ rmSync(testDir, { recursive: true, force: true })
350
+ }
351
+ })
352
+
353
+ it("verifyChecksumOnly passes with valid checksum", async () => {
354
+ const { verifyChecksumOnly } = await import("../src/engine/verify.js")
355
+
356
+ const content = Buffer.from("valid engine binary")
357
+ const hash = createHash("sha256").update(content).digest("hex")
358
+
359
+ const binaryPath = join(testDir, "engine")
360
+ writeFileSync(binaryPath, content)
361
+
362
+ const checksumPath = join(testDir, "checksums.sha256")
363
+ writeFileSync(checksumPath, `${hash} engine-0.1.0-linux-x64\n`)
364
+
365
+ // Should not throw
366
+ await verifyChecksumOnly(binaryPath, checksumPath, "engine-0.1.0-linux-x64")
367
+ })
368
+
369
+ it("verifyChecksumOnly rejects and deletes corrupt binary", async () => {
370
+ const { verifyChecksumOnly } = await import("../src/engine/verify.js")
371
+
372
+ const binaryPath = join(testDir, "engine")
373
+ writeFileSync(binaryPath, "corrupt content")
374
+
375
+ const checksumPath = join(testDir, "checksums.sha256")
376
+ writeFileSync(checksumPath, `${"f".repeat(64)} engine-0.1.0-linux-x64\n`)
377
+
378
+ await expect(
379
+ verifyChecksumOnly(binaryPath, checksumPath, "engine-0.1.0-linux-x64"),
380
+ ).rejects.toThrow("checksum mismatch")
381
+
382
+ // Binary should be deleted
383
+ expect(existsSync(binaryPath)).toBe(false)
384
+ })
385
+
386
+ it("verifyBinary rejects when signature is invalid", async () => {
387
+ const { verifyBinary } = await import("../src/engine/verify.js")
388
+
389
+ const content = Buffer.from("binary content")
390
+ const hash = createHash("sha256").update(content).digest("hex")
391
+
392
+ const binaryPath = join(testDir, "engine")
393
+ writeFileSync(binaryPath, content)
394
+
395
+ const checksumPath = join(testDir, "checksums.sha256")
396
+ writeFileSync(checksumPath, `${hash} engine-artifact\n`)
397
+
398
+ const sigPath = join(testDir, "checksums.sha256.minisig")
399
+ writeFileSync(sigPath, `untrusted comment: bad\n${Buffer.alloc(74).toString("base64")}\n`)
400
+
401
+ await expect(
402
+ verifyBinary(binaryPath, checksumPath, sigPath, "engine-artifact"),
403
+ ).rejects.toThrow("signature verification failed")
404
+
405
+ // Binary should be deleted
406
+ expect(existsSync(binaryPath)).toBe(false)
407
+ })
408
+ })
409
+
410
+ // ── Binary size check (placeholder for CI) ───────────────────────────
411
+
412
+ describe("Binary size", () => {
413
+ it("has a 20MB size target documented", () => {
414
+ // This is a CI-level check. Here we just verify the constant is defined.
415
+ const MAX_BINARY_SIZE_MB = 20
416
+ expect(MAX_BINARY_SIZE_MB).toBe(20)
417
+ })
418
+ })
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
+ import { mkdirSync, rmSync, existsSync, readFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import { scaffold } from "../src/commands/init.js"
6
+
7
+ let tmpRoot: string
8
+
9
+ beforeEach(() => {
10
+ tmpRoot = join(tmpdir(), `dt-init-test-${Date.now()}`)
11
+ mkdirSync(tmpRoot, { recursive: true })
12
+ })
13
+
14
+ afterEach(() => {
15
+ rmSync(tmpRoot, { recursive: true, force: true })
16
+ })
17
+
18
+ describe("scaffold()", () => {
19
+ it("creates all expected files", () => {
20
+ scaffold(tmpRoot, "my-app")
21
+
22
+ const expected = [
23
+ "supatype.config.ts",
24
+ "schema/index.ts",
25
+ ".env",
26
+ "docker-compose.yml",
27
+ ".supatype/kong.yml",
28
+ ".supatype/pgbouncer.ini",
29
+ ".supatype/userlist.txt",
30
+ "seed.ts",
31
+ ".gitignore",
32
+ ]
33
+ for (const rel of expected) {
34
+ expect(existsSync(join(tmpRoot, rel)), `${rel} should exist`).toBe(true)
35
+ }
36
+ })
37
+
38
+ it("supatype.config.ts embeds the project name and exports defineConfig", () => {
39
+ scaffold(tmpRoot, "blog-app")
40
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
41
+ expect(content).toContain("blog-app")
42
+ expect(content).toContain("defineConfig")
43
+ expect(content).toContain("schema:")
44
+ expect(content).toContain("output:")
45
+ })
46
+
47
+ it("supatype.config.ts contains commented selfHost section", () => {
48
+ scaffold(tmpRoot, "my-app")
49
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
50
+ expect(content).toContain("selfHost")
51
+ expect(content).toContain("domain")
52
+ })
53
+
54
+ it("docker-compose.yml references project name, correct images, and health check", () => {
55
+ scaffold(tmpRoot, "shop")
56
+ const content = readFileSync(join(tmpRoot, "docker-compose.yml"), "utf8")
57
+ expect(content).toContain("shop")
58
+ expect(content).toContain("supatype/postgres")
59
+ expect(content).toContain("postgrest/postgrest")
60
+ expect(content).toContain("kong:")
61
+ expect(content).toContain("service_healthy")
62
+ })
63
+
64
+ it("docker-compose.yml includes GoTrue auth service", () => {
65
+ scaffold(tmpRoot, "shop")
66
+ const content = readFileSync(join(tmpRoot, "docker-compose.yml"), "utf8")
67
+ expect(content).toContain("gotrue:")
68
+ expect(content).toContain("supatype/auth")
69
+ expect(content).toContain("GOTRUE_JWT_SECRET")
70
+ expect(content).toContain("9999")
71
+ })
72
+
73
+ it("docker-compose.yml includes PgBouncer service connecting services via port 6432", () => {
74
+ scaffold(tmpRoot, "shop")
75
+ const content = readFileSync(join(tmpRoot, "docker-compose.yml"), "utf8")
76
+ expect(content).toContain("pgbouncer:")
77
+ expect(content).toContain("pgbouncer/pgbouncer")
78
+ expect(content).toContain("pgbouncer:6432")
79
+ expect(content).toContain("PGRST_DB_POOL")
80
+ })
81
+
82
+ it("docker-compose.yml includes studio service (merged admin + studio)", () => {
83
+ scaffold(tmpRoot, "shop")
84
+ const content = readFileSync(join(tmpRoot, "docker-compose.yml"), "utf8")
85
+ expect(content).toContain("studio:")
86
+ expect(content).toContain("ghcr.io/supatype/studio")
87
+ expect(content).toContain("3002:3002")
88
+ // Admin was merged into studio — no separate admin service
89
+ expect(content).not.toContain("ghcr.io/supatype/admin")
90
+ })
91
+
92
+ it("docker-compose.yml includes commented app service slot", () => {
93
+ scaffold(tmpRoot, "shop")
94
+ const content = readFileSync(join(tmpRoot, "docker-compose.yml"), "utf8")
95
+ expect(content).toContain("supatype app add")
96
+ expect(content).toContain("SUPATYPE_URL")
97
+ expect(content).toContain("SUPATYPE_ANON_KEY")
98
+ })
99
+
100
+ it(".supatype/pgbouncer.ini has correct pool settings", () => {
101
+ scaffold(tmpRoot, "my-app")
102
+ const content = readFileSync(join(tmpRoot, ".supatype/pgbouncer.ini"), "utf8")
103
+ expect(content).toContain("pool_mode = transaction")
104
+ expect(content).toContain("default_pool_size = 20")
105
+ expect(content).toContain("max_db_connections = 60")
106
+ expect(content).toContain("listen_port = 6432")
107
+ })
108
+
109
+ it(".env contains DATABASE_URL, JWT_SECRET, POSTGRES_PASSWORD, POSTGRES_DB", () => {
110
+ scaffold(tmpRoot, "my-app")
111
+ const content = readFileSync(join(tmpRoot, ".env"), "utf8")
112
+ expect(content).toContain("DATABASE_URL=")
113
+ expect(content).toContain("JWT_SECRET=")
114
+ expect(content).toContain("POSTGRES_PASSWORD=")
115
+ expect(content).toContain("POSTGRES_DB=")
116
+ })
117
+
118
+ it(".env contains ANON_KEY, SERVICE_ROLE_KEY, and SITE_URL placeholders", () => {
119
+ scaffold(tmpRoot, "my-app")
120
+ const content = readFileSync(join(tmpRoot, ".env"), "utf8")
121
+ expect(content).toContain("ANON_KEY=")
122
+ expect(content).toContain("SERVICE_ROLE_KEY=")
123
+ expect(content).toContain("SITE_URL=")
124
+ })
125
+
126
+ it("schema/index.ts exports a User model with field builders and access rules", () => {
127
+ scaffold(tmpRoot, "my-app")
128
+ const content = readFileSync(join(tmpRoot, "schema/index.ts"), "utf8")
129
+ expect(content).toContain("export const User")
130
+ expect(content).toContain("model(")
131
+ expect(content).toContain("field.")
132
+ expect(content).toContain("access.")
133
+ expect(content).toContain("options: { timestamps: true }")
134
+ })
135
+
136
+ it(".supatype/kong.yml declares REST, GraphQL, and auth routes", () => {
137
+ scaffold(tmpRoot, "my-app")
138
+ const content = readFileSync(join(tmpRoot, ".supatype/kong.yml"), "utf8")
139
+ expect(content).toContain("/rest/v1/")
140
+ expect(content).toContain("/graphql/v1")
141
+ expect(content).toContain("/auth/v1/")
142
+ expect(content).toContain("postgrest")
143
+ expect(content).toContain("gotrue")
144
+ })
145
+
146
+ it(".supatype/kong.yml contains commented app fallback route", () => {
147
+ scaffold(tmpRoot, "my-app")
148
+ const content = readFileSync(join(tmpRoot, ".supatype/kong.yml"), "utf8")
149
+ expect(content).toContain("supatype app add")
150
+ expect(content).toContain("app-root")
151
+ })
152
+
153
+ it(".gitignore excludes .env, node_modules, and engine binary", () => {
154
+ scaffold(tmpRoot, "my-app")
155
+ const content = readFileSync(join(tmpRoot, ".gitignore"), "utf8")
156
+ expect(content).toContain(".env")
157
+ expect(content).toContain("node_modules/")
158
+ expect(content).toContain(".supatype/engine/")
159
+ })
160
+
161
+ it("seed.ts references the project name", () => {
162
+ scaffold(tmpRoot, "acme")
163
+ const content = readFileSync(join(tmpRoot, "seed.ts"), "utf8")
164
+ expect(content).toContain("acme")
165
+ })
166
+
167
+ it("different project names produce different connection strings", () => {
168
+ scaffold(tmpRoot, "alpha")
169
+ const alpha = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
170
+
171
+ const tmp2 = join(tmpdir(), `dt-init-test2-${Date.now()}`)
172
+ mkdirSync(tmp2, { recursive: true })
173
+ try {
174
+ scaffold(tmp2, "beta")
175
+ const beta = readFileSync(join(tmp2, "supatype.config.ts"), "utf8")
176
+ expect(alpha).toContain("alpha")
177
+ expect(beta).toContain("beta")
178
+ expect(alpha).not.toContain("beta")
179
+ expect(beta).not.toContain("alpha")
180
+ } finally {
181
+ rmSync(tmp2, { recursive: true, force: true })
182
+ }
183
+ })
184
+ })