create-nextblock 0.2.23 → 0.2.25

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.
@@ -1,23 +1,23 @@
1
- #!/usr/bin/env node
2
-
3
- import * as clack from '@clack/prompts';
4
- import { spawn } from 'node:child_process';
5
- import crypto from 'node:crypto';
6
- import { dirname, resolve, relative, sep, basename } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
- import { createRequire } from 'node:module';
9
- import { execa } from 'execa';
10
- import { program } from 'commander';
11
- import chalk from 'chalk';
12
- import fs from 'fs-extra';
13
- import open from 'open';
14
-
15
- const DEFAULT_PROJECT_NAME = 'nextblock-cms';
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = dirname(__filename);
18
- const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
19
- const REPO_ROOT = resolve(__dirname, '../../..');
20
- const require = createRequire(import.meta.url);
1
+ #!/usr/bin/env node
2
+
3
+ import * as clack from '@clack/prompts';
4
+ import { spawn } from 'node:child_process';
5
+ import crypto from 'node:crypto';
6
+ import { dirname, resolve, relative, sep, basename } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { createRequire } from 'node:module';
9
+ import { execa } from 'execa';
10
+ import { program } from 'commander';
11
+ import chalk from 'chalk';
12
+ import fs from 'fs-extra';
13
+ import open from 'open';
14
+
15
+ const DEFAULT_PROJECT_NAME = 'nextblock-cms';
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+ const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
19
+ const REPO_ROOT = resolve(__dirname, '../../..');
20
+ const require = createRequire(import.meta.url);
21
21
  const EDITOR_UTILS_SOURCE_DIR = resolve(REPO_ROOT, 'libs/editor/src/lib/utils');
22
22
  const IS_WINDOWS = process.platform === 'win32';
23
23
 
