@supatype/cli 0.1.2 → 0.1.4
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 +111 -108
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/commands/admin.d.ts +12 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +97 -23
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +287 -182
- package/dist/commands/init.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/dev-compose.js +3 -3
- package/dist/dev-compose.js.map +1 -1
- package/dist/dev-log-bus.d.ts +1 -1
- package/dist/dev-log-bus.d.ts.map +1 -1
- package/dist/dev-log-bus.js +5 -3
- package/dist/dev-log-bus.js.map +1 -1
- package/dist/dev-ready-panel.d.ts +2 -0
- package/dist/dev-ready-panel.d.ts.map +1 -1
- package/dist/dev-ready-panel.js +18 -0
- package/dist/dev-ready-panel.js.map +1 -1
- package/dist/init-project-detect.d.ts +14 -0
- package/dist/init-project-detect.d.ts.map +1 -0
- package/dist/init-project-detect.js +89 -0
- package/dist/init-project-detect.js.map +1 -0
- package/dist/type-extractor.js +44 -0
- package/dist/type-extractor.js.map +1 -1
- package/dist/ui/dev/DevDashboard.d.ts.map +1 -1
- package/dist/ui/dev/DevDashboard.js +11 -4
- package/dist/ui/dev/DevDashboard.js.map +1 -1
- package/dist/ui/dev/DevReadyPanelView.d.ts +2 -1
- package/dist/ui/dev/DevReadyPanelView.d.ts.map +1 -1
- package/dist/ui/dev/DevReadyPanelView.js +21 -2
- package/dist/ui/dev/DevReadyPanelView.js.map +1 -1
- package/dist/ui/flows/FlowApp.d.ts.map +1 -1
- package/dist/ui/flows/FlowApp.js +3 -1
- package/dist/ui/flows/FlowApp.js.map +1 -1
- package/dist/ui/flows/prompt-fields.d.ts.map +1 -1
- package/dist/ui/flows/prompt-fields.js +9 -5
- package/dist/ui/flows/prompt-fields.js.map +1 -1
- package/dist/ui/runtime/run-clack-flow.d.ts.map +1 -1
- package/dist/ui/runtime/run-clack-flow.js +15 -3
- package/dist/ui/runtime/run-clack-flow.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts +3 -0
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -0
- package/dist/ui/runtime/stdin-after-ink.js +16 -0
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/admin.ts +135 -26
- package/src/commands/init.ts +360 -213
- package/src/config.ts +13 -1
- package/src/dev-compose.ts +4 -4
- package/src/dev-log-bus.ts +5 -3
- package/src/dev-ready-panel.ts +17 -0
- package/src/init-project-detect.ts +99 -0
- package/src/type-extractor.ts +57 -0
- package/src/ui/dev/DevDashboard.tsx +14 -4
- package/src/ui/dev/DevReadyPanelView.tsx +76 -9
- package/src/ui/flows/FlowApp.tsx +3 -1
- package/src/ui/flows/prompt-fields.tsx +13 -7
- package/src/ui/runtime/run-clack-flow.tsx +16 -3
- package/src/ui/runtime/stdin-after-ink.ts +18 -0
- package/tests/admin-ensure.test.ts +80 -0
- package/tests/config.test.ts +19 -0
- package/tests/dev-ready-panel.test.ts +18 -1
- package/tests/init-project-detect.test.ts +53 -0
- package/tests/stdin-after-ink.test.ts +8 -0
- package/tests/type-extractor.test.ts +101 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/init.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
3
3
|
import { resolve, join, basename } from "node:path"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
5
|
import { p, runClackFlow } from "../ui/clack.js"
|
|
6
|
-
import { ensureNotCancelled
|
|
6
|
+
import { ensureNotCancelled } from "../ui/prompts.js"
|
|
7
7
|
import { generateAndWriteKeys } from "./keys.js"
|
|
8
8
|
import { file, error, info, plain, warn } from "../ui/messages.js"
|
|
9
9
|
import { nextSteps } from "../ui/next-steps.js"
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
resolveInitDependencyVersions,
|
|
21
21
|
type InitDependencyVersions,
|
|
22
22
|
} from "../init-dependency-versions.js"
|
|
23
|
+
import { detectProjectSetup, type DetectedProjectSetup } from "../init-project-detect.js"
|
|
23
24
|
|
|
24
25
|
// ─── Options model ─────────────────────────────────────────────────────────--
|
|
25
26
|
|
|
@@ -155,8 +156,14 @@ export function registerInit(program: Command): void {
|
|
|
155
156
|
|
|
156
157
|
let result: WizardResult
|
|
157
158
|
if (interactive) {
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
await runInteractiveInit({
|
|
160
|
+
name,
|
|
161
|
+
dir,
|
|
162
|
+
defaultName,
|
|
163
|
+
modeTarget,
|
|
164
|
+
opts,
|
|
165
|
+
})
|
|
166
|
+
return
|
|
160
167
|
} else {
|
|
161
168
|
result = {
|
|
162
169
|
...defaultScaffoldOptions(defaultName, modeTarget),
|
|
@@ -199,14 +206,25 @@ export function registerInit(program: Command): void {
|
|
|
199
206
|
|
|
200
207
|
scaffold(dir, result, deps)
|
|
201
208
|
|
|
202
|
-
const installOk = doInstall ? runInstall(dir, result.packageManager) :
|
|
209
|
+
const installOk = doInstall ? runInstall(dir, result.packageManager) : false
|
|
210
|
+
const keysGenerated = doKeys ? writeKeys(dir) : false
|
|
211
|
+
const binariesReady =
|
|
212
|
+
doInstall && installOk ? await ensureInitBinaries(dir) : false
|
|
203
213
|
|
|
204
|
-
const binariesReady = installOk ? await ensureInitBinaries(dir) : false
|
|
205
214
|
if (doInstall && !installOk) {
|
|
206
|
-
|
|
215
|
+
error(
|
|
216
|
+
`Dependency install failed. Run "${result.packageManager} install" in the project directory, then "supatype update".`,
|
|
217
|
+
)
|
|
218
|
+
process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
if (doKeys && !keysGenerated) {
|
|
221
|
+
error("Could not generate API keys. Check .env has JWT_SECRET, then run `supatype keys`.")
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
if (doInstall && installOk && !binariesReady) {
|
|
225
|
+
error("Component binaries could not be downloaded. Run `supatype update` in the project directory.")
|
|
226
|
+
process.exit(1)
|
|
207
227
|
}
|
|
208
|
-
|
|
209
|
-
const keysGenerated = doKeys ? writeKeys(dir) : false
|
|
210
228
|
|
|
211
229
|
warnDockerUnavailableForProvider(result.provider)
|
|
212
230
|
|
|
@@ -220,206 +238,304 @@ export function registerInit(program: Command): void {
|
|
|
220
238
|
})
|
|
221
239
|
}
|
|
222
240
|
|
|
223
|
-
|
|
241
|
+
interface InteractiveInitArgs {
|
|
242
|
+
name: string | undefined
|
|
243
|
+
dir: string
|
|
244
|
+
defaultName: string
|
|
245
|
+
modeTarget: ProductionTarget
|
|
246
|
+
opts: InitCliOptions
|
|
247
|
+
}
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
/** Interactive init — one Ink session for wizard + scaffold + install + finish. */
|
|
250
|
+
async function runInteractiveInit(args: InteractiveInitArgs): Promise<void> {
|
|
251
|
+
const { name, dir, defaultName, modeTarget, opts } = args
|
|
252
|
+
|
|
253
|
+
await runClackFlow(async () => {
|
|
254
|
+
p.intro("Create a new Supatype project")
|
|
255
|
+
const detected = detectProjectSetup(dir)
|
|
256
|
+
if (detected.hasExistingFiles && detected.summaryLines.length > 0) {
|
|
257
|
+
p.note(
|
|
258
|
+
["Existing project detected:", ...detected.summaryLines.map((line) => ` • ${line}`)].join(
|
|
259
|
+
"\n",
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
let result = await collectWizardAnswers(defaultName, modeTarget, dir, detected)
|
|
264
|
+
|
|
265
|
+
if (opts.admin === false) {
|
|
266
|
+
delete result.adminEmail
|
|
267
|
+
delete result.adminPassword
|
|
268
|
+
} else if (opts.adminEmail && opts.adminPassword) {
|
|
269
|
+
result.adminEmail = opts.adminEmail
|
|
270
|
+
result.adminPassword = opts.adminPassword
|
|
271
|
+
}
|
|
231
272
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
message: "Project name",
|
|
235
|
-
defaultValue: defaultName,
|
|
236
|
-
placeholder: defaultName,
|
|
237
|
-
}),
|
|
238
|
-
).trim() || defaultName
|
|
273
|
+
const doInstall = opts.install !== false && result.install
|
|
274
|
+
const doKeys = opts.keys !== false && result.generateKeys
|
|
239
275
|
|
|
240
|
-
|
|
241
|
-
await p.select<PackageManager>({
|
|
242
|
-
message: "Package manager",
|
|
243
|
-
initialValue: detectInvokingPackageManager(),
|
|
244
|
-
options: [
|
|
245
|
-
{ value: "npm", label: "npm" },
|
|
246
|
-
{ value: "pnpm", label: "pnpm" },
|
|
247
|
-
{ value: "yarn", label: "yarn" },
|
|
248
|
-
{ value: "bun", label: "bun" },
|
|
249
|
-
],
|
|
250
|
-
}),
|
|
251
|
-
)
|
|
276
|
+
if (name) mkdirSync(dir, { recursive: true })
|
|
252
277
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
{ value: "later", label: "Decide later", hint: "local development only for now" },
|
|
261
|
-
],
|
|
262
|
-
}),
|
|
263
|
-
)
|
|
278
|
+
const deps = await resolveInitDependencyVersions()
|
|
279
|
+
const runningCli = cliPackageVersion()
|
|
280
|
+
if (deps.cli !== runningCli || deps.types !== runningCli) {
|
|
281
|
+
info(
|
|
282
|
+
`Using published npm versions: @supatype/cli ^${deps.cli}, @supatype/types ^${deps.types}`,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
264
285
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (domain) {
|
|
276
|
-
tlsEmail =
|
|
277
|
-
ensureNotCancelled(
|
|
278
|
-
await p.text({
|
|
279
|
-
message: "Email for Let's Encrypt (HTTPS) certificates",
|
|
280
|
-
placeholder: "you@example.com",
|
|
281
|
-
defaultValue: "",
|
|
282
|
-
}),
|
|
283
|
-
).trim() || undefined
|
|
286
|
+
const setupSpinner = p.spinner()
|
|
287
|
+
setupSpinner.start("Setting up your project...")
|
|
288
|
+
|
|
289
|
+
scaffold(dir, result, deps)
|
|
290
|
+
|
|
291
|
+
let installOk = !doInstall
|
|
292
|
+
if (doInstall) {
|
|
293
|
+
setupSpinner.start(`Installing dependencies with ${result.packageManager}...`)
|
|
294
|
+
installOk = runInstall(dir, result.packageManager)
|
|
295
|
+
setupSpinner.stop(installOk ? "Dependencies installed." : "Dependency install failed.")
|
|
284
296
|
}
|
|
285
|
-
}
|
|
286
297
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
{ value: "native", label: "Native", hint: "host Postgres + server binaries, no Docker" },
|
|
294
|
-
],
|
|
295
|
-
}),
|
|
296
|
-
)
|
|
298
|
+
let keysGenerated = !doKeys
|
|
299
|
+
if (doKeys) {
|
|
300
|
+
setupSpinner.start("Generating API keys...")
|
|
301
|
+
keysGenerated = writeKeys(dir)
|
|
302
|
+
setupSpinner.stop(keysGenerated ? "API keys generated." : "Could not generate API keys.")
|
|
303
|
+
}
|
|
297
304
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
let binariesReady = false
|
|
306
|
+
if (doInstall && installOk) {
|
|
307
|
+
setupSpinner.start("Preparing component binaries...")
|
|
308
|
+
binariesReady = await ensureInitBinaries(dir)
|
|
309
|
+
setupSpinner.stop(binariesReady ? "Component binaries ready." : "Some binaries are missing.")
|
|
310
|
+
}
|
|
303
311
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
312
|
+
if (doInstall && !installOk) {
|
|
313
|
+
error(
|
|
314
|
+
`Dependency install failed. Run "${result.packageManager} install" in the project directory, then "supatype update".`,
|
|
315
|
+
)
|
|
316
|
+
process.exit(1)
|
|
317
|
+
}
|
|
318
|
+
if (doKeys && !keysGenerated) {
|
|
319
|
+
error("Could not generate API keys. Check .env has JWT_SECRET, then run `supatype keys`.")
|
|
320
|
+
process.exit(1)
|
|
321
|
+
}
|
|
322
|
+
if (doInstall && installOk && !binariesReady) {
|
|
323
|
+
error("Component binaries could not be downloaded. Run `supatype update` in the project directory.")
|
|
324
|
+
process.exit(1)
|
|
325
|
+
}
|
|
311
326
|
|
|
312
|
-
|
|
327
|
+
warnDockerUnavailableForProvider(result.provider)
|
|
313
328
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
],
|
|
324
|
-
}),
|
|
325
|
-
)
|
|
329
|
+
p.outro(`Supatype project ready${name ? ` in ${name}/` : ""}.`)
|
|
330
|
+
printNextSteps({
|
|
331
|
+
name,
|
|
332
|
+
result,
|
|
333
|
+
installed: doInstall && installOk,
|
|
334
|
+
keysGenerated,
|
|
335
|
+
binariesReady,
|
|
336
|
+
})
|
|
337
|
+
})
|
|
326
338
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
message: "Local storage (for development)?",
|
|
330
|
-
initialValue: "local",
|
|
331
|
-
options: STORAGE_PROVIDER_OPTIONS,
|
|
332
|
-
}),
|
|
333
|
-
)
|
|
339
|
+
process.exit(0)
|
|
340
|
+
}
|
|
334
341
|
|
|
335
|
-
|
|
336
|
-
await p.select<StorageProvider>({
|
|
337
|
-
message: "Production storage?",
|
|
338
|
-
initialValue: "local",
|
|
339
|
-
options: STORAGE_PROVIDER_OPTIONS,
|
|
340
|
-
}),
|
|
341
|
-
)
|
|
342
|
+
// ─── Wizard ──────────────────────────────────────────────────────────────────
|
|
342
343
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
344
|
+
async function collectWizardAnswers(
|
|
345
|
+
defaultName: string,
|
|
346
|
+
defaultTarget: ProductionTarget,
|
|
347
|
+
dir: string,
|
|
348
|
+
detected: DetectedProjectSetup,
|
|
349
|
+
): Promise<WizardResult> {
|
|
350
|
+
const projectName = ensureNotCancelled(
|
|
351
|
+
await p.text({
|
|
352
|
+
message: "Project name",
|
|
353
|
+
defaultValue: defaultName,
|
|
354
|
+
placeholder: defaultName,
|
|
355
|
+
}),
|
|
356
|
+
).trim() || defaultName
|
|
357
|
+
|
|
358
|
+
const admin = await promptAdminUser()
|
|
359
|
+
|
|
360
|
+
const packageManager = ensureNotCancelled(
|
|
361
|
+
await p.select<PackageManager>({
|
|
362
|
+
message: "Package manager",
|
|
363
|
+
initialValue: detectInvokingPackageManager(),
|
|
364
|
+
options: [
|
|
365
|
+
{ value: "npm", label: "npm" },
|
|
366
|
+
{ value: "pnpm", label: "pnpm" },
|
|
367
|
+
{ value: "yarn", label: "yarn" },
|
|
368
|
+
{ value: "bun", label: "bun" },
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
371
|
+
)
|
|
349
372
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
const productionTarget = ensureNotCancelled(
|
|
374
|
+
await p.select<ProductionTarget>({
|
|
375
|
+
message: "Where will this run in production?",
|
|
376
|
+
initialValue: defaultTarget,
|
|
377
|
+
options: [
|
|
378
|
+
{ value: "cloud", label: "Supatype Cloud", hint: "managed; deploy via supatype link" },
|
|
379
|
+
{ value: "self-host", label: "Self-host", hint: "your own server with TLS" },
|
|
380
|
+
{ value: "later", label: "Decide later", hint: "local development only for now" },
|
|
381
|
+
],
|
|
382
|
+
}),
|
|
383
|
+
)
|
|
356
384
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
385
|
+
let domain: string | undefined
|
|
386
|
+
let tlsEmail: string | undefined
|
|
387
|
+
if (productionTarget === "self-host") {
|
|
388
|
+
domain = ensureNotCancelled(
|
|
389
|
+
await p.text({
|
|
390
|
+
message: "Production domain for ACME TLS (optional, can set later)",
|
|
391
|
+
placeholder: "api.example.com",
|
|
392
|
+
defaultValue: "",
|
|
393
|
+
}),
|
|
394
|
+
).trim()
|
|
395
|
+
if (domain) {
|
|
396
|
+
tlsEmail =
|
|
397
|
+
ensureNotCancelled(
|
|
398
|
+
await p.text({
|
|
399
|
+
message: "Email for Let's Encrypt (HTTPS) certificates",
|
|
400
|
+
placeholder: "you@example.com",
|
|
401
|
+
defaultValue: "",
|
|
402
|
+
}),
|
|
403
|
+
).trim() || undefined
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const provider = ensureNotCancelled(
|
|
408
|
+
await p.select<ScaffoldOptions["provider"]>({
|
|
409
|
+
message: "How should Postgres and the server run for local development?",
|
|
410
|
+
initialValue: "docker",
|
|
411
|
+
options: [
|
|
412
|
+
{ value: "docker", label: "Docker", hint: "Docker Compose stack (recommended)" },
|
|
413
|
+
{ value: "native", label: "Native", hint: "host Postgres + server binaries, no Docker" },
|
|
414
|
+
],
|
|
415
|
+
}),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
let kongPort: number | undefined
|
|
419
|
+
if (provider === "docker") {
|
|
420
|
+
const { promptKongPortChoice } = await import("../dev-ports.js")
|
|
421
|
+
kongPort = await promptKongPortChoice()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const defaultSchemaPath = existsSync(join(dir, "schema/index.ts"))
|
|
425
|
+
? "schema/index.ts"
|
|
426
|
+
: "schema/index.ts"
|
|
427
|
+
const schemaPath = ensureNotCancelled(
|
|
428
|
+
await p.text({
|
|
429
|
+
message: "Where should your schema live?",
|
|
430
|
+
defaultValue: defaultSchemaPath,
|
|
431
|
+
placeholder: "schema/index.ts",
|
|
432
|
+
}),
|
|
433
|
+
).trim() || defaultSchemaPath
|
|
434
|
+
|
|
435
|
+
const app = await promptApp(detected, productionTarget)
|
|
436
|
+
|
|
437
|
+
const email = ensureNotCancelled(
|
|
438
|
+
await p.select<ScaffoldOptions["email"]>({
|
|
439
|
+
message: "Email provider",
|
|
440
|
+
initialValue: "console",
|
|
441
|
+
options: [
|
|
442
|
+
{ value: "console", label: "console", hint: "log emails to the terminal (dev)" },
|
|
443
|
+
{ value: "smtp", label: "SMTP" },
|
|
444
|
+
{ value: "resend", label: "Resend" },
|
|
445
|
+
{ value: "ses", label: "Amazon SES" },
|
|
446
|
+
],
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
const storageLocal = ensureNotCancelled(
|
|
451
|
+
await p.select<StorageProvider>({
|
|
452
|
+
message: "Local storage (for development)?",
|
|
453
|
+
initialValue: "local",
|
|
454
|
+
options: STORAGE_PROVIDER_OPTIONS,
|
|
455
|
+
}),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
const storageProduction = ensureNotCancelled(
|
|
459
|
+
await p.select<StorageProvider>({
|
|
460
|
+
message: "Production storage?",
|
|
461
|
+
initialValue: "local",
|
|
462
|
+
options: STORAGE_PROVIDER_OPTIONS,
|
|
463
|
+
}),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
const helloFunction = ensureNotCancelled(
|
|
467
|
+
await p.confirm({
|
|
468
|
+
message: "Create a hello-world edge function?",
|
|
469
|
+
initialValue: false,
|
|
470
|
+
}),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
projectName,
|
|
475
|
+
provider,
|
|
476
|
+
...(kongPort !== undefined ? { kongPort } : {}),
|
|
477
|
+
productionTarget,
|
|
478
|
+
...(domain !== undefined ? { domain } : {}),
|
|
479
|
+
...(tlsEmail !== undefined ? { tlsEmail } : {}),
|
|
480
|
+
schemaPath,
|
|
481
|
+
app,
|
|
482
|
+
email,
|
|
483
|
+
storageLocal,
|
|
484
|
+
storageProduction,
|
|
485
|
+
helloFunction,
|
|
486
|
+
packageManager,
|
|
487
|
+
install: true,
|
|
488
|
+
generateKeys: true,
|
|
489
|
+
...admin,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
363
492
|
|
|
364
|
-
|
|
365
|
-
let adminPassword: string | undefined
|
|
493
|
+
async function promptAdminUser(): Promise<{ adminEmail?: string; adminPassword?: string }> {
|
|
366
494
|
const createAdmin = ensureNotCancelled(
|
|
367
495
|
await p.confirm({
|
|
368
496
|
message: "Create an admin user for /admin?",
|
|
369
497
|
initialValue: true,
|
|
370
498
|
}),
|
|
371
499
|
)
|
|
372
|
-
if (createAdmin) {
|
|
373
|
-
adminEmail =
|
|
374
|
-
ensureNotCancelled(
|
|
375
|
-
await p.text({
|
|
376
|
-
message: "Admin email",
|
|
377
|
-
placeholder: "you@example.com",
|
|
378
|
-
defaultValue: "",
|
|
379
|
-
}),
|
|
380
|
-
).trim() || undefined
|
|
381
|
-
if (adminEmail) {
|
|
382
|
-
adminPassword =
|
|
383
|
-
ensureNotCancelled(
|
|
384
|
-
await p.text({
|
|
385
|
-
message: "Admin password (min 8 chars)",
|
|
386
|
-
defaultValue: "",
|
|
387
|
-
}),
|
|
388
|
-
).trim() || undefined
|
|
389
|
-
if (adminPassword && adminPassword.length < 8) {
|
|
390
|
-
adminPassword = undefined
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
500
|
+
if (!createAdmin) return {}
|
|
394
501
|
|
|
395
|
-
p.
|
|
502
|
+
p.note("First sign-in at /admin — created automatically on supatype dev or push.")
|
|
396
503
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
generateKeys,
|
|
413
|
-
...(adminEmail && adminPassword ? { adminEmail, adminPassword } : {}),
|
|
504
|
+
const adminEmail = ensureNotCancelled(
|
|
505
|
+
await p.text({
|
|
506
|
+
message: "Admin email",
|
|
507
|
+
placeholder: "you@example.com",
|
|
508
|
+
validate: (value) => (value.trim() ? undefined : "Email is required"),
|
|
509
|
+
}),
|
|
510
|
+
).trim()
|
|
511
|
+
|
|
512
|
+
let adminPassword = ""
|
|
513
|
+
while (true) {
|
|
514
|
+
adminPassword = ensureNotCancelled(
|
|
515
|
+
await p.password({ message: "Admin password (min 8 characters)" }),
|
|
516
|
+
).trim()
|
|
517
|
+
if (adminPassword.length >= 8) break
|
|
518
|
+
p.log.warn("Password must be at least 8 characters.")
|
|
414
519
|
}
|
|
415
|
-
|
|
520
|
+
|
|
521
|
+
return { adminEmail, adminPassword }
|
|
416
522
|
}
|
|
417
523
|
|
|
418
|
-
async function promptApp(
|
|
524
|
+
async function promptApp(
|
|
525
|
+
detected: DetectedProjectSetup,
|
|
526
|
+
productionTarget: ProductionTarget,
|
|
527
|
+
): Promise<ScaffoldAppOptions> {
|
|
528
|
+
const initialMode: ScaffoldAppOptions["mode"] =
|
|
529
|
+
detected.hasVite && productionTarget !== "later"
|
|
530
|
+
? "static"
|
|
531
|
+
: detected.hasVite
|
|
532
|
+
? "proxy"
|
|
533
|
+
: "none"
|
|
534
|
+
|
|
419
535
|
const mode = ensureNotCancelled(
|
|
420
536
|
await p.select<ScaffoldAppOptions["mode"]>({
|
|
421
537
|
message: "Host a frontend app at /?",
|
|
422
|
-
initialValue:
|
|
538
|
+
initialValue: initialMode,
|
|
423
539
|
options: [
|
|
424
540
|
{ value: "none", label: "No app", hint: "API only" },
|
|
425
541
|
{ value: "static", label: "Static site", hint: "serve a built directory" },
|
|
@@ -432,11 +548,11 @@ async function promptApp(): Promise<ScaffoldAppOptions> {
|
|
|
432
548
|
const staticDir = ensureNotCancelled(
|
|
433
549
|
await p.text({
|
|
434
550
|
message: "Directory to serve",
|
|
435
|
-
defaultValue:
|
|
436
|
-
placeholder:
|
|
551
|
+
defaultValue: detected.staticDir,
|
|
552
|
+
placeholder: detected.staticDir,
|
|
437
553
|
}),
|
|
438
|
-
).trim() ||
|
|
439
|
-
const viteDevUrl = await promptViteDevUrl()
|
|
554
|
+
).trim() || detected.staticDir
|
|
555
|
+
const viteDevUrl = await promptViteDevUrl(detected, productionTarget)
|
|
440
556
|
return { mode, staticDir, ...(viteDevUrl ? { viteDevUrl } : {}) }
|
|
441
557
|
}
|
|
442
558
|
|
|
@@ -444,40 +560,49 @@ async function promptApp(): Promise<ScaffoldAppOptions> {
|
|
|
444
560
|
const upstream = ensureNotCancelled(
|
|
445
561
|
await p.text({
|
|
446
562
|
message: "URL of your running dev server",
|
|
447
|
-
defaultValue: "http://localhost:3000",
|
|
448
|
-
placeholder: "http://localhost:3000",
|
|
563
|
+
defaultValue: detected.hasVite ? detected.viteDevUrl : "http://localhost:3000",
|
|
564
|
+
placeholder: detected.hasVite ? detected.viteDevUrl : "http://localhost:3000",
|
|
449
565
|
}),
|
|
450
|
-
).trim() || "http://localhost:3000"
|
|
566
|
+
).trim() || (detected.hasVite ? detected.viteDevUrl : "http://localhost:3000")
|
|
451
567
|
const start = ensureNotCancelled(
|
|
452
568
|
await p.text({
|
|
453
569
|
message: "package.json script that starts your dev server",
|
|
454
|
-
defaultValue: "dev",
|
|
455
|
-
placeholder: "dev",
|
|
570
|
+
defaultValue: detected.hasVite ? "vite" : "dev",
|
|
571
|
+
placeholder: detected.hasVite ? "vite" : "dev",
|
|
456
572
|
}),
|
|
457
|
-
).trim() || "dev"
|
|
458
|
-
const viteDevUrl = await promptViteDevUrl()
|
|
573
|
+
).trim() || (detected.hasVite ? "vite" : "dev")
|
|
574
|
+
const viteDevUrl = await promptViteDevUrl(detected, productionTarget)
|
|
459
575
|
return { mode, upstream, start, ...(viteDevUrl ? { viteDevUrl } : {}) }
|
|
460
576
|
}
|
|
461
577
|
|
|
462
578
|
return { mode: "none" }
|
|
463
579
|
}
|
|
464
580
|
|
|
465
|
-
async function promptViteDevUrl(
|
|
581
|
+
async function promptViteDevUrl(
|
|
582
|
+
detected: DetectedProjectSetup,
|
|
583
|
+
productionTarget: ProductionTarget,
|
|
584
|
+
): Promise<string | undefined> {
|
|
585
|
+
const defaultYes = detected.hasVite || productionTarget !== "later"
|
|
466
586
|
const useVite = ensureNotCancelled(
|
|
467
587
|
await p.confirm({
|
|
468
|
-
message:
|
|
469
|
-
|
|
588
|
+
message: detected.hasVite
|
|
589
|
+
? "Use Vite for local development?"
|
|
590
|
+
: "Develop locally with Vite (live reload)?",
|
|
591
|
+
initialValue: defaultYes,
|
|
470
592
|
}),
|
|
471
593
|
)
|
|
472
594
|
if (!useVite) return undefined
|
|
595
|
+
|
|
596
|
+
p.note(`Press Enter for ${detected.viteDevUrl}, or type a custom URL.`)
|
|
597
|
+
|
|
473
598
|
return (
|
|
474
599
|
ensureNotCancelled(
|
|
475
600
|
await p.text({
|
|
476
601
|
message: "Vite dev server URL",
|
|
477
|
-
defaultValue:
|
|
478
|
-
placeholder:
|
|
602
|
+
defaultValue: detected.viteDevUrl,
|
|
603
|
+
placeholder: detected.viteDevUrl,
|
|
479
604
|
}),
|
|
480
|
-
).trim() ||
|
|
605
|
+
).trim() || detected.viteDevUrl
|
|
481
606
|
)
|
|
482
607
|
}
|
|
483
608
|
|
|
@@ -492,14 +617,12 @@ function detectInvokingPackageManager(): PackageManager {
|
|
|
492
617
|
}
|
|
493
618
|
|
|
494
619
|
function runInstall(dir: string, pm: PackageManager): boolean {
|
|
495
|
-
info(`Installing dependencies with ${pm}...`)
|
|
496
620
|
const res = spawnSync(pm, ["install"], {
|
|
497
621
|
cwd: dir,
|
|
498
622
|
stdio: "inherit",
|
|
499
623
|
shell: process.platform === "win32",
|
|
500
624
|
})
|
|
501
625
|
if (res.status !== 0 || res.error) {
|
|
502
|
-
warn(`Dependency install did not complete (run "${pm} install" manually).`)
|
|
503
626
|
return false
|
|
504
627
|
}
|
|
505
628
|
return true
|
|
@@ -574,7 +697,7 @@ export function scaffold(
|
|
|
574
697
|
write(".env", envTemplate(opts))
|
|
575
698
|
write("seed.ts", seedTemplate(opts.projectName))
|
|
576
699
|
write("seeds/.gitkeep", "")
|
|
577
|
-
scaffoldAppAssets(opts, write)
|
|
700
|
+
scaffoldAppAssets(dir, opts, write)
|
|
578
701
|
|
|
579
702
|
if (opts.helloFunction) scaffoldHelloFunction(dir, write)
|
|
580
703
|
|
|
@@ -598,45 +721,63 @@ function staticDirRelative(staticDir?: string): string {
|
|
|
598
721
|
}
|
|
599
722
|
|
|
600
723
|
function scaffoldAppAssets(
|
|
724
|
+
dir: string,
|
|
601
725
|
opts: ScaffoldOptions,
|
|
602
726
|
write: (rel: string, content: string) => void,
|
|
603
727
|
): void {
|
|
728
|
+
const writeUnlessExists = (rel: string, content: string) => {
|
|
729
|
+
const full = join(dir, rel)
|
|
730
|
+
if (existsSync(full)) {
|
|
731
|
+
file("skipped", `${rel} (already exists)`)
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
write(rel, content)
|
|
735
|
+
}
|
|
736
|
+
|
|
604
737
|
const holding = holdingPageTemplate(opts.projectName)
|
|
605
738
|
const hasVite = Boolean(opts.app.viteDevUrl)
|
|
606
739
|
|
|
607
740
|
if (opts.app.mode === "static") {
|
|
608
741
|
const staticRel = staticDirRelative(opts.app.staticDir)
|
|
609
742
|
if (opts.app.viteDevUrl && opts.productionTarget !== "later") {
|
|
610
|
-
|
|
611
|
-
scaffoldVite(opts,
|
|
743
|
+
writeUnlessExists("dist/index.html", holding)
|
|
744
|
+
scaffoldVite(dir, opts, writeUnlessExists, holding)
|
|
612
745
|
return
|
|
613
746
|
}
|
|
614
|
-
|
|
615
|
-
if (opts.app.viteDevUrl) scaffoldVite(opts,
|
|
747
|
+
writeUnlessExists(`${staticRel}/index.html`, holding)
|
|
748
|
+
if (opts.app.viteDevUrl) scaffoldVite(dir, opts, writeUnlessExists, holding)
|
|
616
749
|
return
|
|
617
750
|
}
|
|
618
751
|
|
|
619
752
|
if (opts.app.mode === "proxy" && opts.productionTarget !== "later") {
|
|
620
|
-
|
|
621
|
-
if (hasVite) scaffoldVite(opts,
|
|
753
|
+
writeUnlessExists("dist/index.html", holding)
|
|
754
|
+
if (hasVite) scaffoldVite(dir, opts, writeUnlessExists, holding)
|
|
622
755
|
return
|
|
623
756
|
}
|
|
624
757
|
|
|
625
758
|
if (opts.app.mode === "proxy" && hasVite) {
|
|
626
|
-
scaffoldVite(opts,
|
|
759
|
+
scaffoldVite(dir, opts, writeUnlessExists, holding)
|
|
627
760
|
return
|
|
628
761
|
}
|
|
629
762
|
|
|
630
|
-
|
|
763
|
+
writeUnlessExists("public/.gitkeep", "")
|
|
631
764
|
}
|
|
632
765
|
|
|
633
766
|
function scaffoldVite(
|
|
767
|
+
dir: string,
|
|
634
768
|
opts: ScaffoldOptions,
|
|
635
769
|
write: (rel: string, content: string) => void,
|
|
636
770
|
holding: string,
|
|
637
771
|
): void {
|
|
638
772
|
if (!opts.app.viteDevUrl) return
|
|
639
773
|
write("index.html", holding)
|
|
774
|
+
const viteConfigRel = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"].find(
|
|
775
|
+
(name) => existsSync(join(dir, name)),
|
|
776
|
+
)
|
|
777
|
+
if (viteConfigRel) {
|
|
778
|
+
file("skipped", `${viteConfigRel} (already exists)`)
|
|
779
|
+
return
|
|
780
|
+
}
|
|
640
781
|
write("vite.config.ts", viteConfigTemplate(opts.app.viteDevUrl))
|
|
641
782
|
}
|
|
642
783
|
|
|
@@ -1265,26 +1406,32 @@ function printNextSteps(args: {
|
|
|
1265
1406
|
binariesReady: boolean
|
|
1266
1407
|
}): void {
|
|
1267
1408
|
const { name, result, installed, keysGenerated, binariesReady } = args
|
|
1409
|
+
const setupComplete = installed && keysGenerated && binariesReady
|
|
1268
1410
|
const steps: string[] = []
|
|
1269
1411
|
if (name) steps.push(`cd ${name}`)
|
|
1270
|
-
if (!installed) steps.push(`${result.packageManager} install`)
|
|
1271
|
-
if (!binariesReady) steps.push("supatype update # download component binaries")
|
|
1272
|
-
if (!keysGenerated) steps.push("supatype keys")
|
|
1273
1412
|
const kongHint =
|
|
1274
1413
|
result.provider === "docker" && result.kongPort !== undefined
|
|
1275
1414
|
? `supatype dev # Docker Compose stack (Kong :${result.kongPort})`
|
|
1276
1415
|
: "supatype dev # Docker Compose stack (Kong :18473)"
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1416
|
+
if (setupComplete) {
|
|
1417
|
+
steps.push(kongHint)
|
|
1418
|
+
steps.push("supatype push # apply schema + generate types")
|
|
1419
|
+
if (result.adminEmail) {
|
|
1420
|
+
steps.push(" # first admin user is created on dev or push")
|
|
1421
|
+
}
|
|
1422
|
+
} else {
|
|
1423
|
+
if (!installed) steps.push(`${result.packageManager} install`)
|
|
1424
|
+
if (!binariesReady) steps.push("supatype update # download component binaries")
|
|
1425
|
+
if (!keysGenerated) steps.push("supatype keys")
|
|
1426
|
+
steps.push(kongHint)
|
|
1427
|
+
steps.push("supatype push # apply schema + generate types")
|
|
1281
1428
|
}
|
|
1282
1429
|
if (result.helloFunction) {
|
|
1283
1430
|
steps.push("supatype functions serve # run edge functions locally")
|
|
1284
1431
|
}
|
|
1285
1432
|
|
|
1286
1433
|
info(`Supatype project ready${name ? ` in ${name}/` : ""}.`)
|
|
1287
|
-
nextSteps("Next steps:", steps)
|
|
1434
|
+
nextSteps(setupComplete ? "Next steps:" : "Finish setup:", steps)
|
|
1288
1435
|
|
|
1289
1436
|
if (result.app.mode === "none") {
|
|
1290
1437
|
nextSteps("Static frontend (self-host):", [
|