@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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +111 -108
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/commands/admin.d.ts +12 -0
  5. package/dist/commands/admin.d.ts.map +1 -1
  6. package/dist/commands/admin.js +97 -23
  7. package/dist/commands/admin.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +287 -182
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +12 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/dev-compose.js +3 -3
  15. package/dist/dev-compose.js.map +1 -1
  16. package/dist/dev-log-bus.d.ts +1 -1
  17. package/dist/dev-log-bus.d.ts.map +1 -1
  18. package/dist/dev-log-bus.js +5 -3
  19. package/dist/dev-log-bus.js.map +1 -1
  20. package/dist/dev-ready-panel.d.ts +2 -0
  21. package/dist/dev-ready-panel.d.ts.map +1 -1
  22. package/dist/dev-ready-panel.js +18 -0
  23. package/dist/dev-ready-panel.js.map +1 -1
  24. package/dist/init-project-detect.d.ts +14 -0
  25. package/dist/init-project-detect.d.ts.map +1 -0
  26. package/dist/init-project-detect.js +89 -0
  27. package/dist/init-project-detect.js.map +1 -0
  28. package/dist/type-extractor.js +44 -0
  29. package/dist/type-extractor.js.map +1 -1
  30. package/dist/ui/dev/DevDashboard.d.ts.map +1 -1
  31. package/dist/ui/dev/DevDashboard.js +11 -4
  32. package/dist/ui/dev/DevDashboard.js.map +1 -1
  33. package/dist/ui/dev/DevReadyPanelView.d.ts +2 -1
  34. package/dist/ui/dev/DevReadyPanelView.d.ts.map +1 -1
  35. package/dist/ui/dev/DevReadyPanelView.js +21 -2
  36. package/dist/ui/dev/DevReadyPanelView.js.map +1 -1
  37. package/dist/ui/flows/FlowApp.d.ts.map +1 -1
  38. package/dist/ui/flows/FlowApp.js +3 -1
  39. package/dist/ui/flows/FlowApp.js.map +1 -1
  40. package/dist/ui/flows/prompt-fields.d.ts.map +1 -1
  41. package/dist/ui/flows/prompt-fields.js +9 -5
  42. package/dist/ui/flows/prompt-fields.js.map +1 -1
  43. package/dist/ui/runtime/run-clack-flow.d.ts.map +1 -1
  44. package/dist/ui/runtime/run-clack-flow.js +15 -3
  45. package/dist/ui/runtime/run-clack-flow.js.map +1 -1
  46. package/dist/ui/runtime/stdin-after-ink.d.ts +3 -0
  47. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -0
  48. package/dist/ui/runtime/stdin-after-ink.js +16 -0
  49. package/dist/ui/runtime/stdin-after-ink.js.map +1 -0
  50. package/package.json +1 -1
  51. package/src/commands/admin.ts +135 -26
  52. package/src/commands/init.ts +360 -213
  53. package/src/config.ts +13 -1
  54. package/src/dev-compose.ts +4 -4
  55. package/src/dev-log-bus.ts +5 -3
  56. package/src/dev-ready-panel.ts +17 -0
  57. package/src/init-project-detect.ts +99 -0
  58. package/src/type-extractor.ts +57 -0
  59. package/src/ui/dev/DevDashboard.tsx +14 -4
  60. package/src/ui/dev/DevReadyPanelView.tsx +76 -9
  61. package/src/ui/flows/FlowApp.tsx +3 -1
  62. package/src/ui/flows/prompt-fields.tsx +13 -7
  63. package/src/ui/runtime/run-clack-flow.tsx +16 -3
  64. package/src/ui/runtime/stdin-after-ink.ts +18 -0
  65. package/tests/admin-ensure.test.ts +80 -0
  66. package/tests/config.test.ts +19 -0
  67. package/tests/dev-ready-panel.test.ts +18 -1
  68. package/tests/init-project-detect.test.ts +53 -0
  69. package/tests/stdin-after-ink.test.ts +8 -0
  70. package/tests/type-extractor.test.ts +101 -0
  71. package/tsconfig.tsbuildinfo +1 -1
@@ -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, printLogo } from "../ui/prompts.js"
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
- printLogo()
159
- result = await runWizard(defaultName, modeTarget)
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) : true
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
- warn("Skipping component binary check until dependencies install successfully.")
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
- // ─── Wizard ──────────────────────────────────────────────────────────────────
241
+ interface InteractiveInitArgs {
242
+ name: string | undefined
243
+ dir: string
244
+ defaultName: string
245
+ modeTarget: ProductionTarget
246
+ opts: InitCliOptions
247
+ }
224
248
 
