@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
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve, join, basename } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { p, runClackFlow } from "../ui/clack.js";
5
- import { ensureNotCancelled, printLogo } from "../ui/prompts.js";
5
+ import { ensureNotCancelled } from "../ui/prompts.js";
6
6
  import { generateAndWriteKeys } from "./keys.js";
7
7
  import { file, error, info, plain, warn } from "../ui/messages.js";
8
8
  import { nextSteps } from "../ui/next-steps.js";
@@ -12,6 +12,7 @@ import { ensureComponentBinaries, reportComponentBinaryFailures, } from "../ensu
12
12
  import { mergeSupatypePackageJson } from "../init-package-json.js";
13
13
  import { cliPackageVersion } from "../cli-package-version.js";
14
14
  import { initDependencyVersionsFallback, resolveInitDependencyVersions, } from "../init-dependency-versions.js";
15
+ import { detectProjectSetup } from "../init-project-detect.js";
15
16
  const STORAGE_PROVIDER_OPTIONS = [
16
17
  { value: "local", label: "Local", hint: "storage you host yourself (MinIO)" },
17
18
  { value: "s3", label: "S3", hint: "external bucket (AWS S3 or compatible)" },
@@ -67,8 +68,14 @@ export function registerInit(program) {
67
68
  const modeTarget = productionTargetFromMode(opts.mode);
68
69
  let result;
69
70
  if (interactive) {
70
- printLogo();
71
- result = await runWizard(defaultName, modeTarget);
71
+ await runInteractiveInit({
72
+ name,
73
+ dir,
74
+ defaultName,
75
+ modeTarget,
76
+ opts,
77
+ });
78
+ return;
72
79
  }
73
80
  else {
74
81
  result = {
@@ -106,12 +113,21 @@ export function registerInit(program) {
106
113
  info(`Using published npm versions: @supatype/cli ^${deps.cli}, @supatype/types ^${deps.types}`);
107
114
  }
108
115
  scaffold(dir, result, deps);
109
- const installOk = doInstall ? runInstall(dir, result.packageManager) : true;
110
- const binariesReady = installOk ? await ensureInitBinaries(dir) : false;
116
+ const installOk = doInstall ? runInstall(dir, result.packageManager) : false;
117
+ const keysGenerated = doKeys ? writeKeys(dir) : false;
118
+ const binariesReady = doInstall && installOk ? await ensureInitBinaries(dir) : false;
111
119
  if (doInstall && !installOk) {
112
- warn("Skipping component binary check until dependencies install successfully.");
120
+ error(`Dependency install failed. Run "${result.packageManager} install" in the project directory, then "supatype update".`);
121
+ process.exit(1);
122
+ }
123
+ if (doKeys && !keysGenerated) {
124
+ error("Could not generate API keys. Check .env has JWT_SECRET, then run `supatype keys`.");
125
+ process.exit(1);
126
+ }
127
+ if (doInstall && installOk && !binariesReady) {
128
+ error("Component binaries could not be downloaded. Run `supatype update` in the project directory.");
129
+ process.exit(1);
113
130
  }
114
- const keysGenerated = doKeys ? writeKeys(dir) : false;
115
131
  warnDockerUnavailableForProvider(result.provider);
116
132
  printNextSteps({
117
133
  name,
@@ -122,151 +138,218 @@ export function registerInit(program) {
122
138
  });
123
139
  });
124
140
  }
125
- // ─── Wizard ──────────────────────────────────────────────────────────────────
126
- async function runWizard(defaultName, defaultTarget) {
127
- return runClackFlow(async () => {
141
+ /** Interactive init — one Ink session for wizard + scaffold + install + finish. */
142
+ async function runInteractiveInit(args) {
143
+ const { name, dir, defaultName, modeTarget, opts } = args;
144
+ await runClackFlow(async () => {
128
145
  p.intro("Create a new Supatype project");
129
- const projectName = ensureNotCancelled(await p.text({
130
- message: "Project name",
131
- defaultValue: defaultName,
132
- placeholder: defaultName,
133
- })).trim() || defaultName;
134
- const packageManager = ensureNotCancelled(await p.select({
135
- message: "Package manager",
136
- initialValue: detectInvokingPackageManager(),
137
- options: [
138
- { value: "npm", label: "npm" },
139
- { value: "pnpm", label: "pnpm" },
140
- { value: "yarn", label: "yarn" },
141
- { value: "bun", label: "bun" },
142
- ],
143
- }));
144
- const productionTarget = ensureNotCancelled(await p.select({
145
- message: "Where will this run in production?",
146
- initialValue: defaultTarget,
147
- options: [
148
- { value: "cloud", label: "Supatype Cloud", hint: "managed; deploy via supatype link" },
149
- { value: "self-host", label: "Self-host", hint: "your own server with TLS" },
150
- { value: "later", label: "Decide later", hint: "local development only for now" },
151
- ],
152
- }));
153
- let domain;
154
- let tlsEmail;
155
- if (productionTarget === "self-host") {
156
- domain = ensureNotCancelled(await p.text({
157
- message: "Production domain for ACME TLS (optional, can set later)",
158
- placeholder: "api.example.com",
159
- defaultValue: "",
160
- })).trim();
161
- if (domain) {
162
- tlsEmail =
163
- ensureNotCancelled(await p.text({
164
- message: "Email for Let's Encrypt (HTTPS) certificates",
165
- placeholder: "you@example.com",
166
- defaultValue: "",
167
- })).trim() || undefined;
168
- }
146
+ const detected = detectProjectSetup(dir);
147
+ if (detected.hasExistingFiles && detected.summaryLines.length > 0) {
148
+ p.note(["Existing project detected:", ...detected.summaryLines.map((line) => ` • ${line}`)].join("\n"));
149
+ }
150
+ let result = await collectWizardAnswers(defaultName, modeTarget, dir, detected);
151
+ if (opts.admin === false) {
152
+ delete result.adminEmail;
153
+ delete result.adminPassword;
169
154
  }
170
- const provider = ensureNotCancelled(await p.select({
171
- message: "How should Postgres and the server run for local development?",
172
- initialValue: "docker",
173
- options: [
174
- { value: "docker", label: "Docker", hint: "Docker Compose stack (recommended)" },
175
- { value: "native", label: "Native", hint: "host Postgres + server binaries, no Docker" },
176
- ],
177
- }));
178
- let kongPort;
179
- if (provider === "docker") {
180
- const { promptKongPortChoice } = await import("../dev-ports.js");
181
- kongPort = await promptKongPortChoice();
155
+ else if (opts.adminEmail && opts.adminPassword) {
156
+ result.adminEmail = opts.adminEmail;
157
+ result.adminPassword = opts.adminPassword;
182
158
  }
183
- const schemaPath = ensureNotCancelled(await p.text({
184
- message: "Where should your schema live?",
185
- defaultValue: "schema/index.ts",
186
- placeholder: "schema/index.ts",
187
- })).trim() || "schema/index.ts";
188
- const app = await promptApp();
189
- const email = ensureNotCancelled(await p.select({
190
- message: "Email provider",
191
- initialValue: "console",
192
- options: [
193
- { value: "console", label: "console", hint: "log emails to the terminal (dev)" },
194
- { value: "smtp", label: "SMTP" },
195
- { value: "resend", label: "Resend" },
196
- { value: "ses", label: "Amazon SES" },
197
- ],
198
- }));
199
- const storageLocal = ensureNotCancelled(await p.select({
200
- message: "Local storage (for development)?",
201
- initialValue: "local",
202
- options: STORAGE_PROVIDER_OPTIONS,
203
- }));
204
- const storageProduction = ensureNotCancelled(await p.select({
205
- message: "Production storage?",
206
- initialValue: "local",
207
- options: STORAGE_PROVIDER_OPTIONS,
208
- }));
209
- const helloFunction = ensureNotCancelled(await p.confirm({
210
- message: "Create a hello-world edge function?",
211
- initialValue: false,
212
- }));
213
- const install = ensureNotCancelled(await p.confirm({
214
- message: `Install dependencies with ${packageManager} now?`,
215
- initialValue: true,
216
- }));
217
- const generateKeys = ensureNotCancelled(await p.confirm({
218
- message: "Generate ANON_KEY and SERVICE_ROLE_KEY now?",
219
- initialValue: true,
220
- }));
221
- let adminEmail;
222
- let adminPassword;
223
- const createAdmin = ensureNotCancelled(await p.confirm({
224
- message: "Create an admin user for /admin?",
225
- initialValue: true,
226
- }));
227
- if (createAdmin) {
228
- adminEmail =
159
+ const doInstall = opts.install !== false && result.install;
160
+ const doKeys = opts.keys !== false && result.generateKeys;
161
+ if (name)
162
+ mkdirSync(dir, { recursive: true });
163
+ const deps = await resolveInitDependencyVersions();
164
+ const runningCli = cliPackageVersion();
165
+ if (deps.cli !== runningCli || deps.types !== runningCli) {
166
+ info(`Using published npm versions: @supatype/cli ^${deps.cli}, @supatype/types ^${deps.types}`);
167
+ }
168
+ const setupSpinner = p.spinner();
169
+ setupSpinner.start("Setting up your project...");
170
+ scaffold(dir, result, deps);
171
+ let installOk = !doInstall;
172
+ if (doInstall) {
173
+ setupSpinner.start(`Installing dependencies with ${result.packageManager}...`);
174
+ installOk = runInstall(dir, result.packageManager);
175
+ setupSpinner.stop(installOk ? "Dependencies installed." : "Dependency install failed.");
176
+ }
177
+ let keysGenerated = !doKeys;
178
+ if (doKeys) {
179
+ setupSpinner.start("Generating API keys...");
180
+ keysGenerated = writeKeys(dir);
181
+ setupSpinner.stop(keysGenerated ? "API keys generated." : "Could not generate API keys.");
182
+ }
183
+ let binariesReady = false;
184
+ if (doInstall && installOk) {
185
+ setupSpinner.start("Preparing component binaries...");
186
+ binariesReady = await ensureInitBinaries(dir);
187
+ setupSpinner.stop(binariesReady ? "Component binaries ready." : "Some binaries are missing.");
188
+ }
189
+ if (doInstall && !installOk) {
190
+ error(`Dependency install failed. Run "${result.packageManager} install" in the project directory, then "supatype update".`);
191
+ process.exit(1);
192
+ }
193
+ if (doKeys && !keysGenerated) {
194
+ error("Could not generate API keys. Check .env has JWT_SECRET, then run `supatype keys`.");
195
+ process.exit(1);
196
+ }
197
+ if (doInstall && installOk && !binariesReady) {
198
+ error("Component binaries could not be downloaded. Run `supatype update` in the project directory.");
199
+ process.exit(1);
200
+ }
201
+ warnDockerUnavailableForProvider(result.provider);
202
+ p.outro(`Supatype project ready${name ? ` in ${name}/` : ""}.`);
203
+ printNextSteps({
204
+ name,
205
+ result,
206
+ installed: doInstall && installOk,
207
+ keysGenerated,
208
+ binariesReady,
209
+ });
210
+ });
211
+ process.exit(0);
212
+ }
213
+ // ─── Wizard ──────────────────────────────────────────────────────────────────
214
+ async function collectWizardAnswers(defaultName, defaultTarget, dir, detected) {
215
+ const projectName = ensureNotCancelled(await p.text({
216
+ message: "Project name",
217
+ defaultValue: defaultName,
218
+ placeholder: defaultName,
219
+ })).trim() || defaultName;
220
+ const admin = await promptAdminUser();
221
+ const packageManager = ensureNotCancelled(await p.select({
222
+ message: "Package manager",
223
+ initialValue: detectInvokingPackageManager(),
224
+ options: [
225
+ { value: "npm", label: "npm" },
226
+ { value: "pnpm", label: "pnpm" },
227
+ { value: "yarn", label: "yarn" },
228
+ { value: "bun", label: "bun" },
229
+ ],
230
+ }));
231
+ const productionTarget = ensureNotCancelled(await p.select({
232
+ message: "Where will this run in production?",
233
+ initialValue: defaultTarget,
234
+ options: [
235
+ { value: "cloud", label: "Supatype Cloud", hint: "managed; deploy via supatype link" },
236
+ { value: "self-host", label: "Self-host", hint: "your own server with TLS" },
237
+ { value: "later", label: "Decide later", hint: "local development only for now" },
238
+ ],
239
+ }));
240
+ let domain;
241
+ let tlsEmail;
242
+ if (productionTarget === "self-host") {
243
+ domain = ensureNotCancelled(await p.text({
244
+ message: "Production domain for ACME TLS (optional, can set later)",
245
+ placeholder: "api.example.com",
246
+ defaultValue: "",
247
+ })).trim();
248
+ if (domain) {
249
+ tlsEmail =
229
250
  ensureNotCancelled(await p.text({
230
- message: "Admin email",
251
+ message: "Email for Let's Encrypt (HTTPS) certificates",
231
252
  placeholder: "you@example.com",
232
253
  defaultValue: "",
233
254
  })).trim() || undefined;
234
- if (adminEmail) {
235
- adminPassword =
236
- ensureNotCancelled(await p.text({
237
- message: "Admin password (min 8 chars)",
238
- defaultValue: "",
239
- })).trim() || undefined;
240
- if (adminPassword && adminPassword.length < 8) {
241
- adminPassword = undefined;
242
- }
243
- }
244
255
  }
245
- p.outro("Setting up your project...");
246
- return {
247
- projectName,
248
- provider,
249
- ...(kongPort !== undefined ? { kongPort } : {}),
250
- productionTarget,
251
- ...(domain !== undefined ? { domain } : {}),
252
- ...(tlsEmail !== undefined ? { tlsEmail } : {}),
253
- schemaPath,
254
- app,
255
- email,
256
- storageLocal,
257
- storageProduction,
258
- helloFunction,
259
- packageManager,
260
- install,
261
- generateKeys,
262
- ...(adminEmail && adminPassword ? { adminEmail, adminPassword } : {}),
263
- };
264
- });
256
+ }
257
+ const provider = ensureNotCancelled(await p.select({
258
+ message: "How should Postgres and the server run for local development?",
259
+ initialValue: "docker",
260
+ options: [
261
+ { value: "docker", label: "Docker", hint: "Docker Compose stack (recommended)" },
262
+ { value: "native", label: "Native", hint: "host Postgres + server binaries, no Docker" },
263
+ ],
264
+ }));
265
+ let kongPort;
266
+ if (provider === "docker") {
267
+ const { promptKongPortChoice } = await import("../dev-ports.js");
268
+ kongPort = await promptKongPortChoice();
269
+ }
270
+ const defaultSchemaPath = existsSync(join(dir, "schema/index.ts"))
271
+ ? "schema/index.ts"
272
+ : "schema/index.ts";
273
+ const schemaPath = ensureNotCancelled(await p.text({
274
+ message: "Where should your schema live?",
275
+ defaultValue: defaultSchemaPath,
276
+ placeholder: "schema/index.ts",
277
+ })).trim() || defaultSchemaPath;
278
+ const app = await promptApp(detected, productionTarget);
279
+ const email = ensureNotCancelled(await p.select({
280
+ message: "Email provider",
281
+ initialValue: "console",
282
+ options: [
283
+ { value: "console", label: "console", hint: "log emails to the terminal (dev)" },
284
+ { value: "smtp", label: "SMTP" },
285
+ { value: "resend", label: "Resend" },
286
+ { value: "ses", label: "Amazon SES" },
287
+ ],
288
+ }));
289
+ const storageLocal = ensureNotCancelled(await p.select({
290
+ message: "Local storage (for development)?",
291
+ initialValue: "local",
292
+ options: STORAGE_PROVIDER_OPTIONS,
293
+ }));
294
+ const storageProduction = ensureNotCancelled(await p.select({
295
+ message: "Production storage?",
296
+ initialValue: "local",
297
+ options: STORAGE_PROVIDER_OPTIONS,
298
+ }));
299
+ const helloFunction = ensureNotCancelled(await p.confirm({
300
+ message: "Create a hello-world edge function?",
301
+ initialValue: false,
302
+ }));
303
+ return {
304
+ projectName,
305
+ provider,
306
+ ...(kongPort !== undefined ? { kongPort } : {}),
307
+ productionTarget,
308
+ ...(domain !== undefined ? { domain } : {}),
309
+ ...(tlsEmail !== undefined ? { tlsEmail } : {}),
310
+ schemaPath,
311
+ app,
312
+ email,
313
+ storageLocal,
314
+ storageProduction,
315
+ helloFunction,
316
+ packageManager,
317
+ install: true,
318
+ generateKeys: true,
319
+ ...admin,
320
+ };
321
+ }
322
+ async function promptAdminUser() {
323
+ const createAdmin = ensureNotCancelled(await p.confirm({
324
+ message: "Create an admin user for /admin?",
325
+ initialValue: true,
326
+ }));
327
+ if (!createAdmin)
328
+ return {};
329
+ p.note("First sign-in at /admin — created automatically on supatype dev or push.");
330
+ const adminEmail = ensureNotCancelled(await p.text({
331
+ message: "Admin email",
332
+ placeholder: "you@example.com",
333
+ validate: (value) => (value.trim() ? undefined : "Email is required"),
334
+ })).trim();
335
+ let adminPassword = "";
336
+ while (true) {
337
+ adminPassword = ensureNotCancelled(await p.password({ message: "Admin password (min 8 characters)" })).trim();
338
+ if (adminPassword.length >= 8)
339
+ break;
340
+ p.log.warn("Password must be at least 8 characters.");
341
+ }
342
+ return { adminEmail, adminPassword };
265
343
  }
266
- async function promptApp() {
344
+ async function promptApp(detected, productionTarget) {
345
+ const initialMode = detected.hasVite && productionTarget !== "later"
346
+ ? "static"
347
+ : detected.hasVite
348
+ ? "proxy"
349
+ : "none";
267
350
  const mode = ensureNotCancelled(await p.select({
268
351
  message: "Host a frontend app at /?",
269
- initialValue: "none",
352
+ initialValue: initialMode,
270
353
  options: [
271
354
  { value: "none", label: "No app", hint: "API only" },
272
355
  { value: "static", label: "Static site", hint: "serve a built directory" },
@@ -276,40 +359,44 @@ async function promptApp() {
276
359
  if (mode === "static") {
277
360
  const staticDir = ensureNotCancelled(await p.text({
278
361
  message: "Directory to serve",
279
- defaultValue: "./public",
280
- placeholder: "./public",
281
- })).trim() || "./public";
282
- const viteDevUrl = await promptViteDevUrl();
362
+ defaultValue: detected.staticDir,
363
+ placeholder: detected.staticDir,
364
+ })).trim() || detected.staticDir;
365
+ const viteDevUrl = await promptViteDevUrl(detected, productionTarget);
283
366
  return { mode, staticDir, ...(viteDevUrl ? { viteDevUrl } : {}) };
284
367
  }
285
368
  if (mode === "proxy") {
286
369
  const upstream = ensureNotCancelled(await p.text({
287
370
  message: "URL of your running dev server",
288
- defaultValue: "http://localhost:3000",
289
- placeholder: "http://localhost:3000",
290
- })).trim() || "http://localhost:3000";
371
+ defaultValue: detected.hasVite ? detected.viteDevUrl : "http://localhost:3000",
372
+ placeholder: detected.hasVite ? detected.viteDevUrl : "http://localhost:3000",
373
+ })).trim() || (detected.hasVite ? detected.viteDevUrl : "http://localhost:3000");
291
374
  const start = ensureNotCancelled(await p.text({
292
375
  message: "package.json script that starts your dev server",
293
- defaultValue: "dev",
294
- placeholder: "dev",
295
- })).trim() || "dev";
296
- const viteDevUrl = await promptViteDevUrl();
376
+ defaultValue: detected.hasVite ? "vite" : "dev",
377
+ placeholder: detected.hasVite ? "vite" : "dev",
378
+ })).trim() || (detected.hasVite ? "vite" : "dev");
379
+ const viteDevUrl = await promptViteDevUrl(detected, productionTarget);
297
380
  return { mode, upstream, start, ...(viteDevUrl ? { viteDevUrl } : {}) };
298
381
  }
299
382
  return { mode: "none" };
300
383
  }
301
- async function promptViteDevUrl() {
384
+ async function promptViteDevUrl(detected, productionTarget) {
385
+ const defaultYes = detected.hasVite || productionTarget !== "later";
302
386
  const useVite = ensureNotCancelled(await p.confirm({
303
- message: "Enable live reload from a separate Vite dev server?",
304
- initialValue: false,
387
+ message: detected.hasVite
388
+ ? "Use Vite for local development?"
389
+ : "Develop locally with Vite (live reload)?",
390
+ initialValue: defaultYes,
305
391
  }));
306
392
  if (!useVite)
307
393
  return undefined;
394
+ p.note(`Press Enter for ${detected.viteDevUrl}, or type a custom URL.`);
308
395
  return (ensureNotCancelled(await p.text({
309
396
  message: "Vite dev server URL",
310
- defaultValue: "http://127.0.0.1:5173",
311
- placeholder: "http://127.0.0.1:5173",
312
- })).trim() || "http://127.0.0.1:5173");
397
+ defaultValue: detected.viteDevUrl,
398
+ placeholder: detected.viteDevUrl,
399
+ })).trim() || detected.viteDevUrl);
313
400
  }
