@supatype/cli 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +209 -92
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app-config.d.ts +10 -0
  5. package/dist/app-config.d.ts.map +1 -1
  6. package/dist/app-config.js +72 -0
  7. package/dist/app-config.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +2 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/add.d.ts +3 -0
  12. package/dist/commands/add.d.ts.map +1 -0
  13. package/dist/commands/add.js +83 -0
  14. package/dist/commands/add.js.map +1 -0
  15. package/dist/commands/app.js +2 -2
  16. package/dist/commands/app.js.map +1 -1
  17. package/dist/commands/init.d.ts +29 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +569 -90
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/keys.d.ts +15 -1
  22. package/dist/commands/keys.d.ts.map +1 -1
  23. package/dist/commands/keys.js +39 -4
  24. package/dist/commands/keys.js.map +1 -1
  25. package/dist/commands/self-host.d.ts.map +1 -1
  26. package/dist/commands/self-host.js +5 -5
  27. package/dist/commands/self-host.js.map +1 -1
  28. package/dist/kong-config.d.ts +9 -0
  29. package/dist/kong-config.d.ts.map +1 -1
  30. package/dist/kong-config.js +18 -1
  31. package/dist/kong-config.js.map +1 -1
  32. package/dist/project-config.d.ts +16 -0
  33. package/dist/project-config.d.ts.map +1 -1
  34. package/dist/project-config.js +34 -0
  35. package/dist/project-config.js.map +1 -1
  36. package/dist/prompts.d.ts +8 -0
  37. package/dist/prompts.d.ts.map +1 -0
  38. package/dist/prompts.js +20 -0
  39. package/dist/prompts.js.map +1 -0
  40. package/dist/self-host-compose.d.ts.map +1 -1
  41. package/dist/self-host-compose.js +62 -17
  42. package/dist/self-host-compose.js.map +1 -1
  43. package/package.json +2 -1
  44. package/src/app-config.ts +80 -0
  45. package/src/cli.ts +2 -0
  46. package/src/commands/add.ts +94 -0
  47. package/src/commands/app.ts +2 -2
  48. package/src/commands/init.ts +738 -88
  49. package/src/commands/keys.ts +49 -4
  50. package/src/commands/self-host.ts +25 -5
  51. package/src/kong-config.ts +24 -1
  52. package/src/project-config.ts +45 -0
  53. package/src/prompts.ts +21 -0
  54. package/src/self-host-compose.ts +62 -17
  55. package/tests/config.test.ts +26 -0
  56. package/tests/init.test.ts +128 -15
  57. package/tests/runtime-contract.test.ts +111 -1
  58. package/tsconfig.tsbuildinfo +1 -1
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
2
  import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { tmpdir } from "node:os"
5
- import { scaffold } from "../src/commands/init.js"
5
+ import { scaffold, defaultScaffoldOptions } from "../src/commands/init.js"
6
6
 
7
7
  let tmpRoot: string
8
8
 
