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