create-nextblock 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +562 -568
- package/package.json +1 -1
package/bin/create-nextblock.js
CHANGED
|
@@ -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
|
|
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
|
+
|
|
183
182
|
const supabaseDir = resolve(projectPath, 'supabase');
|
|
184
183
|
await fs.ensureDir(supabaseDir);
|
|
184
|
+
await resetSupabaseProjectRef(projectPath);
|
|
185
185
|
|
|
186
186
|
clack.note('Connecting to Supabase...');
|
|
187
187
|
clack.note('I will now open your browser to log into Supabase.');
|
|
188
188
|
await runSupabaseCli(['login'], { cwd: projectPath });
|
|
189
189
|
|
|
190
|
-
await ensureSupabaseAssets(projectPath, { required: true, resetProjectRef: true });
|
|
191
190
|
clack.note('Now, please select your NextBlock project when prompted.');
|
|
192
191
|
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
|
-
|
|
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
|
+
|
|
203
202
|
let projectId = await readSupabaseProjectRef(projectPath);
|
|
204
203
|
|
|
205
204
|
if (!projectId) {
|
|
206
205
|
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;
|
|
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();
|
|
501
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 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,161 +681,146 @@ 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
|
-
|
|
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
716
|
async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
717
|
-
const { required = false
|
|
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
|
-
|
|
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
734
|
let migrationsCopied = false;
|
|
735
735
|
let configCopied = false;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
if (await fs.pathExists(sourceConfigPath)) {
|
|
736
|
+
|
|
737
|
+
const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
|
|
738
|
+
const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
|
|
739
|
+
if (await fs.pathExists(sourceConfigPath)) {
|
|
741
740
|
await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
|
|
742
741
|
configCopied = true;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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
|
+
|
|
766
751
|
if (required) {
|
|
767
752
|
if (!configCopied) {
|
|
768
753
|
throw new Error(
|
|
769
754
|
`Missing supabase/config.toml in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
|
|
770
755
|
);
|
|
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
|
|
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 };
|
|
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 };
|
|
837
765
|
}
|
|
838
|
-
|
|
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
|
+
|
|
839
824
|
async function readSupabaseProjectRef(projectDir) {
|
|
840
825
|
const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
|
|
841
826
|
if (await fs.pathExists(projectRefPath)) {
|
|
@@ -848,11 +833,20 @@ async function readSupabaseProjectRef(projectDir) {
|
|
|
848
833
|
return null;
|
|
849
834
|
}
|
|
850
835
|
|
|
851
|
-
async function
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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) {
|