@@ -17,7 +17,7 @@ afterEach(() => {
17
17
 
18
18
  describe("scaffold()", () => {
19
19
  it("creates all expected files", () => {
20
- scaffold(tmpRoot, "my-app")
20
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
21
21
 
22
22
  const expected = [
23
23
  "package.json",
@@ -35,7 +35,7 @@ describe("scaffold()", () => {
35
35
  })
36
36
 
37
37
  it("supatype.config.ts embeds the project name and exports defineConfig", () => {
38
- scaffold(tmpRoot, "blog-app")
38
+ scaffold(tmpRoot, defaultScaffoldOptions("blog-app"))
39
39
  const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
40
40
  expect(content).toContain("blog-app")
41
41
  expect(content).toContain("defineConfig")
@@ -46,7 +46,7 @@ describe("scaffold()", () => {
46
46
  })
47
47
 
48
48
  it("package.json includes @supatype/cli and @supatype/types", () => {
49
- scaffold(tmpRoot, "pkg-app")
49
+ scaffold(tmpRoot, defaultScaffoldOptions("pkg-app"))
50
50
  const content = readFileSync(join(tmpRoot, "package.json"), "utf8")
51
51
  expect(content).toContain("@supatype/cli")
52
52
  expect(content).toContain("@supatype/types")
@@ -56,18 +56,18 @@ describe("scaffold()", () => {
56
56
  it("skips package.json when it already exists", () => {
57
57
  const pkgPath = join(tmpRoot, "package.json")
58
58
  writeFileSync(pkgPath, '{"name":"existing"}', "utf8")
59
- scaffold(tmpRoot, "my-app")
59
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
60
60
  expect(readFileSync(pkgPath, "utf8")).toBe('{"name":"existing"}')
61
61
  })
62
62
 
63
63
  it("supatype.config.ts documents self-host workflow", () => {
64
- scaffold(tmpRoot, "my-app")
64
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
65
65
  const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
66
66
  expect(content).toContain("self-host")
67
67
  })
68
68
 
69
69
  it(".env contains DATABASE_URL, JWT_SECRET, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB", () => {
70
- scaffold(tmpRoot, "my-app")
70
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
71
71
  const content = readFileSync(join(tmpRoot, ".env"), "utf8")
72
72
  expect(content).toContain("DATABASE_URL=")
73
73
  expect(content).toContain("JWT_SECRET=")
@@ -77,23 +77,24 @@ describe("scaffold()", () => {
77
77
  })
78
78
 
79
79
  it(".env contains ANON_KEY, SERVICE_ROLE_KEY, and SITE_URL placeholders", () => {
80
- scaffold(tmpRoot, "my-app")
80
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
81
81
  const content = readFileSync(join(tmpRoot, ".env"), "utf8")
82
82
  expect(content).toContain("ANON_KEY=")
83
83
  expect(content).toContain("SERVICE_ROLE_KEY=")
84
84
  expect(content).toContain("SITE_URL=")
85
85
  })
86
86
 
87
- it("schema/index.ts exports a User model using RFC v2 Model<>", () => {
88
- scaffold(tmpRoot, "my-app")
87
+ it("schema/index.ts exports a Profile model using RFC v2 Model<>", () => {
88
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
89
89
  const content = readFileSync(join(tmpRoot, "schema/index.ts"), "utf8")
90
- expect(content).toContain("export type User")
90
+ expect(content).toContain("export type Profile")
91
+ expect(content).toContain("display_name")
91
92
  expect(content).toContain("Model<")
92
93
  expect(content).toContain("access:")
93
94
  })
94
95
 
95
96
  it(".gitignore excludes .env, node_modules, and engine binary", () => {
96
- scaffold(tmpRoot, "my-app")
97
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app"))
97
98
  const content = readFileSync(join(tmpRoot, ".gitignore"), "utf8")
98
99
  expect(content).toContain(".env")
99
100
  expect(content).toContain("node_modules/")
@@ -102,19 +103,19 @@ describe("scaffold()", () => {
102
103
  })
103
104
 
104
105
  it("seed.ts references the project name", () => {
105
- scaffold(tmpRoot, "acme")
106
+ scaffold(tmpRoot, defaultScaffoldOptions("acme"))
106
107
  const content = readFileSync(join(tmpRoot, "seed.ts"), "utf8")
107
108
  expect(content).toContain("acme")
108
109
  })
109
110
 
110
111
  it("different project names produce different config bodies", () => {
111
- scaffold(tmpRoot, "alpha")
112
+ scaffold(tmpRoot, defaultScaffoldOptions("alpha"))
112
113
  const alpha = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
113
114
 
114
115
  const tmp2 = join(tmpdir(), `dt-init-test2-${Date.now()}`)
115
116
  mkdirSync(tmp2, { recursive: true })
116
117
  try {
117
- scaffold(tmp2, "beta")
118
+ scaffold(tmp2, defaultScaffoldOptions("beta"))
118
119
  const beta = readFileSync(join(tmp2, "supatype.config.ts"), "utf8")
119
120
  expect(alpha).toContain("alpha")
120
121
  expect(beta).toContain("beta")
@@ -124,4 +125,116 @@ describe("scaffold()", () => {
124
125
  rmSync(tmp2, { recursive: true, force: true })
125
126
  }
126
127
  })
128
+
129
+ it("self-host target emits standalone mode + domain and a local override", () => {
130
+ scaffold(tmpRoot, { ...defaultScaffoldOptions("my-app", "self-host"), domain: "api.example.com" })
131
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
132
+ expect(content).toContain('mode: "standalone"')
133
+ expect(content).toContain('domain: "api.example.com"')
134
+ expect(content).toContain('environments: { default: "production" }')
135
+ expect(existsSync(join(tmpRoot, "supatype.local.config.ts"))).toBe(true)
136
+ const local = readFileSync(join(tmpRoot, "supatype.local.config.ts"), "utf8")
137
+ expect(local).toContain('mode: "dev"')
138
+ expect(local).toContain("Partial<SupatypeConfig>")
139
+ })
140
+
141
+ it("self-host with a TLS email emits an active tls block", () => {
142
+ scaffold(tmpRoot, {
143
+ ...defaultScaffoldOptions("my-app", "self-host"),
144
+ domain: "api.example.com",
145
+ tlsEmail: "ops@example.com",
146
+ })
147
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
148
+ expect(content).toContain('tls: { email: "ops@example.com" }')
149
+ })
150
+
151
+ it("self-host without a TLS email emits a commented tls hint", () => {
152
+ scaffold(tmpRoot, { ...defaultScaffoldOptions("my-app", "self-host"), domain: "api.example.com" })
153
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
154
+ expect(content).toContain('// tls: { email: "you@example.com" }')
155
+ })
156
+
157
+ it("cloud target emits managed mode + environments and a local override", () => {
158
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app", "cloud"))
159
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
160
+ expect(content).toContain('mode: "managed"')
161
+ expect(content).toContain('environments: { default: "production" }')
162
+ expect(existsSync(join(tmpRoot, "supatype.local.config.ts"))).toBe(true)
163
+ })
164
+
165
+ it("later target stays in dev mode with no local override", () => {
166
+ scaffold(tmpRoot, defaultScaffoldOptions("my-app", "later"))
167
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
168
+ expect(content).toContain('mode: "dev"')
169
+ expect(content).not.toContain("environments:")
170
+ expect(existsSync(join(tmpRoot, "supatype.local.config.ts"))).toBe(false)
171
+ })
172
+
173
+ it("static app mode writes the static dir and config block", () => {
174
+ scaffold(tmpRoot, {
175
+ ...defaultScaffoldOptions("my-app"),
176
+ app: { mode: "static", staticDir: "./dist" },
177
+ })
178
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
179
+ expect(content).toContain('mode: "static"')
180
+ expect(content).toContain('static_dir: "./dist"')
181
+ expect(existsSync(join(tmpRoot, "dist/.gitkeep"))).toBe(true)
182
+ })
183
+
184
+ it("proxy app mode writes upstream and start in the config", () => {
185
+ scaffold(tmpRoot, {
186
+ ...defaultScaffoldOptions("my-app"),
187
+ app: { mode: "proxy", upstream: "http://localhost:4000", start: "dev" },
188
+ })
189
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
190
+ expect(content).toContain('mode: "proxy"')
191
+ expect(content).toContain('upstream: "http://localhost:4000"')
192
+ expect(content).toContain('start: "dev"')
193
+ })
194
+
195
+ it("hello-world function scaffolds function files and a functions script", () => {
196
+ scaffold(tmpRoot, { ...defaultScaffoldOptions("my-app"), helloFunction: true })
197
+ expect(existsSync(join(tmpRoot, "functions/hello/index.ts"))).toBe(true)
198
+ expect(existsSync(join(tmpRoot, "functions/_shared/README.md"))).toBe(true)
199
+ expect(existsSync(join(tmpRoot, "functions/.env.local"))).toBe(true)
200
+ const pkg = readFileSync(join(tmpRoot, "package.json"), "utf8")
201
+ expect(pkg).toContain("supatype functions serve")
202
+ })
203
+
204
+ it("custom schema path is honored", () => {
205
+ scaffold(tmpRoot, { ...defaultScaffoldOptions("my-app"), schemaPath: "db/schema.ts" })
206
+ expect(existsSync(join(tmpRoot, "db/schema.ts"))).toBe(true)
207
+ const content = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
208
+ expect(content).toContain('path: "db/schema.ts"')
209
+ })
210
+
211
+ it("s3 storage and resend email reflect in config and .env", () => {
212
+ scaffold(tmpRoot, {
213
+ ...defaultScaffoldOptions("my-app"),
214
+ email: "resend",
215
+ storageLocal: "s3",
216
+ storageProduction: "s3",
217
+ })
218
+ const config = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
219
+ expect(config).toContain('email: { provider: "resend" }')
220
+ expect(config).toContain('storage: { provider: "s3" }')
221
+ const env = readFileSync(join(tmpRoot, ".env"), "utf8")
222
+ expect(env).toContain("RESEND_API_KEY=")
223
+ expect(env).toContain("S3_BUCKET=")
224
+ expect(env).toContain("local development and production")
225
+ })
226
+
227
+ it("mixed local dev and s3 production writes both storage sections", () => {
228
+ scaffold(tmpRoot, {
229
+ ...defaultScaffoldOptions("my-app"),
230
+ storageLocal: "local",
231
+ storageProduction: "s3",
232
+ })
233
+ const config = readFileSync(join(tmpRoot, "supatype.config.ts"), "utf8")
234
+ expect(config).toContain('provider: "local"')
235
+ expect(config).toContain("Production storage: external S3")
236
+ const env = readFileSync(join(tmpRoot, ".env"), "utf8")
237
+ expect(env).toContain("local development — MinIO")
238
+ expect(env).toContain("production — external bucket")
239
+ })
127
240
  })
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os"
5
5
  import { runtimeRouteSpec } from "../src/runtime-routes.js"
6
6
  import { buildKongDeclarative } from "../src/kong-config.js"
7
7
  import { composeDockerImageEnv, composePullNeedsIgnoreFailures, hasLocalVersionPins, isRegistryPullableImageRef, renderSelfHostCompose, writeSelfHostCompose } from "../src/self-host-compose.js"
8
- import { updateAppConfigInProject } from "../src/app-config.js"
8
+ import { updateAppConfigInProject, updateServerConfigInProject } from "../src/app-config.js"
9
9
  import type { SupatypeProjectConfig } from "../src/project-config.js"
10
10
  import { DENO_RELEASE_PIN } from "../src/release-pins.js"
11
11
 
@@ -376,6 +376,60 @@ export default defineConfig({
376
376
  }
377
377
  })
378
378
 
379
+ it("server config updater sets standalone mode, domain, and tls (preserving other keys)", () => {
380
+ const dir = mkdtempSync(join(tmpdir(), "supatype-server-config-"))
381
+ try {
382
+ const configPath = join(dir, "supatype.config.ts")
383
+ writeFileSync(
384
+ configPath,
385
+ `import { defineConfig } from "@supatype/cli"
386
+
387
+ export default defineConfig({
388
+ project: { name: "x" },
389
+ database: { provider: "docker" },
390
+ server: { mode: "dev", port: 54321 },
391
+ app: { mode: "none" },
392
+ })
393
+ `,
394
+ "utf8",
395
+ )
396
+ updateServerConfigInProject(dir, { domain: "demo.supatype.com", tlsEmail: "hello@supatype.com" })
397
+ const next = readFileSync(configPath, "utf8")
398
+ expect(next).toContain(`mode: "standalone"`)
399
+ expect(next).toContain(`domain: "demo.supatype.com"`)
400
+ expect(next).toContain(`tls: { email: "hello@supatype.com", provider: "kong" }`)
401
+ expect(next).toContain(`port: 54321`)
402
+ } finally {
403
+ rmSync(dir, { recursive: true, force: true })
404
+ }
405
+ })
406
+
407
+ it("server config updater is idempotent and overwrites a prior domain/email", () => {
408
+ const dir = mkdtempSync(join(tmpdir(), "supatype-server-config-"))
409
+ try {
410
+ const configPath = join(dir, "supatype.config.ts")
411
+ writeFileSync(
412
+ configPath,
413
+ `export default {
414
+ project: { name: "x" },
415
+ database: { provider: "docker" },
416
+ server: { mode: "standalone", domain: "old.example.com", tls: { email: "old@example.com", provider: "kong" } },
417
+ app: { mode: "none" },
418
+ }
419
+ `,
420
+ "utf8",
421
+ )
422
+ updateServerConfigInProject(dir, { domain: "new.supatype.com", tlsEmail: "new@supatype.com" })
423
+ const next = readFileSync(configPath, "utf8")
424
+ expect(next).toContain(`domain: "new.supatype.com"`)
425
+ expect(next).toContain(`email: "new@supatype.com"`)
426
+ expect(next).not.toContain("old.example.com")
427
+ expect(next).not.toContain("old@example.com")
428
+ } finally {
429
+ rmSync(dir, { recursive: true, force: true })
430
+ }
431
+ })
432
+
379
433
  it("writes self-host compose artifacts under .supatype/self-host", () => {
380
434
  const dir = mkdtempSync(join(tmpdir(), "supatype-compose-"))
381
435
  try {
@@ -399,6 +453,62 @@ export default defineConfig({
399
453
  }
400
454
  })
401
455
 
456
+ const tlsConfig: SupatypeProjectConfig = {
457
+ ...baseConfig,
458
+ server: { mode: "standalone", domain: "api.example.com", tls: { email: "ops@example.com" } },
459
+ }
460
+
461
+ it("self-host compose renders Kong TLS + Valkey when standalone domain + email are set", () => {
462
+ const compose = renderSelfHostCompose(tlsConfig)
463
+ expect(compose).toContain("\n valkey:\n")
464
+ expect(compose).toContain("valkey/valkey:8-alpine")
465
+ expect(compose).toContain('- "80:8000"')
466
+ expect(compose).toContain('- "443:8443"')
467
+ expect(compose).toContain("KONG_PROXY_LISTEN")
468
+ expect(compose).toContain("- valkey")
469
+ expect(compose).toContain("https://api.example.com")
470
+ expect(compose).toMatch(/^\s{2}valkey-data:/m)
471
+ expect(compose).not.toContain("HTTPS is off")
472
+ })
473
+
474
+ it("self-host compose stays plain HTTP with a discoverable hint when TLS is off", () => {
475
+ const compose = renderSelfHostCompose(baseConfig)
476
+ expect(compose).not.toContain("\n valkey:\n")
477
+ expect(compose).not.toContain('- "443:8443"')
478
+ expect(compose).toContain("${SUPATYPE_KONG_PORT:-18473}:8000")
479
+ expect(compose).toContain("HTTPS is off")
480
+ })
481
+
482
+ it("self-host compose does not enable TLS for a domain without an ACME email", () => {
483
+ const compose = renderSelfHostCompose({
484
+ ...baseConfig,
485
+ server: { mode: "standalone", domain: "api.example.com" },
486
+ })
487
+ expect(compose).not.toContain("\n valkey:\n")
488
+ expect(compose).not.toContain('- "443:8443"')
489
+ })
490
+
491
+ it("writeSelfHostCompose emits a Kong acme plugin backed by Valkey when TLS is on", () => {
492
+ const dir = mkdtempSync(join(tmpdir(), "supatype-tls-"))
493
+ try {
494
+ const out = writeSelfHostCompose(dir, tlsConfig)
495
+ const kong = readFileSync(out.kongPath, "utf8")
496
+ expect(kong).toContain("name: acme")
497
+ expect(kong).toContain('account_email: "ops@example.com"')
498
+ expect(kong).toContain("tos_accepted: true")
499
+ expect(kong).toContain('- "api.example.com"')
500
+ expect(kong).toContain("storage: redis")
501
+ expect(kong).toContain('host: "valkey"')
502
+ } finally {
503
+ rmSync(dir, { recursive: true, force: true })
504
+ }
505
+ })
506
+
507
+ it("kong declarative omits the acme plugin when no acme options are provided", () => {
508
+ const kong = buildKongDeclarative({ unifiedGateway: true })
509
+ expect(kong).not.toContain("name: acme")
510
+ })
511
+
402
512
  it("writes default manifest when missing for compose", () => {
403
513
  const dir = mkdtempSync(join(tmpdir(), "supatype-manifest-"))
404
514
  try {