@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
@@ -1,6 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { resolve, join, dirname } from "node:path";
2
+ import { resolve, join, dirname, basename } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ import * as p from "@clack/prompts";
6
+ import { ensureNotCancelled, printLogo } from "../prompts.js";
7
+ import { generateAndWriteKeys } from "./keys.js";
4
8
  export { scaffold };
5
9
  // ─── Markers used by `supatype app add / remove` (app.ts) ────────────────────
6
10
  export const APP_COMPOSE_MARKER = " # ─── App service (run: supatype app add) ───";
@@ -15,42 +19,275 @@ function cliPackageVersion() {
15
19
  return "0.1.0";
16
20
  }
17
21
  }
22
+ const STORAGE_PROVIDER_OPTIONS = [
23
+ { value: "local", label: "Local", hint: "storage you host yourself (MinIO)" },
24
+ { value: "s3", label: "S3", hint: "external bucket (AWS S3 or compatible)" },
25
+ ];
26
+ /** `--mode dev|standalone` is mapped onto a production target for back-compat. */
27
+ function productionTargetFromMode(mode) {
28
+ return mode === "standalone" ? "self-host" : "later";
29
+ }
30
+ /** supatype-server mode written to the committed config for a production target. */
31
+ function serverModeForTarget(target) {
32
+ switch (target) {
33
+ case "cloud":
34
+ return "managed";
35
+ case "self-host":
36
+ return "standalone";
37
+ case "later":
38
+ return "dev";
39
+ }
40
+ }
41
+ export function defaultScaffoldOptions(projectName, productionTarget = "later") {
42
+ return {
43
+ projectName,
44
+ provider: "docker",
45
+ productionTarget,
46
+ ...(productionTarget === "self-host" ? { domain: "" } : {}),
47
+ schemaPath: "schema/index.ts",
48
+ app: { mode: "none" },
49
+ email: "console",
50
+ storageLocal: "local",
51
+ storageProduction: "local",
52
+ helloFunction: false,
53
+ };
54
+ }
18
55
  export function registerInit(program) {
19
56
  program
20
57
  .command("init [name]")
21
58
  .description("Scaffold a new Supatype project")
22
- .option("--mode <mode>", "Server mode in supatype.config.ts: dev (default) | standalone (native ACME — not Compose self-host)", "dev")
23
- .action(async (name, opts = { mode: "dev" }) => {
24
- const projectName = name ?? "my-project";
59
+ .option("--mode <mode>", "Back-compat: dev (default, local only) | standalone (self-host production target)", "dev")
60
+ .option("-y, --defaults", "Skip all prompts and use sensible defaults")
61
+ .option("--no-install", "Do not run the package manager install step")
62
+ .option("--no-keys", "Do not generate ANON_KEY / SERVICE_ROLE_KEY")
63
+ .action(async (name, opts) => {
25
64
  const dir = name ? resolve(process.cwd(), name) : process.cwd();
26
65
  if (name && existsSync(dir)) {
27
66
  console.error(`Directory already exists: ${dir}`);
28
67
  process.exit(1);
29
68
  }
69
+ const defaultName = name ?? basename(dir) ?? "my-project";
70
+ const interactive = !opts.defaults && Boolean(process.stdin.isTTY);
71
+ const modeTarget = productionTargetFromMode(opts.mode);
72
+ let result;
73
+ if (interactive) {
74
+ printLogo();
75
+ result = await runWizard(defaultName, modeTarget);
76
+ }
77
+ else {
78
+ result = {
79
+ ...defaultScaffoldOptions(defaultName, modeTarget),
80
+ packageManager: detectInvokingPackageManager(),
81
+ install: true,
82
+ generateKeys: true,
83
+ };
84
+ }
85
+ // CLI flags override wizard / default action choices.
86
+ const doInstall = opts.install !== false && result.install;
87
+ const doKeys = opts.keys !== false && result.generateKeys;
30
88
  if (name)
31
89
  mkdirSync(dir, { recursive: true });
32
- scaffold(dir, projectName, opts.mode);
33
- console.log(`\nSupatype project ready${name ? ` in ${name}/` : ""}.\n`);
34
- console.log("Next steps:");
35
- if (name)
36
- console.log(` cd ${name}`);
37
- console.log(" npm install");
38
- console.log(" supatype keys");
39
- console.log(" supatype dev # Docker Compose stack (Kong :18473)");
40
- console.log(" supatype push # apply schema + generate types");
41
- console.log("\nStatic frontend (self-host):");
42
- console.log(" supatype app add --static ./public");
43
- console.log(" npm run build # write files into public/");
44
- console.log(" supatype self-host compose up -d");
45
- if (opts.mode === "standalone") {
46
- console.log("\nStandalone (native TLS with ACME):");
47
- console.log(" Edit supatype.config.ts set server.domain");
48
- console.log(" supatype dev # or run supatype-server with your TLS setup");
90
+ scaffold(dir, result);
91
+ if (doInstall)
92
+ runInstall(dir, result.packageManager);
93
+ const keysGenerated = doKeys ? writeKeys(dir) : false;
94
+ printNextSteps({
95
+ name,
96
+ result,
97
+ installed: doInstall,
98
+ keysGenerated,
99
+ });
100
+ });
101
+ }
102
+ // ─── Wizard ──────────────────────────────────────────────────────────────────
103
+ async function runWizard(defaultName, defaultTarget) {
104
+ p.intro("Create a new Supatype project");
105
+ const projectName = ensureNotCancelled(await p.text({
106
+ message: "Project name",
107
+ defaultValue: defaultName,
108
+ placeholder: defaultName,
109
+ })).trim() || defaultName;
110
+ const packageManager = ensureNotCancelled(await p.select({
111
+ message: "Package manager",
112
+ initialValue: detectInvokingPackageManager(),
113
+ options: [
114
+ { value: "npm", label: "npm" },
115
+ { value: "pnpm", label: "pnpm" },
116
+ { value: "yarn", label: "yarn" },
117
+ { value: "bun", label: "bun" },
118
+ ],
119
+ }));
120
+ const productionTarget = ensureNotCancelled(await p.select({
121
+ message: "Where will this run in production?",
122
+ initialValue: defaultTarget,
123
+ options: [
124
+ { value: "cloud", label: "Supatype Cloud", hint: "managed; deploy via supatype link" },
125
+ { value: "self-host", label: "Self-host", hint: "your own server with TLS" },
126
+ { value: "later", label: "Decide later", hint: "local development only for now" },
127
+ ],
128
+ }));
129
+ let domain;
130
+ let tlsEmail;
131
+ if (productionTarget === "self-host") {
132
+ domain = ensureNotCancelled(await p.text({
133
+ message: "Production domain for ACME TLS (optional, can set later)",
134
+ placeholder: "api.example.com",
135
+ defaultValue: "",
136
+ })).trim();
137
+ if (domain) {
138
+ tlsEmail =
139
+ ensureNotCancelled(await p.text({
140
+ message: "Email for Let's Encrypt (HTTPS) certificates",
141
+ placeholder: "you@example.com",
142
+ defaultValue: "",
143
+ })).trim() || undefined;
49
144
  }
50
- console.log();
145
+ }
146
+ const provider = ensureNotCancelled(await p.select({
147
+ message: "How should Postgres and the server run for local development?",
148
+ initialValue: "docker",
149
+ options: [
150
+ { value: "docker", label: "Docker", hint: "Docker Compose stack (recommended)" },
151
+ { value: "native", label: "Native", hint: "host Postgres + server binaries, no Docker" },
152
+ ],
153
+ }));
154
+ const schemaPath = ensureNotCancelled(await p.text({
155
+ message: "Where should your schema live?",
156
+ defaultValue: "schema/index.ts",
157
+ placeholder: "schema/index.ts",
158
+ })).trim() || "schema/index.ts";
159
+ const app = await promptApp();
160
+ const email = ensureNotCancelled(await p.select({
161
+ message: "Email provider",
162
+ initialValue: "console",
163
+ options: [
164
+ { value: "console", label: "console", hint: "log emails to the terminal (dev)" },
165
+ { value: "smtp", label: "SMTP" },
166
+ { value: "resend", label: "Resend" },
167
+ { value: "ses", label: "Amazon SES" },
168
+ ],
169
+ }));
170
+ const storageLocal = ensureNotCancelled(await p.select({
171
+ message: "Local storage (for development)?",
172
+ initialValue: "local",
173
+ options: STORAGE_PROVIDER_OPTIONS,
174
+ }));
175
+ const storageProduction = ensureNotCancelled(await p.select({
176
+ message: "Production storage?",
177
+ initialValue: "local",
178
+ options: STORAGE_PROVIDER_OPTIONS,
179
+ }));
180
+ const helloFunction = ensureNotCancelled(await p.confirm({
181
+ message: "Create a hello-world edge function?",
182
+ initialValue: false,
183
+ }));
184
+ const install = ensureNotCancelled(await p.confirm({
185
+ message: `Install dependencies with ${packageManager} now?`,
186
+ initialValue: true,
187
+ }));
188
+ const generateKeys = ensureNotCancelled(await p.confirm({
189
+ message: "Generate ANON_KEY and SERVICE_ROLE_KEY now?",
190
+ initialValue: true,
191
+ }));
192
+ p.outro("Setting up your project...");
193
+ return {
194
+ projectName,
195
+ provider,
196
+ productionTarget,
197
+ ...(domain !== undefined ? { domain } : {}),
198
+ ...(tlsEmail !== undefined ? { tlsEmail } : {}),
199
+ schemaPath,
200
+ app,
201
+ email,
202
+ storageLocal,
203
+ storageProduction,
204
+ helloFunction,
205
+ packageManager,
206
+ install,
207
+ generateKeys,
208
+ };
209
+ }
210
+ async function promptApp() {
211
+ const mode = ensureNotCancelled(await p.select({
212
+ message: "Host a frontend app at /?",
213
+ initialValue: "none",
214
+ options: [
215
+ { value: "none", label: "No app", hint: "API only" },
216
+ { value: "static", label: "Static site", hint: "serve a built directory" },
217
+ { value: "proxy", label: "Local dev server", hint: "forward requests to a dev server you run" },
218
+ ],
219
+ }));
220
+ if (mode === "static") {
221
+ const staticDir = ensureNotCancelled(await p.text({
222
+ message: "Directory to serve",
223
+ defaultValue: "./public",
224
+ placeholder: "./public",
225
+ })).trim() || "./public";
226
+ const viteDevUrl = await promptViteDevUrl();
227
+ return { mode, staticDir, ...(viteDevUrl ? { viteDevUrl } : {}) };
228
+ }
229
+ if (mode === "proxy") {
230
+ const upstream = ensureNotCancelled(await p.text({
231
+ message: "URL of your running dev server",
232
+ defaultValue: "http://localhost:3000",
233
+ placeholder: "http://localhost:3000",
234
+ })).trim() || "http://localhost:3000";
235
+ const start = ensureNotCancelled(await p.text({
236
+ message: "package.json script that starts your dev server",
237
+ defaultValue: "dev",
238
+ placeholder: "dev",
239
+ })).trim() || "dev";
240
+ const viteDevUrl = await promptViteDevUrl();
241
+ return { mode, upstream, start, ...(viteDevUrl ? { viteDevUrl } : {}) };
242
+ }
243
+ return { mode: "none" };
244
+ }
245
+ async function promptViteDevUrl() {
246
+ const useVite = ensureNotCancelled(await p.confirm({
247
+ message: "Enable live reload from a separate Vite dev server?",
248
+ initialValue: false,
249
+ }));
250
+ if (!useVite)
251
+ return undefined;
252
+ return (ensureNotCancelled(await p.text({
253
+ message: "Vite dev server URL",
254
+ defaultValue: "http://127.0.0.1:5173",
255
+ placeholder: "http://127.0.0.1:5173",
256
+ })).trim() || "http://127.0.0.1:5173");
257
+ }
258
+ // ─── Package manager ───────────────────────────────────────────────────────--
259
+ function detectInvokingPackageManager() {
260
+ const ua = process.env["npm_config_user_agent"] ?? "";
261
+ if (ua.startsWith("pnpm"))
262
+ return "pnpm";
263
+ if (ua.startsWith("yarn"))
264
+ return "yarn";
265
+ if (ua.startsWith("bun"))
266
+ return "bun";
267
+ return "npm";
268
+ }
269
+ function runInstall(dir, pm) {
270
+ console.log(`\nInstalling dependencies with ${pm}...`);
271
+ const res = spawnSync(pm, ["install"], {
272
+ cwd: dir,
273
+ stdio: "inherit",
274
+ shell: process.platform === "win32",
51
275
  });
276
+ if (res.status !== 0 || res.error) {
277
+ console.warn(`\n[supatype] Dependency install did not complete (run "${pm} install" manually).`);
278
+ }
52
279
  }
53
- function scaffold(dir, projectName, mode = "dev") {
280
+ function writeKeys(dir) {
281
+ const keys = generateAndWriteKeys(dir);
282
+ if (!keys) {
283
+ console.warn("\n[supatype] Could not generate keys (JWT_SECRET missing). Run `supatype keys` manually.");
284
+ return false;
285
+ }
286
+ return true;
287
+ }
288
+ // ─── Scaffold ──────────────────────────────────────────────────────────────--
289
+ function scaffold(dir, optsOrName) {
290
+ const opts = typeof optsOrName === "string" ? defaultScaffoldOptions(optsOrName) : optsOrName;
54
291
  const write = (rel, content) => {
55
292
  const full = join(dir, rel);
56
293
  mkdirSync(resolve(full, ".."), { recursive: true });
@@ -59,17 +296,28 @@ function scaffold(dir, projectName, mode = "dev") {
59
296
  };
60
297
  const pkgPath = join(dir, "package.json");
61
298
  if (!existsSync(pkgPath)) {
62
- write("package.json", packageJsonTemplate(projectName, cliPackageVersion()));
299
+ write("package.json", packageJsonTemplate(opts, cliPackageVersion()));
63
300
  }
64
301
  else {
65
302
  console.log(" skipped package.json (already exists)");
66
303
  }
67
- write("supatype.config.ts", tsConfigTemplate(projectName, mode));
68
- write("schema/index.ts", schemaTemplate());
69
- write(".env", envTemplate(projectName));
70
- write("seed.ts", seedTemplate(projectName));
304
+ write("supatype.config.ts", tsConfigTemplate(opts));
305
+ if (opts.productionTarget !== "later") {
306
+ write("supatype.local.config.ts", localConfigTemplate());
307
+ }
308
+ write(opts.schemaPath, schemaTemplate());
309
+ write(".env", envTemplate(opts));
310
+ write("seed.ts", seedTemplate(opts.projectName));
71
311
  write("seeds/.gitkeep", "");
72
- write("public/.gitkeep", "");
312
+ if (opts.app.mode === "static") {
313
+ const staticRel = staticDirRelative(opts.app.staticDir);
314
+ write(`${staticRel}/.gitkeep`, "");
315
+ }
316
+ else {
317
+ write("public/.gitkeep", "");
318
+ }
319
+ if (opts.helloFunction)
320
+ scaffoldHelloFunction(dir, write);
73
321
  const gitignorePath = join(dir, ".gitignore");
74
322
  if (existsSync(gitignorePath)) {
75
323
  const merged = mergeGitignoreTemplate(readFileSync(gitignorePath, "utf8"));
@@ -85,16 +333,35 @@ function scaffold(dir, projectName, mode = "dev") {
85
333
  write(".gitignore", gitignoreTemplate());
86
334
  }
87
335
  }
336
+ function staticDirRelative(staticDir) {
337
+ const raw = (staticDir ?? "./public").trim();
338
+ return raw.replace(/^\.\//, "").replace(/\/+$/, "") || "public";
339
+ }
340
+ function scaffoldHelloFunction(dir, write) {
341
+ write("functions/hello/index.ts", helloFunctionTemplate());
342
+ if (!existsSync(join(dir, "functions/_shared/README.md"))) {
343
+ write("functions/_shared/README.md", sharedFunctionsReadme());
344
+ }
345
+ if (!existsSync(join(dir, "functions/.env.local"))) {
346
+ write("functions/.env.local", functionsEnvLocalTemplate());
347
+ }
348
+ }
88
349
  // ─── Templates ───────────────────────────────────────────────────────────────
89
- function packageJsonTemplate(projectName, cliVersion) {
350
+ function packageJsonTemplate(opts, cliVersion) {
351
+ const scripts = [
352
+ ` "dev": "supatype dev"`,
353
+ ` "push": "supatype push"`,
354
+ ` "seed": "tsx seed.ts"`,
355
+ ];
356
+ if (opts.helloFunction) {
357
+ scripts.push(` "functions": "supatype functions serve"`);
358
+ }
90
359
  return `{
91
- "name": "${projectName}",
360
+ "name": "${opts.projectName}",
92
361
  "private": true,
93
362
  "type": "module",
94
363
  "scripts": {
95
- "dev": "supatype dev",
96
- "push": "supatype push",
97
- "seed": "tsx seed.ts"
364
+ ${scripts.join(",\n")}
98
365
  },
99
366
  "dependencies": {
100
367
  "@supatype/cli": "^${cliVersion}",
@@ -107,53 +374,125 @@ function packageJsonTemplate(projectName, cliVersion) {
107
374
  }
108
375
  `;
109
376
  }
110
- function tsConfigTemplate(projectName, mode) {
111
- const domainField = mode === "standalone"
112
- ? ` domain: "", // e.g. "api.example.com" for ACME TLS\n`
113
- : "";
114
- return `import { defineConfig } from "@supatype/cli"
377
+ function tsConfigTemplate(opts) {
378
+ const serverMode = serverModeForTarget(opts.productionTarget);
379
+ const hasLocalOverride = opts.productionTarget !== "later";
380
+ const lines = [];
381
+ lines.push(`import { defineConfig } from "@supatype/cli"`);
382
+ lines.push("");
383
+ if (hasLocalOverride) {
384
+ lines.push(`// Committed config = ${opts.productionTarget} production target.`);
385
+ lines.push(`// Local development overrides live in supatype.local.config.ts (gitignored).`);
386
+ }
387
+ lines.push(`export default defineConfig({`);
388
+ lines.push(` project: { name: "${opts.projectName}" },`);
389
+ lines.push(` provider: "${opts.provider}",`);
390
+ if (opts.provider === "docker") {
391
+ lines.push(` // provider: "native" // host Postgres + supatype-server binaries (no Docker)`);
392
+ }
393
+ lines.push(` database: {`);
394
+ lines.push(` provider: "${opts.provider}",`);
395
+ lines.push(` },`);
396
+ lines.push(` server: {`);
397
+ lines.push(` mode: "${serverMode}",`);
398
+ lines.push(` port: 54321,`);
399
+ if (serverMode === "standalone") {
400
+ lines.push(` domain: "${opts.domain ?? ""}", // e.g. "api.example.com" for ACME TLS`);
401
+ if (opts.tlsEmail) {
402
+ lines.push(` tls: { email: "${opts.tlsEmail}" }, // automatic HTTPS via Let's Encrypt`);
403
+ }
404
+ else {
405
+ lines.push(` // tls: { email: "you@example.com" }, // set to enable automatic HTTPS (Let's Encrypt)`);
406
+ }
407
+ }
408
+ lines.push(` },`);
409
+ lines.push(...appConfigLines(opts.app));
410
+ if (opts.productionTarget !== "later") {
411
+ lines.push(` environments: { default: "production" }, // supatype link --env production ...`);
412
+ }
413
+ lines.push(` // Optional: pin component versions (native cache + Docker images synced to .env on dev/push)`);
414
+ lines.push(` // versions: { engine: "0.1.2", server: "1.0.5", postgres: "17.2", deno: "2.2.0" },`);
415
+ lines.push(` email: { provider: "${opts.email}" },`);
416
+ lines.push(...storageConfigLines(opts.storageLocal, opts.storageProduction));
417
+ lines.push(` schema: { path: "${opts.schemaPath}", pg_schema: "public" },`);
418
+ lines.push(` // Self-host production: supatype self-host compose (Docker only). Standalone + domain = native ACME dev.`);
419
+ lines.push(`})`);
420
+ return lines.join("\n") + "\n";
421
+ }
422
+ function localConfigTemplate() {
423
+ return `import type { SupatypeConfig } from "@supatype/cli"
115
424
 
116
- export default defineConfig({
117
- project: { name: "${projectName}" },
118
- provider: "docker",
119
- // provider: "native" // host Postgres + supatype-server binaries (no Docker)
120
- database: {
121
- provider: "docker",
122
- },
123
- server: {
124
- mode: "${mode}",
125
- port: 54321,
126
- ${domainField} },
127
- app: {
128
- mode: "none",
129
- // mode: "static", static_dir: "./public", // supatype app add --static ./public
130
- // mode: "proxy", upstream: "http://localhost:3000", start: "dev",
131
- // vite_dev_url: "http://127.0.0.1:5173", // dev HMR at /_vite (when using a separate Vite server)
132
- },
133
- // Optional: pin component versions (native cache + Docker images synced to .env on dev/push)
134
- // versions: { engine: "0.1.2", server: "1.0.5", postgres: "17.2", deno: "2.2.0" },
135
- email: { provider: "console" },
136
- storage: { provider: "local", local_path: ".supatype/storage" },
137
- schema: { path: "schema/index.ts", pg_schema: "public" },
138
- // Self-host production: supatype self-host compose (Docker only). Standalone + domain = native ACME dev.
139
- })
425
+ // Local development overrides — gitignored, deep-merged over supatype.config.ts.
426
+ // Keeps \`supatype dev\` in local mode while the committed config targets production.
427
+ const localConfig: Partial<SupatypeConfig> = {
428
+ server: { mode: "dev" },
429
+ }
430
+
431
+ export default localConfig
140
432
  `;
141
433
  }
434
+ function appConfigLines(app) {
435
+ if (app.mode === "static") {
436
+ const out = [
437
+ ` app: {`,
438
+ ` mode: "static",`,
439
+ ` static_dir: "${app.staticDir ?? "./public"}",`,
440
+ ];
441
+ if (app.viteDevUrl)
442
+ out.push(` vite_dev_url: "${app.viteDevUrl}",`);
443
+ out.push(` },`);
444
+ return out;
445
+ }
446
+ if (app.mode === "proxy") {
447
+ const out = [
448
+ ` app: {`,
449
+ ` mode: "proxy",`,
450
+ ` upstream: "${app.upstream ?? "http://localhost:3000"}",`,
451
+ ` start: "${app.start ?? "dev"}",`,
452
+ ];
453
+ if (app.viteDevUrl)
454
+ out.push(` vite_dev_url: "${app.viteDevUrl}",`);
455
+ out.push(` },`);
456
+ return out;
457
+ }
458
+ return [
459
+ ` app: {`,
460
+ ` mode: "none",`,
461
+ ` // mode: "static", static_dir: "./public", // supatype app add --static ./public`,
462
+ ` // mode: "proxy", upstream: "http://localhost:3000", start: "dev",`,
463
+ ` // vite_dev_url: "http://127.0.0.1:5173", // live reload from a separate Vite dev server`,
464
+ ` },`,
465
+ ];
466
+ }
467
+ function storageConfigLines(storageLocal, storageProduction) {
468
+ const lines = [];
469
+ if (storageLocal === "s3") {
470
+ lines.push(` storage: { provider: "s3" }, // dev — configure S3_* in .env`);
471
+ }
472
+ else {
473
+ lines.push(` storage: { provider: "local", local_path: ".supatype/storage" },`);
474
+ }
475
+ if (storageProduction === "s3" && storageLocal !== "s3") {
476
+ lines.push(` // Production storage: external S3 bucket — set production S3_* in .env`);
477
+ }
478
+ else if (storageProduction === "local" && storageLocal === "s3") {
479
+ lines.push(` // Production storage: MinIO on your server (included in self-host compose)`);
480
+ }
481
+ return lines;
482
+ }
142
483
  function schemaTemplate() {
143
- return `import type { Model, Public, Owner, Role, SupatypeAuthUserId, Unique, Email, UUID } from "@supatype/types"
484
+ return `import type { Model, LoggedIn, Owner, Public, Role, SupatypeAuthUserId, UUID } from "@supatype/types"
144
485
 
145
- export type User = Model<{
486
+ /** App profile for a signed-in user. \`id\` matches the Supatype auth user id. */
487
+ export type Profile = Model<{
146
488
  id: SupatypeAuthUserId
147
- email: Unique<Email>
148
- name: string
149
- created_at: string
150
- updated_at: string
489
+ display_name: string
151
490
  }, {
152
491
  access: {
153
- read: Public
154
- create: Public
492
+ read: LoggedIn
493
+ create: Owner<"id">
155
494
  update: Owner<"id">
156
- delete: Role<"admin">
495
+ delete: Owner<"id">
157
496
  }
158
497
  }>
159
498
 
@@ -170,34 +509,95 @@ export type SiteSettings = Model<{
170
509
  }>
171
510
  `;
172
511
  }
173
- function envTemplate(projectName) {
174
- return `DATABASE_URL=postgresql://supatype_admin:postgres@localhost:5432/${projectName}
512
+ function envTemplate(opts) {
513
+ const sections = [];
514
+ sections.push(`DATABASE_URL=postgresql://supatype_admin:postgres@localhost:5432/${opts.projectName}
175
515
  POSTGRES_USER=supatype_admin
176
516
  POSTGRES_PASSWORD=postgres
177
- POSTGRES_DB=${projectName}
178
-
179
- # JWT — run \`supatype keys\` to generate ANON_KEY and SERVICE_ROLE_KEY
517
+ POSTGRES_DB=${opts.projectName}`);
518
+ sections.push(`# JWT — run \`supatype keys\` to generate ANON_KEY and SERVICE_ROLE_KEY
180
519
  JWT_SECRET=super-secret-jwt-token-change-in-production
181
520
  ANON_KEY=
182
- SERVICE_ROLE_KEY=
183
-
184
- # Site URL (used by GoTrue for email redirects)
185
- SITE_URL=http://localhost:3000
186
-
187
- # SMTP leave empty to use email autoconfirm in dev (no emails sent)
521
+ SERVICE_ROLE_KEY=`);
522
+ sections.push(`# Site URL (used by GoTrue for email redirects)
523
+ SITE_URL=http://localhost:3000`);
524
+ sections.push(emailEnvSection(opts.email, opts.projectName));
525
+ sections.push(storageEnvSections(opts.storageLocal, opts.storageProduction));
526
+ sections.push(`# Self-host compose uses the same DATABASE_URL when Postgres is published on localhost:5432`);
527
+ return sections.join("\n\n") + "\n";
528
+ }
529
+ function emailEnvSection(email, projectName) {
530
+ switch (email) {
531
+ case "resend":
532
+ return `# Email (Resend)
533
+ RESEND_API_KEY=
534
+ RESEND_FROM=onboarding@resend.dev`;
535
+ case "ses":
536
+ return `# Email (Amazon SES)
537
+ SES_FROM=
538
+ AWS_REGION=us-east-1
539
+ AWS_ACCESS_KEY_ID=
540
+ AWS_SECRET_ACCESS_KEY=`;
541
+ case "smtp":
542
+ return `# Email (SMTP)
543
+ SMTP_HOST=
544
+ SMTP_PORT=587
545
+ SMTP_USER=
546
+ SMTP_PASS=
547
+ SMTP_SENDER_NAME=${projectName}`;
548
+ case "console":
549
+ default:
550
+ return `# SMTP — leave empty to use email autoconfirm in dev (no emails sent)
188
551
  SMTP_HOST=
189
552
  SMTP_PORT=
190
553
  SMTP_USER=
191
554
  SMTP_PASS=
192
- SMTP_SENDER_NAME=${projectName}
555
+ SMTP_SENDER_NAME=${projectName}`;
556
+ }
557
+ }
558
+ function storageEnvSections(storageLocal, storageProduction) {
559
+ if (storageLocal === storageProduction) {
560
+ if (storageLocal === "s3") {
561
+ return `# Storage (local development and production — external bucket)
562
+ # Use separate buckets for dev and production in your provider.
563
+ S3_ENDPOINT=
564
+ S3_REGION=us-east-1
565
+ S3_BUCKET=
566
+ S3_ACCESS_KEY=
567
+ S3_SECRET_KEY=`;
568
+ }
569
+ return `${localStorageEnvSection("local")}
193
570
 
194
- # Storage (MinIO for local dev)
571
+ # Production storage (MinIO on your server)
572
+ # Included in the self-host compose stack — no extra configuration needed.`;
573
+ }
574
+ return [localStorageEnvSection(storageLocal), productionStorageEnvSection(storageProduction)].join("\n\n");
575
+ }
576
+ function localStorageEnvSection(storage) {
577
+ if (storage === "s3") {
578
+ return `# Storage (local development — external bucket)
579
+ S3_ENDPOINT=
580
+ S3_REGION=us-east-1
581
+ S3_BUCKET=
582
+ S3_ACCESS_KEY=
583
+ S3_SECRET_KEY=`;
584
+ }
585
+ return `# Storage (local development — MinIO)
195
586
  S3_ENDPOINT=http://localhost:9000
196
587
  S3_ACCESS_KEY=supatype
197
- S3_SECRET_KEY=supatype-secret
198
-
199
- # Self-host compose uses the same DATABASE_URL when Postgres is published on localhost:5432
200
- `;
588
+ S3_SECRET_KEY=supatype-secret`;
589
+ }
590
+ function productionStorageEnvSection(storage) {
591
+ if (storage === "s3") {
592
+ return `# Storage (production — external bucket)
593
+ S3_ENDPOINT=
594
+ S3_REGION=us-east-1
595
+ S3_BUCKET=
596
+ S3_ACCESS_KEY=
597
+ S3_SECRET_KEY=`;
598
+ }
599
+ return `# Storage (production — MinIO on your server)
600
+ # Included in the self-host compose stack — no extra configuration needed.`;
201
601
  }
202
602
  function seedTemplate(projectName) {
203
603
  return `import { sql } from "@supatype/cli/seed"
@@ -212,7 +612,7 @@ async function seed() {
212
612
  console.log("Seeding ${projectName}...")
213
613
 
214
614
  // TODO: insert seed data
215
- // await db\`INSERT INTO users (email, name) VALUES ('admin@example.com', 'Admin')\`
615
+ // await db\`INSERT INTO profile (id, display_name) VALUES ('...', 'Admin')\`
216
616
 
217
617
  await db.end()
218
618
  console.log("Done.")
@@ -224,6 +624,34 @@ seed().catch((e) => {
224
624
  })
225
625
  `;
226
626
  }
627
+ function helloFunctionTemplate() {
628
+ return `// hello — Supatype Edge Function
629
+ // Docs: https://supatype.com/docs/edge-functions
630
+
631
+ export default async function handler(req: Request): Promise<Response> {
632
+ const { method } = req
633
+
634
+ if (method === "POST") {
635
+ const body = await req.json()
636
+ return new Response(JSON.stringify({ message: "Hello from hello!", received: body }), {
637
+ status: 200,
638
+ headers: { "Content-Type": "application/json" },
639
+ })
640
+ }
641
+
642
+ return new Response(JSON.stringify({ message: "Hello from hello!" }), {
643
+ status: 200,
644
+ headers: { "Content-Type": "application/json" },
645
+ })
646
+ }
647
+ `;
648
+ }
649
+ function sharedFunctionsReadme() {
650
+ return "# Shared Code\n\nFiles in `_shared/` are available to all functions via relative imports.\nThis directory is not deployed as a function.\n\nExample: `import { sendEmail } from '../_shared/email.ts'`\n";
651
+ }
652
+ function functionsEnvLocalTemplate() {
653
+ return "# Local environment variables for edge functions\n# These are NOT committed to git\n# Set production env vars via: npx supatype functions env set KEY=value\n";
654
+ }
227
655
  function gitignoreTemplate() {
228
656
  return `.env
229
657
  node_modules/
@@ -247,4 +675,55 @@ export function mergeGitignoreTemplate(existingContent) {
247
675
  `;
248
676
  return existingContent.endsWith("\n") ? `${existingContent}${block}` : `${existingContent}\n${block}`;
249
677
  }
678
+ // ─── Next steps ────────────────────────────────────────────────────────────--
679
+ function printNextSteps(args) {
680
+ const { name, result, installed, keysGenerated } = args;
681
+ console.log(`\nSupatype project ready${name ? ` in ${name}/` : ""}.\n`);
682
+ console.log("Next steps:");
683
+ if (name)
684
+ console.log(` cd ${name}`);
685
+ if (!installed)
686
+ console.log(` ${result.packageManager} install`);
687
+ if (!keysGenerated)
688
+ console.log(" supatype keys");
689
+ console.log(" supatype dev # Docker Compose stack (Kong :18473)");
690
+ console.log(" supatype push # apply schema + generate types");
691
+ if (result.helloFunction) {
692
+ console.log(" supatype functions serve # run edge functions locally");
693
+ }
694
+ if (result.app.mode === "none") {
695
+ console.log("\nStatic frontend (self-host):");
696
+ console.log(" supatype app add --static ./public");
697
+ console.log(" npm run build # write files into public/");
698
+ console.log(" supatype self-host compose up -d");
699
+ }
700
+ if (result.productionTarget === "cloud") {
701
+ console.log("\nDeploy to Supatype Cloud:");
702
+ console.log(" supatype login");
703
+ console.log(" supatype link --env production --project <ref>");
704
+ console.log(" supatype push --env production");
705
+ console.log("\nsupatype.local.config.ts keeps `supatype dev` local while the committed config targets cloud.");
706
+ }
707
+ else if (result.productionTarget === "self-host") {
708
+ console.log("\nSelf-host production (your own server):");
709
+ const domain = result.domain?.trim();
710
+ if (domain) {
711
+ console.log(` 1. Point DNS: an A record for ${domain} -> your server's public IP`);
712
+ console.log(" 2. Open ports 80 and 443 on the server firewall");
713
+ if (!result.tlsEmail) {
714
+ console.log(" 3. Set server.tls.email in supatype.config.ts (required for HTTPS)");
715
+ }
716
+ console.log(" supatype self-host compose up -d # Kong provisions HTTPS automatically");
717
+ console.log(` Your Supatype platform goes live at https://${domain}`);
718
+ console.log(" Your app, REST, Auth, Storage, Realtime, Functions, and Studio — all behind one HTTPS domain (certs persist in valkey-data)");
719
+ }
720
+ else {
721
+ console.log(" Set server.domain + server.tls.email in supatype.config.ts to enable automatic HTTPS");
722
+ console.log(" supatype self-host compose up -d # Docker stack");
723
+ }
724
+ console.log(" supatype link --env production ... # then: supatype push --env production");
725
+ console.log("\nsupatype.local.config.ts keeps `supatype dev` local while the committed config targets self-host.");
726
+ }
727
+ console.log();
728
+ }
250
729
  //# sourceMappingURL=init.js.map