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