@@ -72,23 +72,23 @@ async function handleCommand(projectDirectory, options) {
72
72
  try {
73
73
  let projectName = projectDirectory;
74
74
 
75
- if (!projectName) {
76
- if (yes) {
77
- projectName = DEFAULT_PROJECT_NAME;
78
- console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
79
- } else {
80
- const projectPrompt = await clack.text({
81
- message: 'What is your project named?',
82
- initialValue: DEFAULT_PROJECT_NAME,
83
- validate: (value) => (!value ? 'Project name is required' : undefined),
84
- });
85
- if (clack.isCancel(projectPrompt)) {
86
- handleWizardCancel('Setup cancelled.');
87
- }
88
-
89
- projectName = projectPrompt.trim() || DEFAULT_PROJECT_NAME;
90
- }
91
- }
75
+ if (!projectName) {
76
+ if (yes) {
77
+ projectName = DEFAULT_PROJECT_NAME;
78
+ console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
79
+ } else {
80
+ const projectPrompt = await clack.text({
81
+ message: 'What is your project named?',
82
+ initialValue: DEFAULT_PROJECT_NAME,
83
+ validate: (value) => (!value ? 'Project name is required' : undefined),
84
+ });
85
+ if (clack.isCancel(projectPrompt)) {
86
+ handleWizardCancel('Setup cancelled.');
87
+ }
88
+
89
+ projectName = projectPrompt.trim() || DEFAULT_PROJECT_NAME;
90
+ }
91
+ }
92
92
 
93
93
  const projectDir = resolve(process.cwd(), projectName);
94
94
  await ensureEmptyDirectory(projectDir);
@@ -126,14 +126,14 @@ async function handleCommand(projectDirectory, options) {
126
126
  console.log(chalk.green('Editor utility shims generated.'));
127
127
  }
128
128
 
129
- await ensureGitignore(projectDir);
130
- console.log(chalk.green('.gitignore ready.'));
131
-
132
- await ensureEnvExample(projectDir);
133
- console.log(chalk.green('.env.example ready.'));
134
-
135
- await sanitizeLayout(projectDir);
136
- console.log(chalk.green('Global styles configured.'));
129
+ await ensureGitignore(projectDir);
130
+ console.log(chalk.green('.gitignore ready.'));
131
+
132
+ await ensureEnvExample(projectDir);
133
+ console.log(chalk.green('.env.example ready.'));
134
+
135
+ await sanitizeLayout(projectDir);
136
+ console.log(chalk.green('Global styles configured.'));
137
137
 
138
138
  await sanitizeTailwindConfig(projectDir);
139
139
  console.log(chalk.green('tailwind.config.js sanitized.'));
@@ -144,361 +144,361 @@ async function handleCommand(projectDirectory, options) {
144
144
  await sanitizeNextConfig(projectDir, editorUtilNames);
145
145
  console.log(chalk.green('next.config.js sanitized.'));
146
146
 
147
- await transformPackageJson(projectDir);
148
- console.log(chalk.green('Dependencies updated for public packages.'));
149
-
150
- if (!skipInstall) {
151
- await installDependencies(projectDir);
147
+ await transformPackageJson(projectDir);
148
+ console.log(chalk.green('Dependencies updated for public packages.'));
149
+
150
+ if (!skipInstall) {
151
+ await installDependencies(projectDir);
152
152
  } else {
153
153
  console.log(chalk.yellow('Skipping dependency installation.'));
154
154
  }
155
155
 
156
- // Run setup wizard after dependencies are installed so package assets are available
157
- if (!yes) {
158
- await runSetupWizard(projectDir, projectName);
159
- } else {
160
- console.log(chalk.yellow('Skipping interactive setup wizard because --yes was provided.'));
161
- }
162
-
163
- await initializeGit(projectDir);
164
-
165
- console.log(chalk.green(`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`));
166
- console.log(chalk.cyan('Next step:'));
167
- console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
168
- } catch (error) {
169
- console.error(
170
- chalk.red(error instanceof Error ? error.message : 'An unexpected error occurred'),
171
- );
172
- process.exit(1);
173
- }
174
- }
175
-
176
- async function runSetupWizard(projectDir, projectName) {
177
- const projectPath = resolve(projectDir);
178
- process.chdir(projectPath);
179
-
180
- clack.intro('🚀 Welcome to the NextBlock setup wizard!');
181
-
182
- // Ensure Supabase directory exists so the CLI can place config.toml
183
- const supabaseDir = resolve(projectPath, 'supabase');
184
- await fs.ensureDir(supabaseDir);
185
-
186
- clack.note('Connecting to Supabase...');
187
- clack.note('I will now open your browser to log into Supabase.');
188
- await runSupabaseCli(['login'], { cwd: projectPath });
189
-
190
- await ensureSupabaseAssets(projectPath, { required: true, resetProjectRef: true });
191
- clack.note('Now, please select your NextBlock project when prompted.');
192
- await runSupabaseCli(['link'], { cwd: projectPath });
193
- if (process.stdin.isTTY) {
194
- try {
195
- process.stdin.setRawMode(false);
196
- } catch (_) {
197
- // ignore
198
- }
199
- process.stdin.setEncoding('utf8');
200
- process.stdin.resume();
201
- }
202
-
203
- let projectId = await readSupabaseProjectRef(projectPath);
204
-
205
- if (!projectId) {
206
- clack.note('I could not detect your Supabase project ref automatically.');
207
- const manual = await clack.text({
208
- message:
209
- 'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
210
- validate: (val) => (!val ? 'Project ref is required' : undefined),
211
- });
212
- if (clack.isCancel(manual)) {
213
- handleWizardCancel('Setup cancelled.');
214
- }
215
- projectId = manual.trim();
216
- }
217
-
218
- const siteUrlPrompt = await clack.text({
219
- message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
220
- initialValue: 'http://localhost:3000',
221
- validate: (val) => (!val ? 'URL is required' : undefined),
222
- });
223
- if (clack.isCancel(siteUrlPrompt)) {
224
- handleWizardCancel('Setup cancelled.');
225
- }
226
- const siteUrl = siteUrlPrompt.trim();
227
-
228
- clack.note('Please go to your Supabase project dashboard to get the following secrets.');
229
- const supabaseKeys = await clack.group(
230
- {
231
- dbPassword: () =>
232
- clack.password({
233
- message: 'What is your Database Password? (Settings > Database > Connection Parameters)',
234
- validate: (val) => (!val ? 'Password is required' : undefined),
235
- }),
236
- anonKey: () =>
237
- clack.password({
238
- message: 'What is your Project API Key (anon key)? (Settings > API > Project API Keys)',
239
- validate: (val) => (!val ? 'Anon Key is required' : undefined),
240
- }),
241
- serviceKey: () =>
242
- clack.password({
243
- message: 'What is your Service Role Key (service_role key)? (Settings > API > Project API Keys)',
244
- validate: (val) => (!val ? 'Service Role Key is required' : undefined),
245
- }),
246
- },
247
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
248
- );
249
-
250
- clack.note('Generating local secrets...');
251
- const revalidationToken = crypto.randomBytes(32).toString('hex');
252
- const supabaseUrl = `https://${projectId}.supabase.co`;
253
-
254
- const dbHost = `db.${projectId}.supabase.co`;
255
- const dbUser = 'postgres';
256
- const dbPassword = supabaseKeys.dbPassword;
257
- const dbName = 'postgres';
258
-
259
- const postgresUrl = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/${dbName}`;
260
-
261
- const envPath = resolve(projectPath, '.env');
262
- const appendEnvBlock = async (label, lines) => {
263
- const normalized = lines.join('\n');
264
- const blockContent = normalized.endsWith('\n') ? normalized : `${normalized}\n`;
265
- if (canWriteEnv) {
266
- await fs.appendFile(envPath, blockContent);
267
- } else {
268
- clack.note(`Add the following ${label} values to your existing .env:\n${blockContent}`);
269
- }
270
- };
271
- const envLines = [
272
- `NEXT_PUBLIC_URL=${siteUrl}`,
273
- '# Vercel / Supabase',
274
- `SUPABASE_PROJECT_ID=${projectId}`,
275
- `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
276
- `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
277
- `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
278
- `POSTGRES_URL=${postgresUrl}`,
279
- '',
280
- '# Revalidation',
281
- `REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
282
- '',
283
- ];
284
-
285
- let canWriteEnv = true;
286
- if (await fs.pathExists(envPath)) {
287
- const overwrite = await clack.confirm({
288
- message: '.env already exists. Overwrite with generated values?',
289
- initialValue: false,
290
- });
291
- if (clack.isCancel(overwrite)) {
292
- handleWizardCancel('Setup cancelled.');
293
- }
294
- if (!overwrite) {
295
- canWriteEnv = false;
296
- clack.note('Keeping existing .env. Add/merge the generated values manually.');
297
- }
298
- }
299
-
300
- if (canWriteEnv) {
301
- await fs.writeFile(envPath, envLines.join('\n'));
302
- clack.note('Supabase configuration saved to .env');
303
- }
304
-
305
- clack.note('Setting up your database...');
306
- const dbPushSpinner = clack.spinner();
307
- dbPushSpinner.start('Pushing database schema... (6-9 minutes, please keep this terminal open)');
308
- try {
309
- process.env.POSTGRES_URL = postgresUrl;
310
- const migrationsDir = resolve(projectPath, 'supabase', 'migrations');
311
- const hasMigrations = async () =>
312
- (await fs.pathExists(migrationsDir)) &&
313
- (await fs.readdir(migrationsDir)).some((name) => name.endsWith('.sql'));
314
-
315
- if (!(await hasMigrations())) {
316
- await ensureSupabaseAssets(projectPath);
317
- }
318
-
319
- if (!(await hasMigrations())) {
320
- dbPushSpinner.stop(
321
- `No migrations found in ${migrationsDir}; skipping db push. Ensure @nextblock-cms/db includes supabase/migrations.`,
322
- );
323
- } else {
324
- await execa('npx', ['supabase', 'db', 'push'], { stdio: 'inherit', cwd: projectPath });
325
- dbPushSpinner.stop('Database schema pushed successfully!');
326
- }
327
- } catch (error) {
328
- dbPushSpinner.stop('Database push failed. Please run `npx supabase db push` manually.');
329
- if (error instanceof Error) {
330
- clack.note(error.message);
331
- }
332
- }
333
-
334
- const setupR2 = await clack.confirm({
335
- message: 'Do you want to set up Cloudflare R2 for media storage now? (Optional)',
336
- });
337
- if (clack.isCancel(setupR2)) {
338
- handleWizardCancel('Setup cancelled.');
339
- }
340
-
341
- let r2Values = {
342
- publicBaseUrl: '',
343
- accountId: '',
344
- bucketName: '',
345
- accessKey: '',
346
- secretKey: '',
347
- };
348
-
349
- if (setupR2) {
350
- clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
351
- await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
352
-
353
- const r2Keys = await clack.group(
354
- {
355
- accountId: () =>
356
- clack.text({
357
- message: 'R2: Paste your Cloudflare Account ID:',
358
- validate: (val) => (!val ? 'Account ID is required' : undefined),
359
- }),
360
- bucketName: () =>
361
- clack.text({
362
- message: 'R2: Paste your Bucket Name:',
363
- validate: (val) => (!val ? 'Bucket name is required' : undefined),
364
- }),
365
- accessKey: () =>
366
- clack.password({
367
- message: 'R2: Paste your Access Key ID:',
368
- validate: (val) => (!val ? 'Access Key ID is required' : undefined),
369
- }),
370
- secretKey: () =>
371
- clack.password({
372
- message: 'R2: Paste your Secret Access Key:',
373
- validate: (val) => (!val ? 'Secret Access Key is required' : undefined),
374
- }),
375
- publicBaseUrl: () =>
376
- clack.text({
377
- message: 'R2: Public Base URL (e.g., https://pub-xxx.r2.dev/your-bucket):',
378
- validate: (val) => (!val ? 'Public base URL is required' : undefined),
379
- }),
380
- },
381
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
382
- );
383
-
384
- r2Values = {
385
- publicBaseUrl: r2Keys.publicBaseUrl,
386
- accountId: r2Keys.accountId,
387
- bucketName: r2Keys.bucketName,
388
- accessKey: r2Keys.accessKey,
389
- secretKey: r2Keys.secretKey,
390
- };
391
- }
392
-
393
- await appendEnvBlock('Cloudflare R2', [
394
- '',
395
- '# Cloudflare',
396
- `NEXT_PUBLIC_R2_BASE_URL=${r2Values.publicBaseUrl}`,
397
- `R2_ACCOUNT_ID=${r2Values.accountId}`,
398
- `R2_BUCKET_NAME=${r2Values.bucketName}`,
399
- `R2_ACCESS_KEY_ID=${r2Values.accessKey}`,
400
- `R2_SECRET_ACCESS_KEY=${r2Values.secretKey}`,
401
- '',
402
- ]);
403
- if (setupR2) {
404
- clack.note('Cloudflare R2 configuration saved!');
405
- } else if (canWriteEnv) {
406
- clack.note('Cloudflare R2 placeholders added to .env. Configure them later when ready.');
407
- }
408
-
409
- const setupSMTP = await clack.confirm({
410
- message: 'Do you want to set up an SMTP server for emails now? (Optional)',
411
- });
412
- if (clack.isCancel(setupSMTP)) {
413
- handleWizardCancel('Setup cancelled.');
414
- }
415
-
416
- let smtpValues = {
417
- host: '',
418
- port: '',
419
- user: '',
420
- pass: '',
421
- fromEmail: '',
422
- fromName: '',
423
- };
424
-
425
- if (setupSMTP) {
426
- const smtpKeys = await clack.group(
427
- {
428
- host: () =>
429
- clack.text({
430
- message: 'SMTP: Host (e.g., smtp.resend.com):',
431
- validate: (val) => (!val ? 'SMTP host is required' : undefined),
432
- }),
433
- port: () =>
434
- clack.text({
435
- message: 'SMTP: Port (e.g., 465):',
436
- validate: (val) => (!val ? 'SMTP port is required' : undefined),
437
- }),
438
- user: () =>
439
- clack.text({
440
- message: 'SMTP: User (e.g., apikey):',
441
- validate: (val) => (!val ? 'SMTP user is required' : undefined),
442
- }),
443
- pass: () =>
444
- clack.password({
445
- message: 'SMTP: Password:',
446
- validate: (val) => (!val ? 'SMTP password is required' : undefined),
447
- }),
448
- fromEmail: () =>
449
- clack.text({
450
- message: 'SMTP: From Email (e.g., onboarding@my.site):',
451
- validate: (val) => (!val ? 'From email is required' : undefined),
452
- }),
453
- fromName: () =>
454
- clack.text({
455
- message: 'SMTP: From Name (e.g., NextBlock):',
456
- validate: (val) => (!val ? 'From name is required' : undefined),
457
- }),
458
- },
459
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
460
- );
461
-
462
- smtpValues = {
463
- host: smtpKeys.host,
464
- port: smtpKeys.port,
465
- user: smtpKeys.user,
466
- pass: smtpKeys.pass,
467
- fromEmail: smtpKeys.fromEmail,
468
- fromName: smtpKeys.fromName,
469
- };
470
- }
471
-
472
- await appendEnvBlock('SMTP', [
473
- '',
474
- '# Email SMTP Configuration',
475
- `SMTP_HOST=${smtpValues.host}`,
476
- `SMTP_PORT=${smtpValues.port}`,
477
- `SMTP_USER=${smtpValues.user}`,
478
- `SMTP_PASS=${smtpValues.pass}`,
479
- `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
480
- `SMTP_FROM_NAME=${smtpValues.fromName}`,
481
- '',
482
- ]);
483
- if (setupSMTP) {
484
- clack.note('SMTP configuration saved!');
485
- } else if (canWriteEnv) {
486
- clack.note('SMTP placeholders added to .env. Configure them later when ready.');
487
- }
488
-
489
- clack.outro(`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`);
490
- }
491
-
492
- function handleWizardCancel(message) {
493
- clack.cancel(message ?? 'Setup cancelled.');
494
- process.exit(1);
495
- }
496
-
497
- async function ensureEmptyDirectory(projectDir) {
498
- const exists = await fs.pathExists(projectDir);
499
- if (!exists) {
500
- return;
501
- }
156
+ // Run setup wizard after dependencies are installed so package assets are available
157
+ if (!yes) {
158
+ await runSetupWizard(projectDir, projectName);
159
+ } else {
160
+ console.log(chalk.yellow('Skipping interactive setup wizard because --yes was provided.'));
161
+ }
162
+
163
+ await initializeGit(projectDir);
164
+
165
+ console.log(chalk.green(`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`));
166
+ console.log(chalk.cyan('Next step:'));
167
+ console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
168
+ } catch (error) {
169
+ console.error(
170
+ chalk.red(error instanceof Error ? error.message : 'An unexpected error occurred'),
171
+ );
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ async function runSetupWizard(projectDir, projectName) {
177
+ const projectPath = resolve(projectDir);
178
+ process.chdir(projectPath);
179
+
180
+ clack.intro('🚀 Welcome to the NextBlock setup wizard!');
181
+
182
+ const supabaseDir = resolve(projectPath, 'supabase');
183
+ await fs.ensureDir(supabaseDir);
184
+ await resetSupabaseProjectRef(projectPath);
185
+
186
+ clack.note('Connecting to Supabase...');
187
+ clack.note('I will now open your browser to log into Supabase.');
188
+ await runSupabaseCli(['login'], { cwd: projectPath });
189
+
190
+ clack.note('Now, please select your NextBlock project when prompted.');
191
+ await runSupabaseCli(['link'], { cwd: projectPath });
192
+ if (process.stdin.isTTY) {
193
+ try {
194
+ process.stdin.setRawMode(false);
195
+ } catch (_) {
196
+ // ignore
197
+ }
198
+ process.stdin.setEncoding('utf8');
199
+ process.stdin.resume();
200
+ }
201
+
202
+ let projectId = await readSupabaseProjectRef(projectPath);
203
+
204
+ if (!projectId) {
205
+ clack.note('I could not detect your Supabase project ref automatically.');
206
+ const manual = await clack.text({
207
+ message:
208
+ 'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
209
+ validate: (val) => (!val ? 'Project ref is required' : undefined),
210
+ });
211
+ if (clack.isCancel(manual)) {
212
+ handleWizardCancel('Setup cancelled.');
213
+ }
214
+ projectId = manual.trim();
215
+ }
216
+ await ensureSupabaseAssets(projectPath, { required: true });
217
+
218
+ const siteUrlPrompt = await clack.text({
219
+ message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
220
+ initialValue: 'http://localhost:3000',
221
+ validate: (val) => (!val ? 'URL is required' : undefined),
222
+ });
223
+ if (clack.isCancel(siteUrlPrompt)) {
224
+ handleWizardCancel('Setup cancelled.');
225
+ }
226
+ const siteUrl = siteUrlPrompt.trim();
227
+
228
+ clack.note('Please go to your Supabase project dashboard to get the following secrets.');
229
+ const supabaseKeys = await clack.group(
230
+ {
231
+ dbPassword: () =>
232
+ clack.password({
233
+ message: 'What is your Database Password? (Settings > Database > Connection Parameters)',
234
+ validate: (val) => (!val ? 'Password is required' : undefined),
235
+ }),
236
+ anonKey: () =>
237
+ clack.password({
238
+ message: 'What is your Project API Key (anon key)? (Settings > API > Project API Keys)',
239
+ validate: (val) => (!val ? 'Anon Key is required' : undefined),
240
+ }),
241
+ serviceKey: () =>
242
+ clack.password({
243
+ message: 'What is your Service Role Key (service_role key)? (Settings > API > Project API Keys)',
244
+ validate: (val) => (!val ? 'Service Role Key is required' : undefined),
245
+ }),
246
+ },
247
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
248
+ );
249
+
250
+ clack.note('Generating local secrets...');
251
+ const revalidationToken = crypto.randomBytes(32).toString('hex');
252
+ const supabaseUrl = `https://${projectId}.supabase.co`;
253
+
254
+ const dbHost = `db.${projectId}.supabase.co`;
255
+ const dbUser = 'postgres';
256
+ const dbPassword = supabaseKeys.dbPassword;
257
+ const dbName = 'postgres';
258
+
259
+ const postgresUrl = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/${dbName}`;
260
+
261
+ const envPath = resolve(projectPath, '.env');
262
+ const appendEnvBlock = async (label, lines) => {
263
+ const normalized = lines.join('\n');
264
+ const blockContent = normalized.endsWith('\n') ? normalized : `${normalized}\n`;
265
+ if (canWriteEnv) {
266
+ await fs.appendFile(envPath, blockContent);
267
+ } else {
268
+ clack.note(`Add the following ${label} values to your existing .env:\n${blockContent}`);
269
+ }
270
+ };
271
+ const envLines = [
272
+ `NEXT_PUBLIC_URL=${siteUrl}`,
273
+ '# Vercel / Supabase',
274
+ `SUPABASE_PROJECT_ID=${projectId}`,
275
+ `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
276
+ `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
277
+ `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
278
+ `POSTGRES_URL=${postgresUrl}`,
279
+ '',
280
+ '# Revalidation',
281
+ `REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
282
+ '',
283
+ ];
284
+
285
+ let canWriteEnv = true;
286
+ if (await fs.pathExists(envPath)) {
287
+ const overwrite = await clack.confirm({
288
+ message: '.env already exists. Overwrite with generated values?',
289
+ initialValue: false,
290
+ });
291
+ if (clack.isCancel(overwrite)) {
292
+ handleWizardCancel('Setup cancelled.');
293
+ }
294
+ if (!overwrite) {
295
+ canWriteEnv = false;
296
+ clack.note('Keeping existing .env. Add/merge the generated values manually.');
297
+ }
298
+ }
299
+
300
+ if (canWriteEnv) {
301
+ await fs.writeFile(envPath, envLines.join('\n'));
302
+ clack.note('Supabase configuration saved to .env');
303
+ }
304
+
305
+ clack.note('Setting up your database...');
306
+ const dbPushSpinner = clack.spinner();
307
+ dbPushSpinner.start('Pushing database schema... (~10-15 minutes, please keep this terminal open)');
308
+ try {
309
+ process.env.POSTGRES_URL = postgresUrl;
310
+ const migrationsDir = resolve(projectPath, 'supabase', 'migrations');
311
+ const hasMigrations = async () =>
312
+ (await fs.pathExists(migrationsDir)) &&
313
+ (await fs.readdir(migrationsDir)).some((name) => name.endsWith('.sql'));
314
+
315
+ if (!(await hasMigrations())) {
316
+ await ensureSupabaseAssets(projectPath);
317
+ }
318
+
319
+ if (!(await hasMigrations())) {
320
+ dbPushSpinner.stop(
321
+ `No migrations found in ${migrationsDir}; skipping db push. Ensure @nextblock-cms/db includes supabase/migrations.`,
322
+ );
323
+ } else {
324
+ await execa('npx', ['supabase', 'db', 'push'], { stdio: 'inherit', cwd: projectPath });
325
+ dbPushSpinner.stop('Database schema pushed successfully!');
326
+ }
327
+ } catch (error) {
328
+ dbPushSpinner.stop('Database push failed. Please run `npx supabase db push` manually.');
329
+ if (error instanceof Error) {
330
+ clack.note(error.message);
331
+ }
332
+ }
333
+
334
+ const setupR2 = await clack.confirm({
335
+ message: 'Do you want to set up Cloudflare R2 for media storage now? (Optional)',
336
+ });
337
+ if (clack.isCancel(setupR2)) {
338
+ handleWizardCancel('Setup cancelled.');
339
+ }
340
+
341
+ let r2Values = {
342
+ publicBaseUrl: '',
343
+ accountId: '',
344
+ bucketName: '',
345
+ accessKey: '',
346
+ secretKey: '',
347
+ };
348
+
349
+ if (setupR2) {
350
+ clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
351
+ await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
352
+
353
+ const r2Keys = await clack.group(
354
+ {
355
+ accountId: () =>
356
+ clack.text({
357
+ message: 'R2: Paste your Cloudflare Account ID:',
358
+ validate: (val) => (!val ? 'Account ID is required' : undefined),
359
+ }),
360
+ bucketName: () =>
361
+ clack.text({
362
+ message: 'R2: Paste your Bucket Name:',
363
+ validate: (val) => (!val ? 'Bucket name is required' : undefined),
364
+ }),
365
+ accessKey: () =>
366
+ clack.password({
367
+ message: 'R2: Paste your Access Key ID:',
368
+ validate: (val) => (!val ? 'Access Key ID is required' : undefined),
369
+ }),
370
+ secretKey: () =>
371
+ clack.password({
372
+ message: 'R2: Paste your Secret Access Key:',
373
+ validate: (val) => (!val ? 'Secret Access Key is required' : undefined),
374
+ }),
375
+ publicBaseUrl: () =>
376
+ clack.text({
377
+ message: 'R2: Public Base URL (e.g., https://pub-xxx.r2.dev/your-bucket):',
378
+ validate: (val) => (!val ? 'Public base URL is required' : undefined),
379
+ }),
380
+ },
381
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
382
+ );
383
+
384
+ r2Values = {
385
+ publicBaseUrl: r2Keys.publicBaseUrl,
386
+ accountId: r2Keys.accountId,
387
+ bucketName: r2Keys.bucketName,
388
+ accessKey: r2Keys.accessKey,
389
+ secretKey: r2Keys.secretKey,
390
+ };
391
+ }
392
+
393
+ await appendEnvBlock('Cloudflare R2', [
394
+ '',
395
+ '# Cloudflare',
396
+ `NEXT_PUBLIC_R2_BASE_URL=${r2Values.publicBaseUrl}`,
397
+ `R2_ACCOUNT_ID=${r2Values.accountId}`,
398
+ `R2_BUCKET_NAME=${r2Values.bucketName}`,
399
+ `R2_ACCESS_KEY_ID=${r2Values.accessKey}`,
400
+ `R2_SECRET_ACCESS_KEY=${r2Values.secretKey}`,
401
+ '',
402
+ ]);
403
+ if (setupR2) {
404
+ clack.note('Cloudflare R2 configuration saved!');
405
+ } else if (canWriteEnv) {
406
+ clack.note('Cloudflare R2 placeholders added to .env. Configure them later when ready.');
407
+ }
408
+
409
+ const setupSMTP = await clack.confirm({
410
+ message: 'Do you want to set up an SMTP server for emails now? (Optional)',
411
+ });
412
+ if (clack.isCancel(setupSMTP)) {
413
+ handleWizardCancel('Setup cancelled.');
414
+ }
415
+
416
+ let smtpValues = {
417
+ host: '',
418
+ port: '',
419
+ user: '',
420
+ pass: '',
421
+ fromEmail: '',
422
+ fromName: '',
423
+ };
424
+
425
+ if (setupSMTP) {
426
+ const smtpKeys = await clack.group(
427
+ {
428
+ host: () =>
429
+ clack.text({
430
+ message: 'SMTP: Host (e.g., smtp.resend.com):',
431
+ validate: (val) => (!val ? 'SMTP host is required' : undefined),
432
+ }),
433
+ port: () =>
434
+ clack.text({
435
+ message: 'SMTP: Port (e.g., 465):',
436
+ validate: (val) => (!val ? 'SMTP port is required' : undefined),
437
+ }),
438
+ user: () =>
439
+ clack.text({
440
+ message: 'SMTP: User (e.g., apikey):',
441
+ validate: (val) => (!val ? 'SMTP user is required' : undefined),
442
+ }),
443
+ pass: () =>
444
+ clack.password({
445
+ message: 'SMTP: Password:',
446
+ validate: (val) => (!val ? 'SMTP password is required' : undefined),
447
+ }),
448
+ fromEmail: () =>
449
+ clack.text({
450
+ message: 'SMTP: From Email (e.g., onboarding@my.site):',
451
+ validate: (val) => (!val ? 'From email is required' : undefined),
452
+ }),
453
+ fromName: () =>
454
+ clack.text({
455
+ message: 'SMTP: From Name (e.g., NextBlock):',
456
+ validate: (val) => (!val ? 'From name is required' : undefined),
457
+ }),
458
+ },
459
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
460
+ );
461
+
462
+ smtpValues = {
463
+ host: smtpKeys.host,
464
+ port: smtpKeys.port,
465
+ user: smtpKeys.user,
466
+ pass: smtpKeys.pass,
467
+ fromEmail: smtpKeys.fromEmail,
468
+ fromName: smtpKeys.fromName,
469
+ };
470
+ }
471
+
472
+ await appendEnvBlock('SMTP', [
473
+ '',
474
+ '# Email SMTP Configuration',
475
+ `SMTP_HOST=${smtpValues.host}`,
476
+ `SMTP_PORT=${smtpValues.port}`,
477
+ `SMTP_USER=${smtpValues.user}`,
478
+ `SMTP_PASS=${smtpValues.pass}`,
479
+ `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
480
+ `SMTP_FROM_NAME=${smtpValues.fromName}`,
481
+ '',
482
+ ]);
483
+ if (setupSMTP) {
484
+ clack.note('SMTP configuration saved!');
485
+ } else if (canWriteEnv) {
486
+ clack.note('SMTP placeholders added to .env. Configure them later when ready.');
487
+ }
488
+
489
+ clack.outro(`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`);
490
+ }
491
+
492
+ function handleWizardCancel(message) {
493
+ clack.cancel(message ?? 'Setup cancelled.');
494
+ process.exit(1);
495
+ }
496
+
497
+ async function ensureEmptyDirectory(projectDir) {
498
+ const exists = await fs.pathExists(projectDir);
499
+ if (!exists) {
500
+ return;
501
+ }
502
502
 
503
503
  const contents = await fs.readdir(projectDir);
504
504
  if (contents.length > 0) {
@@ -664,11 +664,11 @@ async function ensureGitignore(projectDir) {
664
664
  }
665
665
  }
666
666
 
667
- async function ensureEnvExample(projectDir) {
668
- const destination = resolve(projectDir, '.env.example');
669
- if (await fs.pathExists(destination)) {
670
- return;
671
- }
667
+ async function ensureEnvExample(projectDir) {
668
+ const destination = resolve(projectDir, '.env.example');
669
+ if (await fs.pathExists(destination)) {
670
+ return;
671
+ }
672
672
 
673
673
  const templatePaths = [
674
674
  resolve(TEMPLATE_DIR, '.env.example'),
@@ -681,178 +681,172 @@ async function ensureEnvExample(projectDir) {
681
681
  await fs.copy(candidate, destination);
682
682
  return;
683
683
  }
684
- }
685
-
686
- const placeholder = `# Environment variables for NextBlock CMS
687
- NEXT_PUBLIC_URL=
688
- # Vercel / Supabase
689
- SUPABASE_PROJECT_ID=
690
- POSTGRES_URL=
691
- NEXT_PUBLIC_SUPABASE_URL=
692
- NEXT_PUBLIC_SUPABASE_ANON_KEY=
693
- SUPABASE_SERVICE_ROLE_KEY=
694
-
695
- # Cloudflare
696
- NEXT_PUBLIC_R2_BASE_URL=
697
- R2_ACCESS_KEY_ID=
698
- R2_SECRET_ACCESS_KEY=
699
- R2_BUCKET_NAME=
700
- R2_ACCOUNT_ID=
701
-
702
- REVALIDATE_SECRET_TOKEN=
703
-
704
- # Email SMTP Configuration
705
- SMTP_HOST=
706
- SMTP_PORT=
707
- SMTP_USER=
708
- SMTP_PASS=
709
- SMTP_FROM_EMAIL=
710
- SMTP_FROM_NAME=
711
- `;
712
-
713
- await fs.writeFile(destination, placeholder);
714
- }
715
-
716
- async function ensureSupabaseAssets(projectDir, options = {}) {
717
- const { required = false, resetProjectRef = false } = options;
718
- const destSupabaseDir = resolve(projectDir, 'supabase');
719
- await fs.ensureDir(destSupabaseDir);
720
-
721
- const { dir: packageSupabaseDir, triedPaths } = await resolvePackageSupabaseDir(projectDir);
722
- if (!packageSupabaseDir) {
723
- const message =
724
- 'Unable to locate supabase assets in @nextblock-cms/db. Please ensure dependencies are installed.' +
725
- (triedPaths.length > 0 ? `\nChecked:\n - ${triedPaths.join('\n - ')}` : '');
726
- if (required) {
727
- throw new Error(message);
728
- } else {
729
- clack.note(message);
730
- return { migrationsCopied: false, configCopied: false, projectId: null };
731
- }
732
- }
733
-
734
- let migrationsCopied = false;
735
- let configCopied = false;
736
- let detectedProjectId = null;
737
-
738
- const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
739
- const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
740
- if (await fs.pathExists(sourceConfigPath)) {
741
- await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
742
- configCopied = true;
743
-
744
- const sourceConfig = await fs.readFile(sourceConfigPath, 'utf8');
745
- const configMatch = sourceConfig.match(/project_id\s*=\s*"([^"]+)"/);
746
- if (configMatch?.[1] && !configMatch[1].includes('env(')) {
747
- detectedProjectId = configMatch[1];
748
- }
749
- }
750
-
751
- const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
752
- const destMigrations = resolve(destSupabaseDir, 'migrations');
753
- if (await fs.pathExists(sourceMigrations)) {
754
- await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
755
- migrationsCopied = true;
756
- }
757
-
758
- if (resetProjectRef) {
759
- const tempDir = resolve(destSupabaseDir, '.temp');
760
- const projectRefPath = resolve(tempDir, 'project-ref');
761
- if (await fs.pathExists(projectRefPath)) {
762
- await fs.writeFile(projectRefPath, '');
763
- }
764
- }
765
-
766
- if (required) {
767
- if (!configCopied) {
768
- throw new Error(
769
- `Missing supabase/config.toml in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
770
- );
771
- }
772
- if (!migrationsCopied) {
773
- throw new Error(
774
- `Missing supabase/migrations in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
775
- );
776
- }
777
- }
778
-
779
- return { migrationsCopied, configCopied, projectId: detectedProjectId };
780
- }
781
-
782
- async function resolvePackageSupabaseDir(projectDir) {
783
- const triedPaths = [];
784
- const candidateBases = new Set();
785
-
786
- const installDir = resolve(projectDir, 'node_modules', '@nextblock-cms', 'db');
787
- candidateBases.add(installDir);
788
-
789
- const tryResolveFrom = (fromPath) => {
790
- try {
791
- const resolver = createRequire(fromPath);
792
- const pkgPath = resolver.resolve('@nextblock-cms/db/package.json');
793
- return dirname(pkgPath);
794
- } catch {
795
- return null;
796
- }
797
- };
798
-
799
- const projectPkg = resolve(projectDir, 'package.json');
800
- const resolvedProject = tryResolveFrom(projectPkg);
801
- if (resolvedProject) {
802
- candidateBases.add(resolvedProject);
803
- const parent = dirname(resolvedProject);
804
- candidateBases.add(parent);
805
- }
806
-
807
- const localResolve = tryResolveFrom(__filename);
808
- if (localResolve) {
809
- candidateBases.add(localResolve);
810
- candidateBases.add(dirname(localResolve));
811
- }
812
-
813
- candidateBases.add(REPO_ROOT);
814
- candidateBases.add(resolve(REPO_ROOT, 'libs', 'db'));
815
- candidateBases.add(resolve(REPO_ROOT, 'dist', 'libs', 'db'));
816
-
817
- const candidateSegments = [
818
- 'supabase',
819
- 'src/supabase',
820
- 'dist/supabase',
821
- 'dist/libs/db/supabase',
822
- 'dist/lib/supabase',
823
- 'lib/supabase',
824
- ];
825
-
826
- for (const base of Array.from(candidateBases).filter(Boolean)) {
827
- for (const segment of candidateSegments) {
828
- const candidate = resolve(base, segment);
829
- triedPaths.push(candidate);
830
- if (await fs.pathExists(candidate)) {
831
- return { dir: candidate, triedPaths };
832
- }
833
- }
834
- }
835
-
836
- return { dir: null, triedPaths };
837
- }
838
-
839
- async function readSupabaseProjectRef(projectDir) {
840
- const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
841
- if (await fs.pathExists(projectRefPath)) {
842
- const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
843
- if (/^[a-z0-9]{20,}$/i.test(value)) {
844
- return value;
845
- }
846
- }
847
-
848
- return null;
849
- }
850
-
851
- async function ensureClientComponents(projectDir) {
852
- const relativePaths = [
853
- 'components/env-var-warning.tsx',
854
- 'app/providers.tsx',
855
- 'app/ToasterProvider.tsx',
684
+ }
685
+
686
+ const placeholder = `# Environment variables for NextBlock CMS
687
+ NEXT_PUBLIC_URL=
688
+ # Vercel / Supabase
689
+ SUPABASE_PROJECT_ID=
690
+ POSTGRES_URL=
691
+ NEXT_PUBLIC_SUPABASE_URL=
692
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
693
+ SUPABASE_SERVICE_ROLE_KEY=
694
+
695
+ # Cloudflare
696
+ NEXT_PUBLIC_R2_BASE_URL=
697
+ R2_ACCESS_KEY_ID=
698
+ R2_SECRET_ACCESS_KEY=
699
+ R2_BUCKET_NAME=
700
+ R2_ACCOUNT_ID=
701
+
702
+ REVALIDATE_SECRET_TOKEN=
703
+
704
+ # Email SMTP Configuration
705
+ SMTP_HOST=
706
+ SMTP_PORT=
707
+ SMTP_USER=
708
+ SMTP_PASS=
709
+ SMTP_FROM_EMAIL=
710
+ SMTP_FROM_NAME=
711
+ `;
712
+
713
+ await fs.writeFile(destination, placeholder);
714
+ }
715
+
716
+ async function ensureSupabaseAssets(projectDir, options = {}) {
717
+ const { required = false } = options;
718
+ const destSupabaseDir = resolve(projectDir, 'supabase');
719
+ await fs.ensureDir(destSupabaseDir);
720
+
721
+ const { dir: packageSupabaseDir, triedPaths } = await resolvePackageSupabaseDir(projectDir);
722
+ if (!packageSupabaseDir) {
723
+ const message =
724
+ 'Unable to locate supabase assets in @nextblock-cms/db. Please ensure dependencies are installed.' +
725
+ (triedPaths.length > 0 ? `\nChecked:\n - ${triedPaths.join('\n - ')}` : '');
726
+ if (required) {
727
+ throw new Error(message);
728
+ } else {
729
+ clack.note(message);
730
+ return { migrationsCopied: false, configCopied: false, projectId: null };
731
+ }
732
+ }
733
+
734
+ let migrationsCopied = false;
735
+ let configCopied = false;
736
+
737
+ const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
738
+ const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
739
+ if (await fs.pathExists(sourceConfigPath)) {
740
+ await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
741
+ configCopied = true;
742
+ }
743
+
744
+ const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
745
+ const destMigrations = resolve(destSupabaseDir, 'migrations');
746
+ if (await fs.pathExists(sourceMigrations)) {
747
+ await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
748
+ migrationsCopied = true;
749
+ }
750
+
751
+ if (required) {
752
+ if (!configCopied) {
753
+ throw new Error(
754
+ `Missing supabase/config.toml in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
755
+ );
756
+ }
757
+ if (!migrationsCopied) {
758
+ throw new Error(
759
+ `Missing supabase/migrations in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
760
+ );
761
+ }
762
+ }
763
+
764
+ return { migrationsCopied, configCopied };
765
+ }
766
+
767
+ async function resolvePackageSupabaseDir(projectDir) {
768
+ const triedPaths = [];
769
+ const candidateBases = new Set();
770
+
771
+ const installDir = resolve(projectDir, 'node_modules', '@nextblock-cms', 'db');
772
+ candidateBases.add(installDir);
773
+
774
+ const tryResolveFrom = (fromPath) => {
775
+ try {
776
+ const resolver = createRequire(fromPath);
777
+ const pkgPath = resolver.resolve('@nextblock-cms/db/package.json');
778
+ return dirname(pkgPath);
779
+ } catch {
780
+ return null;
781
+ }
782
+ };
783
+
784
+ const projectPkg = resolve(projectDir, 'package.json');
785
+ const resolvedProject = tryResolveFrom(projectPkg);
786
+ if (resolvedProject) {
787
+ candidateBases.add(resolvedProject);
788
+ const parent = dirname(resolvedProject);
789
+ candidateBases.add(parent);
790
+ }
791
+
792
+ const localResolve = tryResolveFrom(__filename);
793
+ if (localResolve) {
794
+ candidateBases.add(localResolve);
795
+ candidateBases.add(dirname(localResolve));
796
+ }
797
+
798
+ candidateBases.add(REPO_ROOT);
799
+ candidateBases.add(resolve(REPO_ROOT, 'libs', 'db'));
800
+ candidateBases.add(resolve(REPO_ROOT, 'dist', 'libs', 'db'));
801
+
802
+ const candidateSegments = [
803
+ 'supabase',
804
+ 'src/supabase',
805
+ 'dist/supabase',
806
+ 'dist/libs/db/supabase',
807
+ 'dist/lib/supabase',
808
+ 'lib/supabase',
809
+ ];
810
+
811
+ for (const base of Array.from(candidateBases).filter(Boolean)) {
812
+ for (const segment of candidateSegments) {
813
+ const candidate = resolve(base, segment);
814
+ triedPaths.push(candidate);
815
+ if (await fs.pathExists(candidate)) {
816
+ return { dir: candidate, triedPaths };
817
+ }
818
+ }
819
+ }
820
+
821
+ return { dir: null, triedPaths };
822
+ }
823
+
824
+ async function readSupabaseProjectRef(projectDir) {
825
+ const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
826
+ if (await fs.pathExists(projectRefPath)) {
827
+ const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
828
+ if (/^[a-z0-9]{20,}$/i.test(value)) {
829
+ return value;
830
+ }
831
+ }
832
+
833
+ return null;
834
+ }
835
+
836
+ async function resetSupabaseProjectRef(projectDir) {
837
+ const tempDir = resolve(projectDir, 'supabase', '.temp');
838
+ await fs.ensureDir(tempDir);
839
+ const projectRefPath = resolve(tempDir, 'project-ref');
840
+ if (await fs.pathExists(projectRefPath)) {
841
+ await fs.remove(projectRefPath);
842
+ }
843
+ }
844
+
845
+ async function ensureClientComponents(projectDir) {
846
+ const relativePaths = [
847
+ 'components/env-var-warning.tsx',
848
+ 'app/providers.tsx',
849
+ 'app/ToasterProvider.tsx',
856
850
  'context/AuthContext.tsx',
857
851
  'context/CurrentContentContext.tsx',
858
852
  'context/LanguageContext.tsx',
@@ -1315,11 +1309,11 @@ async function initializeGit(projectDir) {
1315
1309
  }
1316
1310
  }
1317
1311
 
1318
- function runCommand(command, args, options = {}) {
1319
- return new Promise((resolve, reject) => {
1320
- const child = spawn(command, args, {
1321
- stdio: 'inherit',
1322
- shell: IS_WINDOWS,
1312
+ function runCommand(command, args, options = {}) {
1313
+ return new Promise((resolve, reject) => {
1314
+ const child = spawn(command, args, {
1315
+ stdio: 'inherit',
1316
+ shell: IS_WINDOWS,
1323
1317
  ...options,
1324
1318
  });
1325
1319
 
@@ -1334,33 +1328,33 @@ function runCommand(command, args, options = {}) {
1334
1328
  reject(new Error(`${command} exited with code ${code}`));
1335
1329
  }
1336
1330
  });
1337
- });
1338
- }
1339
-
1340
- async function runSupabaseCli(args, options = {}) {
1341
- const { cwd } = options;
1342
- return new Promise((resolve, reject) => {
1343
- const child = spawn('npx', ['supabase', ...args], {
1344
- cwd,
1345
- shell: IS_WINDOWS,
1346
- stdio: 'inherit',
1347
- });
1348
-
1349
- child.on('error', (error) => {
1350
- reject(error);
1351
- });
1352
-
1353
- child.on('close', (code) => {
1354
- if (code === 0) {
1355
- resolve();
1356
- } else {
1357
- reject(new Error(`supabase ${args.join(' ')} exited with code ${code}`));
1358
- }
1359
- });
1360
- });
1361
- }
1362
-
1363
- function buildNextConfigContent(editorUtilNames) {
1331
+ });
1332
+ }
1333
+
1334
+ async function runSupabaseCli(args, options = {}) {
1335
+ const { cwd } = options;
1336
+ return new Promise((resolve, reject) => {
1337
+ const child = spawn('npx', ['supabase', ...args], {
1338
+ cwd,
1339
+ shell: IS_WINDOWS,
1340
+ stdio: 'inherit',
1341
+ });
1342
+
1343
+ child.on('error', (error) => {
1344
+ reject(error);
1345
+ });
1346
+
1347
+ child.on('close', (code) => {
1348
+ if (code === 0) {
1349
+ resolve();
1350
+ } else {
1351
+ reject(new Error(`supabase ${args.join(' ')} exited with code ${code}`));
1352
+ }
1353
+ });
1354
+ });
1355
+ }
1356
+
1357
+ function buildNextConfigContent(editorUtilNames) {
1364
1358
  const aliasLines = [];
1365
1359
 
1366
1360
  for (const moduleName of UI_PROXY_MODULES) {