225
- async function runWizard(
226
- defaultName: string,
227
- defaultTarget: ProductionTarget,
228
- ): Promise<WizardResult> {
229
- return runClackFlow(async () => {
230
- p.intro("Create a new Supatype project")
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
- const projectName = ensureNotCancelled(
233
- await p.text({
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
- const packageManager = ensureNotCancelled(
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
- const productionTarget = ensureNotCancelled(
254
- await p.select<ProductionTarget>({
255
- message: "Where will this run in production?",
256
- initialValue: defaultTarget,
257
- options: [
258
- { value: "cloud", label: "Supatype Cloud", hint: "managed; deploy via supatype link" },
259
- { value: "self-host", label: "Self-host", hint: "your own server with TLS" },
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
- let domain: string | undefined
266
- let tlsEmail: string | undefined
267
- if (productionTarget === "self-host") {
268
- domain = ensureNotCancelled(
269
- await p.text({
270
- message: "Production domain for ACME TLS (optional, can set later)",
271
- placeholder: "api.example.com",
272
- defaultValue: "",
273
- }),
274
- ).trim()
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
- const provider = ensureNotCancelled(
288
- await p.select<ScaffoldOptions["provider"]>({
289
- message: "How should Postgres and the server run for local development?",
290
- initialValue: "docker",
291
- options: [
292
- { value: "docker", label: "Docker", hint: "Docker Compose stack (recommended)" },
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
- let kongPort: number | undefined
299
- if (provider === "docker") {
300
- const { promptKongPortChoice } = await import("../dev-ports.js")
301
- kongPort = await promptKongPortChoice()
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
- const schemaPath = ensureNotCancelled(
305
- await p.text({
306
- message: "Where should your schema live?",
307
- defaultValue: "schema/index.ts",
308
- placeholder: "schema/index.ts",
309
- }),
310
- ).trim() || "schema/index.ts"
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
- const app = await promptApp()
327
+ warnDockerUnavailableForProvider(result.provider)
313
328
 
314
- const email = ensureNotCancelled(
315
- await p.select<ScaffoldOptions["email"]>({
316
- message: "Email provider",
317
- initialValue: "console",
318
- options: [
319
- { value: "console", label: "console", hint: "log emails to the terminal (dev)" },
320
- { value: "smtp", label: "SMTP" },
321
- { value: "resend", label: "Resend" },
322
- { value: "ses", label: "Amazon SES" },
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
- const storageLocal = ensureNotCancelled(
328
- await p.select<StorageProvider>({
329
- message: "Local storage (for development)?",
330
- initialValue: "local",
331
- options: STORAGE_PROVIDER_OPTIONS,
332
- }),
333
- )
339
+ process.exit(0)
340
+ }
334
341
 
335
- const storageProduction = ensureNotCancelled(
336
- await p.select<StorageProvider>({
337
- message: "Production storage?",
338
- initialValue: "local",
339
- options: STORAGE_PROVIDER_OPTIONS,
340
- }),
341
- )
342
+ // ─── Wizard ──────────────────────────────────────────────────────────────────
342
343
 
343
- const helloFunction = ensureNotCancelled(
344
- await p.confirm({
345
- message: "Create a hello-world edge function?",
346
- initialValue: false,
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
- const install = ensureNotCancelled(
351
- await p.confirm({
352
- message: `Install dependencies with ${packageManager} now?`,
353
- initialValue: true,
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
- const generateKeys = ensureNotCancelled(
358
- await p.confirm({
359
- message: "Generate ANON_KEY and SERVICE_ROLE_KEY now?",
360
- initialValue: true,
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
- let adminEmail: string | undefined
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.outro("Setting up your project...")
502
+ p.note("First sign-in at /admin — created automatically on supatype dev or push.")
396
503
 
397
- return {
398
- projectName,
399
- provider,
400
- ...(kongPort !== undefined ? { kongPort } : {}),
401
- productionTarget,
402
- ...(domain !== undefined ? { domain } : {}),
403
- ...(tlsEmail !== undefined ? { tlsEmail } : {}),
404
- schemaPath,
405
- app,
406
- email,
407
- storageLocal,
408
- storageProduction,
409
- helloFunction,
410
- packageManager,
411
- install,
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(): Promise<ScaffoldAppOptions> {
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: "none",
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: "./public",
436
- placeholder: "./public",
551
+ defaultValue: detected.staticDir,
552
+ placeholder: detected.staticDir,
437
553
  }),
438
- ).trim() || "./public"
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(): Promise<string | undefined> {
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: "Enable live reload from a separate Vite dev server?",
469
- initialValue: false,
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: "http://127.0.0.1:5173",
478
- placeholder: "http://127.0.0.1:5173",
602
+ defaultValue: detected.viteDevUrl,
603
+ placeholder: detected.viteDevUrl,
479
604
  }),
480
- ).trim() || "http://127.0.0.1:5173"
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
- write("dist/index.html", holding)
611
- scaffoldVite(opts, write, holding)
743
+ writeUnlessExists("dist/index.html", holding)
744
+ scaffoldVite(dir, opts, writeUnlessExists, holding)
612
745
  return
613
746
  }
614
- write(`${staticRel}/index.html`, holding)
615
- if (opts.app.viteDevUrl) scaffoldVite(opts, write, holding)
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
- write("dist/index.html", holding)
621
- if (hasVite) scaffoldVite(opts, write, holding)
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, write, holding)
759
+ scaffoldVite(dir, opts, writeUnlessExists, holding)
627
760
  return
628
761
  }
629
762
 
630
- write("public/.gitkeep", "")
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
- steps.push(kongHint)
1278
- steps.push("supatype push # apply schema + generate types")
1279
- if (result.adminEmail) {
1280
- steps.push(" # first admin user is created on dev or push")
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):", [