314
401
  // ─── Package manager ───────────────────────────────────────────────────────--
315
402
  function detectInvokingPackageManager() {
@@ -323,14 +410,12 @@ function detectInvokingPackageManager() {
323
410
  return "npm";
324
411
  }
325
412
  function runInstall(dir, pm) {
326
- info(`Installing dependencies with ${pm}...`);
327
413
  const res = spawnSync(pm, ["install"], {
328
414
  cwd: dir,
329
415
  stdio: "inherit",
330
416
  shell: process.platform === "win32",
331
417
  });
332
418
  if (res.status !== 0 || res.error) {
333
- warn(`Dependency install did not complete (run "${pm} install" manually).`);
334
419
  return false;
335
420
  }
336
421
  return true;
@@ -398,7 +483,7 @@ export function scaffold(dir, optsOrName, deps = initDependencyVersionsFallback(
398
483
  write(".env", envTemplate(opts));
399
484
  write("seed.ts", seedTemplate(opts.projectName));
400
485
  write("seeds/.gitkeep", "");
401
- scaffoldAppAssets(opts, write);
486
+ scaffoldAppAssets(dir, opts, write);
402
487
  if (opts.helloFunction)
403
488
  scaffoldHelloFunction(dir, write);
404
489
  const gitignorePath = join(dir, ".gitignore");
@@ -420,37 +505,50 @@ function staticDirRelative(staticDir) {
420
505
  const raw = (staticDir ?? "./public").trim();
421
506
  return raw.replace(/^\.\//, "").replace(/\/+$/, "") || "public";
422
507
  }
423
- function scaffoldAppAssets(opts, write) {
508
+ function scaffoldAppAssets(dir, opts, write) {
509
+ const writeUnlessExists = (rel, content) => {
510
+ const full = join(dir, rel);
511
+ if (existsSync(full)) {
512
+ file("skipped", `${rel} (already exists)`);
513
+ return;
514
+ }
515
+ write(rel, content);
516
+ };
424
517
  const holding = holdingPageTemplate(opts.projectName);
425
518
  const hasVite = Boolean(opts.app.viteDevUrl);
426
519
  if (opts.app.mode === "static") {
427
520
  const staticRel = staticDirRelative(opts.app.staticDir);
428
521
  if (opts.app.viteDevUrl && opts.productionTarget !== "later") {
429
- write("dist/index.html", holding);
430
- scaffoldVite(opts, write, holding);
522
+ writeUnlessExists("dist/index.html", holding);
523
+ scaffoldVite(dir, opts, writeUnlessExists, holding);
431
524
  return;
432
525
  }
433
- write(`${staticRel}/index.html`, holding);
526
+ writeUnlessExists(`${staticRel}/index.html`, holding);
434
527
  if (opts.app.viteDevUrl)
435
- scaffoldVite(opts, write, holding);
528
+ scaffoldVite(dir, opts, writeUnlessExists, holding);
436
529
  return;
437
530
  }
438
531
  if (opts.app.mode === "proxy" && opts.productionTarget !== "later") {
439
- write("dist/index.html", holding);
532
+ writeUnlessExists("dist/index.html", holding);
440
533
  if (hasVite)
441
- scaffoldVite(opts, write, holding);
534
+ scaffoldVite(dir, opts, writeUnlessExists, holding);
442
535
  return;
443
536
  }
444
537
  if (opts.app.mode === "proxy" && hasVite) {
445
- scaffoldVite(opts, write, holding);
538
+ scaffoldVite(dir, opts, writeUnlessExists, holding);
446
539
  return;
447
540
  }
448
- write("public/.gitkeep", "");
541
+ writeUnlessExists("public/.gitkeep", "");
449
542
  }
450
- function scaffoldVite(opts, write, holding) {
543
+ function scaffoldVite(dir, opts, write, holding) {
451
544
  if (!opts.app.viteDevUrl)
452
545
  return;
453
546
  write("index.html", holding);
547
+ const viteConfigRel = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"].find((name) => existsSync(join(dir, name)));
548
+ if (viteConfigRel) {
549
+ file("skipped", `${viteConfigRel} (already exists)`);
550
+ return;
551
+ }
454
552
  write("vite.config.ts", viteConfigTemplate(opts.app.viteDevUrl));
455
553
  }
456
554
  function scaffoldHelloFunction(dir, write) {
@@ -1025,28 +1123,35 @@ function warnDockerUnavailableForProvider(provider) {
1025
1123
  }
1026
1124
  function printNextSteps(args) {
1027
1125
  const { name, result, installed, keysGenerated, binariesReady } = args;
1126
+ const setupComplete = installed && keysGenerated && binariesReady;
1028
1127
  const steps = [];
1029
1128
  if (name)
1030
1129
  steps.push(`cd ${name}`);
1031
- if (!installed)
1032
- steps.push(`${result.packageManager} install`);
1033
- if (!binariesReady)
1034
- steps.push("supatype update # download component binaries");
1035
- if (!keysGenerated)
1036
- steps.push("supatype keys");
1037
1130
  const kongHint = result.provider === "docker" && result.kongPort !== undefined
1038
1131
  ? `supatype dev # Docker Compose stack (Kong :${result.kongPort})`
1039
1132
  : "supatype dev # Docker Compose stack (Kong :18473)";
1040
- steps.push(kongHint);
1041
- steps.push("supatype push # apply schema + generate types");
1042
- if (result.adminEmail) {
1043
- steps.push(" # first admin user is created on dev or push");
1133
+ if (setupComplete) {
1134
+ steps.push(kongHint);
1135
+ steps.push("supatype push # apply schema + generate types");
1136
+ if (result.adminEmail) {
1137
+ steps.push(" # first admin user is created on dev or push");
1138
+ }
1139
+ }
1140
+ else {
1141
+ if (!installed)
1142
+ steps.push(`${result.packageManager} install`);
1143
+ if (!binariesReady)
1144
+ steps.push("supatype update # download component binaries");
1145
+ if (!keysGenerated)
1146
+ steps.push("supatype keys");
1147
+ steps.push(kongHint);
1148
+ steps.push("supatype push # apply schema + generate types");
1044
1149
  }
1045
1150
  if (result.helloFunction) {
1046
1151
  steps.push("supatype functions serve # run edge functions locally");
1047
1152
  }
1048
1153
  info(`Supatype project ready${name ? ` in ${name}/` : ""}.`);
1049
- nextSteps("Next steps:", steps);
1154
+ nextSteps(setupComplete ? "Next steps:" : "Finish setup:", steps);
1050
1155
  if (result.app.mode === "none") {
1051
1156
  nextSteps("Static frontend (self-host):", [
1052
1157
  "supatype app add --static ./public",