create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +1183 -1013
  3. package/package.json +1 -1
  4. package/templates/base-nextjs/README.md +11 -3
  5. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  6. package/templates/base-nextjs/next.config.ts +7 -0
  7. package/templates/base-nextjs/package.json +6 -7
  8. package/templates/base-nextjs/src/app/globals.css +10 -9
  9. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  10. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  11. package/templates/marketing-site/convex/seed.ts +247 -6
  12. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +37 -0
  13. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  14. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  15. package/templates/marketing-site/src/app/layout.tsx +58 -0
  16. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +51 -0
  17. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  18. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  19. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
  20. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +1 -1
  21. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +1 -1
  22. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +2 -2
  23. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +1 -1
  24. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +2 -2
  25. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -44
  26. package/templates/marketing-site/src/vexcms/collections/pages.ts +3 -0
  27. /package/templates/marketing-site/src/app/(frontend)/{PageContent.tsx → (site)/PageContent.tsx} +0 -0
  28. /package/templates/marketing-site/src/app/(frontend)/{[slug] → (site)/[slug]}/page.tsx +0 -0
  29. /package/templates/marketing-site/src/app/(frontend)/{page.tsx → (site)/page.tsx} +0 -0
package/dist/index.js CHANGED
@@ -121,7 +121,7 @@ function rewriteManifestForMonorepo(props) {
121
121
  }
122
122
 
123
123
  // src/installers/base.ts
124
- import fs4 from "fs-extra";
124
+ import fs5 from "fs-extra";
125
125
  import { execa } from "execa";
126
126
  import ora from "ora";
127
127
  import crypto from "crypto";
@@ -173,585 +173,196 @@ async function overlayTemplate({ overlayDir, targetDir }) {
173
173
  await fs3.copy(overlayDir, targetDir, { overwrite: true });
174
174
  }
175
175
 
176
- // src/installers/base.ts
177
- var VexFrameworkInstaller = class {
178
- /**
179
- * Binds this installer to the destination directory and package name used
180
- * throughout scaffolding (template copy target, package.json, README, etc).
181
- *
182
- * @param targetPath - Absolute path of the directory the project is created in.
183
- * @param projectName - npm package name recorded for subclasses; not read directly by the base class.
184
- */
185
- constructor(targetPath, projectName) {
186
- this.targetPath = targetPath;
187
- this.projectName = projectName;
188
- }
189
- targetPath;
190
- projectName;
191
- /**
192
- * Copy base template files to target directory
193
- */
194
- async copyBaseFiles() {
195
- await copyTemplate(this.frameworkName, this.targetPath);
196
- }
197
- /**
198
- * Apply a template overlay on top of the base template.
199
- * Merges overlay files onto the already-copied base — existing files
200
- * not in the overlay are left untouched.
201
- *
202
- * @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
203
- */
204
- async applyTemplateOverlay(overlay) {
205
- const { dirname: dirname2 } = await import("path");
206
- const { fileURLToPath: fileURLToPath2 } = await import("url");
207
- const __filename = fileURLToPath2(import.meta.url);
208
- const __dirname = dirname2(__filename);
209
- let overlayDir = path2.join(__dirname, "../../templates", overlay);
210
- if (!await fs4.pathExists(overlayDir)) {
211
- overlayDir = path2.join(__dirname, "../templates", overlay);
176
+ // src/installers/string-utils.ts
177
+ import fs4 from "fs-extra";
178
+
179
+ // src/installers/providers.ts
180
+ var OAUTH_PROVIDERS = {
181
+ // ========================================
182
+ // POPULAR PROVIDERS (10)
183
+ // ========================================
184
+ google: {
185
+ id: "google",
186
+ name: "Google",
187
+ envPrefix: "GOOGLE",
188
+ clientIdVar: "GOOGLE_CLIENT_ID",
189
+ clientSecretVar: "GOOGLE_CLIENT_SECRET",
190
+ popular: true,
191
+ betterAuthConfig: {
192
+ import: "",
193
+ clientSideProvider: '"google"',
194
+ socialProvider: `google({
195
+ clientId: process.env.GOOGLE_CLIENT_ID!,
196
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
197
+ })`,
198
+ scopes: []
199
+ },
200
+ env: [
201
+ {
202
+ name: "GOOGLE_CLIENT_ID",
203
+ type: "server",
204
+ description: "Google OAuth Client ID"
205
+ },
206
+ {
207
+ name: "GOOGLE_CLIENT_SECRET",
208
+ type: "server",
209
+ description: "Google OAuth Client Secret"
210
+ }
211
+ ],
212
+ docs: {
213
+ provider: "https://console.cloud.google.com/apis/credentials",
214
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
215
+ },
216
+ requiresExtraConfig: false,
217
+ extraConfigNotes: "",
218
+ readme: {
219
+ title: "Google OAuth Setup",
220
+ content: `## Google OAuth Setup
221
+
222
+ 1. Create OAuth credentials at https://console.cloud.google.com/apis/credentials
223
+ 2. Set the Authorized redirect URI to: \`http://localhost:3000/api/auth/callback/google\` (update for production)
224
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
225
+
226
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
212
227
  }
213
- await overlayTemplate({ overlayDir, targetDir: this.targetPath });
214
- }
215
- /**
216
- * Update the package.json name field, the vex:update script (to match the
217
- * detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
218
- *
219
- * @param name - npm package name to write into `package.json`'s `name` field
220
- * and to substitute for `{{PROJECT_NAME}}` in `README.md`.
221
- */
222
- async updatePackageName(name) {
223
- const pkgPath = path2.join(this.targetPath, "package.json");
224
- const pkg = await fs4.readJson(pkgPath);
225
- pkg.name = name;
226
- const pm = this.detectPackageManager();
227
- if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
228
- const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
229
- pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
228
+ },
229
+ github: {
230
+ id: "github",
231
+ name: "GitHub",
232
+ envPrefix: "GITHUB",
233
+ clientIdVar: "GITHUB_CLIENT_ID",
234
+ clientSecretVar: "GITHUB_CLIENT_SECRET",
235
+ popular: true,
236
+ betterAuthConfig: {
237
+ import: "",
238
+ clientSideProvider: '"github"',
239
+ socialProvider: `github({
240
+ clientId: process.env.GITHUB_CLIENT_ID!,
241
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
242
+ })`,
243
+ scopes: ["user:email"]
244
+ },
245
+ env: [
246
+ {
247
+ name: "GITHUB_CLIENT_ID",
248
+ type: "server",
249
+ description: "GitHub OAuth App Client ID"
250
+ },
251
+ {
252
+ name: "GITHUB_CLIENT_SECRET",
253
+ type: "server",
254
+ description: "GitHub OAuth App Client Secret"
255
+ }
256
+ ],
257
+ docs: {
258
+ provider: "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app",
259
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
260
+ },
261
+ requiresExtraConfig: true,
262
+ extraConfigNotes: "You MUST include the user:email scope in your GitHub app. For GitHub Apps, enable Read-Only access to Email Addresses in Permissions.",
263
+ readme: {
264
+ title: "GitHub OAuth Setup",
265
+ content: `## GitHub OAuth Setup
266
+
267
+ 1. Create a GitHub OAuth App at https://github.com/settings/developers
268
+ 2. Set the Authorization callback URL to: \`http://localhost:3000/api/auth/callback/github\` (update for production)
269
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
270
+ 4. **Important**: Include the \`user:email\` scope in your GitHub app permissions
271
+
272
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
230
273
  }
231
- await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
232
- const readmePath = path2.join(this.targetPath, "README.md");
233
- if (await fs4.pathExists(readmePath)) {
234
- const readme = await fs4.readFile(readmePath, "utf-8");
235
- await fs4.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
274
+ },
275
+ discord: {
276
+ id: "discord",
277
+ name: "Discord",
278
+ envPrefix: "DISCORD",
279
+ clientIdVar: "DISCORD_CLIENT_ID",
280
+ clientSecretVar: "DISCORD_CLIENT_SECRET",
281
+ popular: true,
282
+ betterAuthConfig: {
283
+ import: "",
284
+ clientSideProvider: '"discord"',
285
+ socialProvider: `discord({
286
+ clientId: process.env.DISCORD_CLIENT_ID!,
287
+ clientSecret: process.env.DISCORD_CLIENT_SECRET!,
288
+ })`,
289
+ scopes: []
290
+ },
291
+ env: [
292
+ {
293
+ name: "DISCORD_CLIENT_ID",
294
+ type: "server",
295
+ description: "Discord OAuth Application Client ID"
296
+ },
297
+ {
298
+ name: "DISCORD_CLIENT_SECRET",
299
+ type: "server",
300
+ description: "Discord OAuth Application Client Secret"
301
+ }
302
+ ],
303
+ docs: {
304
+ provider: "https://discord.com/developers/applications",
305
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
306
+ },
307
+ requiresExtraConfig: false,
308
+ extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
309
+ readme: {
310
+ title: "Discord OAuth Setup",
311
+ content: `## Discord OAuth Setup
312
+
313
+ 1. Create an application at https://discord.com/developers/applications
314
+ 2. Add a redirect URL: \`http://localhost:3000/api/auth/callback/discord\` (update for production)
315
+ 3. Copy the Client ID and Client Secret to your \`.env\` file
316
+
317
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
236
318
  }
237
- }
238
- /**
239
- * Configure the dev server port.
240
- * Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
241
- *
242
- * @param port - Dev server port number to write into the `dev` script and `.env.local`.
243
- */
244
- async configurePort(port) {
245
- const pkgPath = path2.join(this.targetPath, "package.json");
246
- const pkg = await fs4.readJson(pkgPath);
247
- if (pkg.scripts?.dev) {
248
- if (pkg.scripts.dev.includes("--port")) {
249
- pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
250
- } else {
251
- pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
319
+ },
320
+ apple: {
321
+ id: "apple",
322
+ name: "Apple",
323
+ envPrefix: "APPLE",
324
+ clientIdVar: "APPLE_CLIENT_ID",
325
+ clientSecretVar: "APPLE_CLIENT_SECRET",
326
+ popular: true,
327
+ betterAuthConfig: {
328
+ import: "",
329
+ clientSideProvider: '"apple"',
330
+ socialProvider: `apple({
331
+ clientId: process.env.APPLE_CLIENT_ID!,
332
+ clientSecret: process.env.APPLE_CLIENT_SECRET!,
333
+ })`,
334
+ scopes: []
335
+ },
336
+ env: [
337
+ {
338
+ name: "APPLE_CLIENT_ID",
339
+ type: "server",
340
+ description: "Apple Sign In Service ID"
341
+ },
342
+ {
343
+ name: "APPLE_CLIENT_SECRET",
344
+ type: "server",
345
+ description: "Apple Sign In Client Secret (JWT token)"
252
346
  }
253
- }
254
- await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
255
- const siteUrl = `http://localhost:${port}`;
256
- const envLocalPath = path2.join(this.targetPath, ".env.local");
257
- const envContent = [
258
- `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
259
- `SITE_URL=${siteUrl}`,
260
- `BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
261
- `NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
262
- `NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
263
- `CONVEX_DEPLOYMENT=`,
264
- ""
265
- ].join("\n");
266
- await fs4.writeFile(envLocalPath, envContent);
267
- }
268
- /**
269
- * Rewrites the scaffolded `package.json` for life inside a host pnpm
270
- * workspace (`--monorepo`). `@vexcms/*` dependencies become
271
- * `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
272
- * catalog also pins becomes `catalog:`; everything else keeps its
273
- * literal, catalog-resolved version. No-op when `options.monorepo` is
274
- * false or `options.workspaceRoot` is unset.
275
- *
276
- * @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
277
- */
278
- async applyMonorepoRewrite(options) {
279
- if (!options.monorepo || !options.workspaceRoot) return;
280
- const pkgPath = path2.join(this.targetPath, "package.json");
281
- const manifest = await fs4.readJson(pkgPath);
282
- const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
283
- const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
284
- await fs4.writeJson(pkgPath, rewritten, { spaces: 2 });
285
- }
286
- /**
287
- * Detect the package manager used to invoke the CLI
288
- *
289
- * @returns The package manager inferred from `npm_config_user_agent`, defaulting to `"npm"` when it can't be determined.
290
- */
291
- detectPackageManager() {
292
- const userAgent = process.env.npm_config_user_agent;
293
- if (userAgent) {
294
- if (userAgent.includes("pnpm")) return "pnpm";
295
- if (userAgent.includes("yarn")) return "yarn";
296
- if (userAgent.includes("bun")) return "bun";
297
- }
298
- return "npm";
299
- }
300
- /**
301
- * Install project dependencies using detected package manager
302
- */
303
- async installDependencies() {
304
- const packageManager = this.detectPackageManager();
305
- const spinner = ora(`Installing dependencies with ${packageManager}...`).start();
306
- try {
307
- await execa(packageManager, ["install"], {
308
- cwd: this.targetPath,
309
- stdio: "pipe"
310
- });
311
- spinner.succeed("Dependencies installed successfully");
312
- } catch (error) {
313
- spinner.fail("Failed to install dependencies");
314
- throw new Error(
315
- `Dependency installation failed: ${error instanceof Error ? error.message : "Unknown error"}`
316
- );
317
- }
318
- }
319
- /**
320
- * Format code using project's formatter
321
- */
322
- async formatCode() {
323
- const packageManager = this.detectPackageManager();
324
- const spinner = ora("Formatting code...").start();
325
- try {
326
- const result = await execa(packageManager, ["run", "format"], {
327
- cwd: this.targetPath,
328
- stdio: "pipe",
329
- reject: false
330
- });
331
- if (result.exitCode === 0) {
332
- spinner.succeed("Code formatted successfully");
333
- } else {
334
- spinner.warn("Formatting completed with warnings");
335
- }
336
- } catch (error) {
337
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
338
- spinner.warn(`Failed to format code: ${errorMessage}`);
339
- }
340
- }
341
- /**
342
- * Lint and fix code using project's ESLint configuration
343
- */
344
- async lintCode() {
345
- const packageManager = this.detectPackageManager();
346
- const spinner = ora("Linting and fixing code...").start();
347
- try {
348
- const args2 = packageManager === "pnpm" ? ["lint", "--fix"] : ["run", "lint", "--", "--fix"];
349
- const result = await execa(packageManager, args2, {
350
- cwd: this.targetPath,
351
- stdio: "pipe",
352
- reject: false
353
- });
354
- if (result.exitCode === 0) {
355
- spinner.succeed("Code linted and fixed successfully");
356
- } else {
357
- spinner.warn("Linting completed with warnings");
358
- }
359
- } catch (error) {
360
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
361
- spinner.warn(`Failed to lint code: ${errorMessage}`);
362
- }
363
- }
364
- /**
365
- * Initialize Git repository in target directory
366
- */
367
- async initGitRepo() {
368
- const spinner = ora("Initializing Git repository...").start();
369
- try {
370
- try {
371
- await execa("git", ["--version"], { stdio: "pipe" });
372
- } catch {
373
- spinner.fail("Git is not installed");
374
- throw new Error("Git is not installed. Please install Git to initialize a repository.");
375
- }
376
- await execa("git", ["init"], { cwd: this.targetPath, stdio: "pipe" });
377
- await execa("git", ["add", "."], { cwd: this.targetPath, stdio: "pipe" });
378
- await execa(
379
- "git",
380
- ["commit", "-m", "Initial commit from create-vexcms"],
381
- { cwd: this.targetPath, stdio: "pipe" }
382
- );
383
- spinner.succeed("Git repository initialized");
384
- } catch (error) {
385
- spinner.fail("Failed to initialize Git repository");
386
- throw new Error(
387
- `Git initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
388
- );
389
- }
390
- }
391
- /**
392
- * Generate a secure random secret for Better Auth
393
- *
394
- * @returns A 64-character hex string (32 random bytes) suitable for `BETTER_AUTH_SECRET`.
395
- */
396
- generateAuthSecret() {
397
- return crypto.randomBytes(32).toString("hex");
398
- }
399
- /**
400
- * Write the generated auth secret to .env.example
401
- */
402
- async writeAuthSecret() {
403
- const envPath = path2.join(this.targetPath, ".env.example");
404
- const secret = this.generateAuthSecret();
405
- if (await fs4.pathExists(envPath)) {
406
- let content = await fs4.readFile(envPath, "utf-8");
407
- content = content.replace(
408
- /BETTER_AUTH_SECRET=.*/,
409
- `BETTER_AUTH_SECRET=${secret}`
410
- );
411
- await fs4.writeFile(envPath, content);
412
- }
413
- }
414
- /**
415
- * Configure the Better Auth organizations plugin.
416
- * When enabled, replaces placeholders with organization import and plugin.
417
- * When disabled, removes the placeholder lines.
418
- *
419
- * @param enabled - Whether the organizations (multi-tenant) plugin should be wired into `convex/auth/plugins/index.ts`.
420
- */
421
- async configureOrganizations(enabled) {
422
- const pluginsPath = path2.join(this.targetPath, "convex/auth/plugins/index.ts");
423
- if (!await fs4.pathExists(pluginsPath)) return;
424
- let content = await fs4.readFile(pluginsPath, "utf-8");
425
- if (enabled) {
426
- content = content.replace(
427
- "// {{ORGANIZATIONS_IMPORT}}",
428
- 'import { organization } from "better-auth/plugins"'
429
- );
430
- content = content.replace(
431
- " // {{ORGANIZATIONS_PLUGIN}}",
432
- " organization(),"
433
- );
434
- } else {
435
- content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
436
- content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
437
- }
438
- await fs4.writeFile(pluginsPath, content);
439
- }
440
- /**
441
- * Main orchestration method for project initialization
442
- *
443
- * @param options - Resolved project options from CLI flags and prompts, driving every scaffolding step (template copy, overlay, port, auth, README, git, install).
444
- */
445
- async initProject(options) {
446
- const copySpinner = ora("Copying template files...").start();
447
- try {
448
- await this.copyBaseFiles();
449
- copySpinner.succeed("Template files copied");
450
- } catch (error) {
451
- copySpinner.fail("Failed to copy template files");
452
- throw error;
453
- }
454
- if (!options.bare) {
455
- const overlaySpinner = ora("Applying marketing site template...").start();
456
- try {
457
- await this.applyTemplateOverlay("marketing-site");
458
- overlaySpinner.succeed("Marketing site template applied");
459
- } catch (error) {
460
- overlaySpinner.fail("Failed to apply template overlay");
461
- throw error;
462
- }
463
- }
464
- const nameSpinner = ora("Configuring project...").start();
465
- try {
466
- await this.updatePackageName(options.projectName);
467
- await this.configurePort(options.port);
468
- nameSpinner.succeed("Project configured");
469
- } catch (error) {
470
- nameSpinner.fail("Failed to configure project");
471
- throw error;
472
- }
473
- const authSpinner = ora("Configuring authentication...").start();
474
- try {
475
- await this.updateOAuthConfig(options.oauthProviders, options.emailPasswordAuth);
476
- authSpinner.succeed(
477
- options.emailPasswordAuth || options.oauthProviders.length > 0 ? "Authentication configuration updated" : "Authentication placeholders cleaned up"
478
- );
479
- } catch (error) {
480
- authSpinner.fail("Failed to configure authentication");
481
- throw error;
482
- }
483
- const oauthUISpinner = ora("Configuring OAuth UI...").start();
484
- try {
485
- await this.updateOAuthUIConfig(options.oauthProviders, options.emailPasswordAuth);
486
- oauthUISpinner.succeed(
487
- options.oauthProviders.length > 0 ? "OAuth UI configuration updated" : "OAuth UI placeholders cleaned up"
488
- );
489
- } catch (error) {
490
- oauthUISpinner.fail("Failed to configure OAuth UI");
491
- throw error;
492
- }
493
- const envSpinner = ora("Updating .env.example...").start();
494
- try {
495
- await this.updateEnvExample(options.oauthProviders);
496
- envSpinner.succeed(
497
- options.oauthProviders.length > 0 ? ".env.example updated" : ".env.example placeholders cleaned up"
498
- );
499
- } catch (error) {
500
- envSpinner.fail("Failed to update .env.example");
501
- throw error;
502
- }
503
- const envTsSpinner = ora("Updating typed env configuration...").start();
504
- try {
505
- await this.updateEnvTs(options.oauthProviders);
506
- envTsSpinner.succeed(
507
- options.oauthProviders.length > 0 ? "Typed env configuration updated" : "Typed env placeholders cleaned up"
508
- );
509
- } catch (error) {
510
- envTsSpinner.fail("Failed to update typed env configuration");
511
- throw error;
512
- }
513
- const readmeSpinner = ora("Updating README...").start();
514
- try {
515
- await this.updateReadme(options.oauthProviders);
516
- readmeSpinner.succeed(
517
- options.oauthProviders.length > 0 ? "README updated" : "README placeholders cleaned up"
518
- );
519
- } catch (error) {
520
- readmeSpinner.fail("Failed to update README");
521
- throw error;
522
- }
523
- const orgsSpinner = ora("Configuring organizations...").start();
524
- try {
525
- await this.configureOrganizations(options.orgs);
526
- orgsSpinner.succeed(
527
- options.orgs ? "Organizations plugin enabled" : "Organizations placeholders cleaned up"
528
- );
529
- } catch (error) {
530
- orgsSpinner.fail("Failed to configure organizations");
531
- throw error;
532
- }
533
- const secretSpinner = ora("Generating auth secret...").start();
534
- try {
535
- await this.writeAuthSecret();
536
- secretSpinner.succeed("Auth secret generated");
537
- } catch (error) {
538
- secretSpinner.fail("Failed to generate auth secret");
539
- throw error;
540
- }
541
- if (options.monorepo) {
542
- const monorepoSpinner = ora("Rewriting dependencies for the host workspace...").start();
543
- try {
544
- await this.applyMonorepoRewrite(options);
545
- monorepoSpinner.succeed("Dependencies rewritten for the host workspace");
546
- } catch (error) {
547
- monorepoSpinner.fail("Failed to rewrite dependencies for the host workspace");
548
- throw error;
549
- }
550
- }
551
- if (options.initGit && !options.monorepo) {
552
- await this.initGitRepo();
553
- }
554
- if (options.installDependencies && !options.monorepo) {
555
- await this.installDependencies();
556
- await this.lintCode();
557
- await this.formatCode();
558
- }
559
- }
560
- };
561
-
562
- // src/installers/nextjs.ts
563
- import { join as join2 } from "path";
564
-
565
- // src/installers/string-utils.ts
566
- import fs5 from "fs-extra";
567
-
568
- // src/installers/providers.ts
569
- var OAUTH_PROVIDERS = {
570
- // ========================================
571
- // POPULAR PROVIDERS (10)
572
- // ========================================
573
- google: {
574
- id: "google",
575
- name: "Google",
576
- envPrefix: "GOOGLE",
577
- clientIdVar: "GOOGLE_CLIENT_ID",
578
- clientSecretVar: "GOOGLE_CLIENT_SECRET",
579
- popular: true,
580
- betterAuthConfig: {
581
- import: "",
582
- clientSideProvider: '"google"',
583
- socialProvider: `google({
584
- clientId: process.env.GOOGLE_CLIENT_ID!,
585
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
586
- })`,
587
- scopes: []
588
- },
589
- env: [
590
- {
591
- name: "GOOGLE_CLIENT_ID",
592
- type: "server",
593
- description: "Google OAuth Client ID"
594
- },
595
- {
596
- name: "GOOGLE_CLIENT_SECRET",
597
- type: "server",
598
- description: "Google OAuth Client Secret"
599
- }
600
- ],
601
- docs: {
602
- provider: "https://console.cloud.google.com/apis/credentials",
603
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
604
- },
605
- requiresExtraConfig: false,
606
- extraConfigNotes: "",
607
- readme: {
608
- title: "Google OAuth Setup",
609
- content: `## Google OAuth Setup
610
-
611
- 1. Create OAuth credentials at https://console.cloud.google.com/apis/credentials
612
- 2. Set the Authorized redirect URI to: \`http://localhost:3000/api/auth/callback/google\` (update for production)
613
- 3. Copy the Client ID and Client Secret to your \`.env\` file
614
-
615
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
616
- }
617
- },
618
- github: {
619
- id: "github",
620
- name: "GitHub",
621
- envPrefix: "GITHUB",
622
- clientIdVar: "GITHUB_CLIENT_ID",
623
- clientSecretVar: "GITHUB_CLIENT_SECRET",
624
- popular: true,
625
- betterAuthConfig: {
626
- import: "",
627
- clientSideProvider: '"github"',
628
- socialProvider: `github({
629
- clientId: process.env.GITHUB_CLIENT_ID!,
630
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
631
- })`,
632
- scopes: ["user:email"]
633
- },
634
- env: [
635
- {
636
- name: "GITHUB_CLIENT_ID",
637
- type: "server",
638
- description: "GitHub OAuth App Client ID"
639
- },
640
- {
641
- name: "GITHUB_CLIENT_SECRET",
642
- type: "server",
643
- description: "GitHub OAuth App Client Secret"
644
- }
645
- ],
646
- docs: {
647
- provider: "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app",
648
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
649
- },
650
- requiresExtraConfig: true,
651
- extraConfigNotes: "You MUST include the user:email scope in your GitHub app. For GitHub Apps, enable Read-Only access to Email Addresses in Permissions.",
652
- readme: {
653
- title: "GitHub OAuth Setup",
654
- content: `## GitHub OAuth Setup
655
-
656
- 1. Create a GitHub OAuth App at https://github.com/settings/developers
657
- 2. Set the Authorization callback URL to: \`http://localhost:3000/api/auth/callback/github\` (update for production)
658
- 3. Copy the Client ID and Client Secret to your \`.env\` file
659
- 4. **Important**: Include the \`user:email\` scope in your GitHub app permissions
660
-
661
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
662
- }
663
- },
664
- discord: {
665
- id: "discord",
666
- name: "Discord",
667
- envPrefix: "DISCORD",
668
- clientIdVar: "DISCORD_CLIENT_ID",
669
- clientSecretVar: "DISCORD_CLIENT_SECRET",
670
- popular: true,
671
- betterAuthConfig: {
672
- import: "",
673
- clientSideProvider: '"discord"',
674
- socialProvider: `discord({
675
- clientId: process.env.DISCORD_CLIENT_ID!,
676
- clientSecret: process.env.DISCORD_CLIENT_SECRET!,
677
- })`,
678
- scopes: []
679
- },
680
- env: [
681
- {
682
- name: "DISCORD_CLIENT_ID",
683
- type: "server",
684
- description: "Discord OAuth Application Client ID"
685
- },
686
- {
687
- name: "DISCORD_CLIENT_SECRET",
688
- type: "server",
689
- description: "Discord OAuth Application Client Secret"
690
- }
691
- ],
692
- docs: {
693
- provider: "https://discord.com/developers/applications",
694
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
695
- },
696
- requiresExtraConfig: false,
697
- extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
698
- readme: {
699
- title: "Discord OAuth Setup",
700
- content: `## Discord OAuth Setup
701
-
702
- 1. Create an application at https://discord.com/developers/applications
703
- 2. Add a redirect URL: \`http://localhost:3000/api/auth/callback/discord\` (update for production)
704
- 3. Copy the Client ID and Client Secret to your \`.env\` file
705
-
706
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
707
- }
708
- },
709
- apple: {
710
- id: "apple",
711
- name: "Apple",
712
- envPrefix: "APPLE",
713
- clientIdVar: "APPLE_CLIENT_ID",
714
- clientSecretVar: "APPLE_CLIENT_SECRET",
715
- popular: true,
716
- betterAuthConfig: {
717
- import: "",
718
- clientSideProvider: '"apple"',
719
- socialProvider: `apple({
720
- clientId: process.env.APPLE_CLIENT_ID!,
721
- clientSecret: process.env.APPLE_CLIENT_SECRET!,
722
- })`,
723
- scopes: []
724
- },
725
- env: [
726
- {
727
- name: "APPLE_CLIENT_ID",
728
- type: "server",
729
- description: "Apple Sign In Service ID"
730
- },
731
- {
732
- name: "APPLE_CLIENT_SECRET",
733
- type: "server",
734
- description: "Apple Sign In Client Secret (JWT token)"
735
- }
736
- ],
737
- docs: {
738
- provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
739
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
740
- },
741
- requiresExtraConfig: true,
742
- extraConfigNotes: "Apple requires additional configuration: clientId is your Service ID, clientSecret must be a JWT token generated from your Team ID, Key ID, and Private Key. You may also need to provide appBundleIdentifier for iOS apps.",
743
- readme: {
744
- title: "Apple Sign In Setup",
745
- content: `## Apple Sign In Setup
746
-
747
- 1. Create a Sign In with Apple service at https://developer.apple.com
748
- 2. Configure your Service ID and return URLs
749
- 3. Generate a client secret JWT using your private key, Team ID, and Key ID
750
- 4. Add credentials to your \`.env\` file
751
-
752
- **Note**: The clientSecret must be a JWT token generated from your Apple credentials, not a standard client secret.
753
-
754
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
347
+ ],
348
+ docs: {
349
+ provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
350
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
351
+ },
352
+ requiresExtraConfig: true,
353
+ extraConfigNotes: "Apple requires additional configuration: clientId is your Service ID, clientSecret must be a JWT token generated from your Team ID, Key ID, and Private Key. You may also need to provide appBundleIdentifier for iOS apps.",
354
+ readme: {
355
+ title: "Apple Sign In Setup",
356
+ content: `## Apple Sign In Setup
357
+
358
+ 1. Create a Sign In with Apple service at https://developer.apple.com
359
+ 2. Configure your Service ID and return URLs
360
+ 3. Generate a client secret JWT using your private key, Team ID, and Key ID
361
+ 4. Add credentials to your \`.env\` file
362
+
363
+ **Note**: The clientSecret must be a JWT token generated from your Apple credentials, not a standard client secret.
364
+
365
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
755
366
  }
756
367
  },
757
368
  microsoft: {
@@ -1829,511 +1440,1070 @@ For more details, see the [Better Auth documentation](https://www.better-auth.co
1829
1440
  },
1830
1441
  env: [
1831
1442
  {
1832
- name: "ROBLOX_CLIENT_ID",
1443
+ name: "ROBLOX_CLIENT_ID",
1444
+ type: "server",
1445
+ description: "Roblox OAuth Client ID"
1446
+ },
1447
+ {
1448
+ name: "ROBLOX_CLIENT_SECRET",
1449
+ type: "server",
1450
+ description: "Roblox OAuth Client Secret"
1451
+ }
1452
+ ],
1453
+ docs: {
1454
+ provider: "https://create.roblox.com/credentials",
1455
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
1456
+ },
1457
+ requiresExtraConfig: true,
1458
+ extraConfigNotes: "Note: Roblox OAuth does not provide user email addresses.",
1459
+ readme: {
1460
+ title: "Roblox OAuth Setup",
1461
+ content: `## Roblox OAuth Setup
1462
+
1463
+ 1. Create OAuth credentials at https://create.roblox.com/credentials
1464
+ 2. Configure OAuth 2.0 settings
1465
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/roblox\` (update for production)
1466
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1467
+
1468
+ **Note**: Roblox does not provide email addresses through OAuth.
1469
+
1470
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1471
+ }
1472
+ },
1473
+ salesforce: {
1474
+ id: "salesforce",
1475
+ name: "Salesforce",
1476
+ envPrefix: "SALESFORCE",
1477
+ clientIdVar: "SALESFORCE_CLIENT_ID",
1478
+ clientSecretVar: "SALESFORCE_CLIENT_SECRET",
1479
+ popular: false,
1480
+ betterAuthConfig: {
1481
+ import: "",
1482
+ clientSideProvider: '"salesforce"',
1483
+ socialProvider: `salesforce({
1484
+ clientId: process.env.SALESFORCE_CLIENT_ID!,
1485
+ clientSecret: process.env.SALESFORCE_CLIENT_SECRET!,
1486
+ environment: process.env.SALESFORCE_ENVIRONMENT || "login",
1487
+ })`,
1488
+ scopes: []
1489
+ },
1490
+ env: [
1491
+ {
1492
+ name: "SALESFORCE_CLIENT_ID",
1493
+ type: "server",
1494
+ description: "Salesforce Connected App Consumer Key"
1495
+ },
1496
+ {
1497
+ name: "SALESFORCE_CLIENT_SECRET",
1498
+ type: "server",
1499
+ description: "Salesforce Connected App Consumer Secret"
1500
+ },
1501
+ {
1502
+ name: "SALESFORCE_ENVIRONMENT",
1503
+ type: "server",
1504
+ description: 'Salesforce environment: "login" (production) or "test" (sandbox) (default: login)'
1505
+ }
1506
+ ],
1507
+ docs: {
1508
+ provider: "https://developer.salesforce.com/",
1509
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
1510
+ },
1511
+ requiresExtraConfig: true,
1512
+ extraConfigNotes: 'Salesforce supports environment field: "login" for production, "test" for sandbox.',
1513
+ readme: {
1514
+ title: "Salesforce OAuth Setup",
1515
+ content: `## Salesforce OAuth Setup
1516
+
1517
+ 1. Create a Connected App in Salesforce Setup
1518
+ 2. Enable OAuth Settings
1519
+ 3. Add callback URL: \`http://localhost:3000/api/auth/callback/salesforce\` (update for production)
1520
+ 4. Copy the Consumer Key and Consumer Secret to your \`.env\` file
1521
+ 5. Set SALESFORCE_ENVIRONMENT to "login" for production or "test" for sandbox
1522
+
1523
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1524
+ }
1525
+ },
1526
+ slack: {
1527
+ id: "slack",
1528
+ name: "Slack",
1529
+ envPrefix: "SLACK",
1530
+ clientIdVar: "SLACK_CLIENT_ID",
1531
+ clientSecretVar: "SLACK_CLIENT_SECRET",
1532
+ popular: false,
1533
+ betterAuthConfig: {
1534
+ import: "",
1535
+ clientSideProvider: '"slack"',
1536
+ socialProvider: `slack({
1537
+ clientId: process.env.SLACK_CLIENT_ID!,
1538
+ clientSecret: process.env.SLACK_CLIENT_SECRET!,
1539
+ })`,
1540
+ scopes: []
1541
+ },
1542
+ env: [
1543
+ {
1544
+ name: "SLACK_CLIENT_ID",
1545
+ type: "server",
1546
+ description: "Slack App Client ID"
1547
+ },
1548
+ {
1549
+ name: "SLACK_CLIENT_SECRET",
1550
+ type: "server",
1551
+ description: "Slack App Client Secret"
1552
+ }
1553
+ ],
1554
+ docs: {
1555
+ provider: "https://api.slack.com/apps",
1556
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
1557
+ },
1558
+ requiresExtraConfig: false,
1559
+ extraConfigNotes: "Supports optional team field for workspace restrictions.",
1560
+ readme: {
1561
+ title: "Slack OAuth Setup",
1562
+ content: `## Slack OAuth Setup
1563
+
1564
+ 1. Create a Slack app at https://api.slack.com/apps
1565
+ 2. Add OAuth & Permissions and configure redirect URLs
1566
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/slack\` (update for production)
1567
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1568
+
1569
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1570
+ }
1571
+ },
1572
+ tiktok: {
1573
+ id: "tiktok",
1574
+ name: "TikTok",
1575
+ envPrefix: "TIKTOK",
1576
+ clientIdVar: "TIKTOK_CLIENT_KEY",
1577
+ clientSecretVar: "TIKTOK_CLIENT_SECRET",
1578
+ popular: false,
1579
+ betterAuthConfig: {
1580
+ import: "",
1581
+ clientSideProvider: '"tiktok"',
1582
+ socialProvider: `tiktok({
1583
+ clientKey: process.env.TIKTOK_CLIENT_KEY!,
1584
+ clientSecret: process.env.TIKTOK_CLIENT_SECRET!,
1585
+ })`,
1586
+ scopes: []
1587
+ },
1588
+ env: [
1589
+ {
1590
+ name: "TIKTOK_CLIENT_KEY",
1591
+ type: "server",
1592
+ description: "TikTok Client Key (not Client ID)"
1593
+ },
1594
+ {
1595
+ name: "TIKTOK_CLIENT_SECRET",
1596
+ type: "server",
1597
+ description: "TikTok Client Secret"
1598
+ }
1599
+ ],
1600
+ docs: {
1601
+ provider: "https://developers.tiktok.com/",
1602
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
1603
+ },
1604
+ requiresExtraConfig: true,
1605
+ extraConfigNotes: "TikTok uses clientKey instead of clientId. Make sure to use the correct field name.",
1606
+ readme: {
1607
+ title: "TikTok OAuth Setup",
1608
+ content: `## TikTok OAuth Setup
1609
+
1610
+ 1. Register an app at https://developers.tiktok.com/
1611
+ 2. Configure Login Kit
1612
+ 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/tiktok\` (update for production)
1613
+ 4. Copy the Client Key (not Client ID) and Client Secret to your \`.env\` file
1614
+
1615
+ **Important**: TikTok uses clientKey instead of clientId.
1616
+
1617
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1618
+ }
1619
+ },
1620
+ vercel: {
1621
+ id: "vercel",
1622
+ name: "Vercel",
1623
+ envPrefix: "VERCEL",
1624
+ clientIdVar: "VERCEL_CLIENT_ID",
1625
+ clientSecretVar: "VERCEL_CLIENT_SECRET",
1626
+ popular: false,
1627
+ betterAuthConfig: {
1628
+ import: "",
1629
+ clientSideProvider: '"vercel"',
1630
+ socialProvider: `vercel({
1631
+ clientId: process.env.VERCEL_CLIENT_ID!,
1632
+ clientSecret: process.env.VERCEL_CLIENT_SECRET!,
1633
+ })`,
1634
+ scopes: []
1635
+ },
1636
+ env: [
1637
+ {
1638
+ name: "VERCEL_CLIENT_ID",
1639
+ type: "server",
1640
+ description: "Vercel OAuth Client ID"
1641
+ },
1642
+ {
1643
+ name: "VERCEL_CLIENT_SECRET",
1644
+ type: "server",
1645
+ description: "Vercel OAuth Client Secret"
1646
+ }
1647
+ ],
1648
+ docs: {
1649
+ provider: "https://vercel.com/account/integrations",
1650
+ betterAuth: "https://www.better-auth.com/docs/authentication/social"
1651
+ },
1652
+ requiresExtraConfig: false,
1653
+ extraConfigNotes: "Uses PKCE for enhanced security.",
1654
+ readme: {
1655
+ title: "Vercel OAuth Setup",
1656
+ content: `## Vercel OAuth Setup
1657
+
1658
+ 1. Create an integration at https://vercel.com/account/integrations
1659
+ 2. Configure OAuth settings
1660
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/vercel\` (update for production)
1661
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1662
+
1663
+ For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1664
+ }
1665
+ },
1666
+ vk: {
1667
+ id: "vk",
1668
+ name: "VK",
1669
+ envPrefix: "VK",
1670
+ clientIdVar: "VK_CLIENT_ID",
1671
+ clientSecretVar: "VK_CLIENT_SECRET",
1672
+ popular: false,
1673
+ betterAuthConfig: {
1674
+ import: "",
1675
+ clientSideProvider: '"vk"',
1676
+ socialProvider: `vk({
1677
+ clientId: process.env.VK_CLIENT_ID!,
1678
+ clientSecret: process.env.VK_CLIENT_SECRET!,
1679
+ })`,
1680
+ scopes: []
1681
+ },
1682
+ env: [
1683
+ {
1684
+ name: "VK_CLIENT_ID",
1833
1685
  type: "server",
1834
- description: "Roblox OAuth Client ID"
1686
+ description: "VK Application ID"
1835
1687
  },
1836
1688
  {
1837
- name: "ROBLOX_CLIENT_SECRET",
1689
+ name: "VK_CLIENT_SECRET",
1838
1690
  type: "server",
1839
- description: "Roblox OAuth Client Secret"
1691
+ description: "VK Secure Key"
1840
1692
  }
1841
1693
  ],
1842
1694
  docs: {
1843
- provider: "https://create.roblox.com/credentials",
1695
+ provider: "https://vk.com/apps?act=manage",
1844
1696
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1845
1697
  },
1846
- requiresExtraConfig: true,
1847
- extraConfigNotes: "Note: Roblox OAuth does not provide user email addresses.",
1698
+ requiresExtraConfig: false,
1699
+ extraConfigNotes: "",
1848
1700
  readme: {
1849
- title: "Roblox OAuth Setup",
1850
- content: `## Roblox OAuth Setup
1851
-
1852
- 1. Create OAuth credentials at https://create.roblox.com/credentials
1853
- 2. Configure OAuth 2.0 settings
1854
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/roblox\` (update for production)
1855
- 4. Copy the Client ID and Client Secret to your \`.env\` file
1701
+ title: "VK OAuth Setup",
1702
+ content: `## VK OAuth Setup
1856
1703
 
1857
- **Note**: Roblox does not provide email addresses through OAuth.
1704
+ 1. Create an app at https://vk.com/apps?act=manage
1705
+ 2. Configure OAuth settings in the app
1706
+ 3. Add authorized redirect URI: \`http://localhost:3000/api/auth/callback/vk\` (update for production)
1707
+ 4. Copy the Application ID and Secure Key to your \`.env\` file
1858
1708
 
1859
1709
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1860
1710
  }
1861
1711
  },
1862
- salesforce: {
1863
- id: "salesforce",
1864
- name: "Salesforce",
1865
- envPrefix: "SALESFORCE",
1866
- clientIdVar: "SALESFORCE_CLIENT_ID",
1867
- clientSecretVar: "SALESFORCE_CLIENT_SECRET",
1712
+ zoom: {
1713
+ id: "zoom",
1714
+ name: "Zoom",
1715
+ envPrefix: "ZOOM",
1716
+ clientIdVar: "ZOOM_CLIENT_ID",
1717
+ clientSecretVar: "ZOOM_CLIENT_SECRET",
1868
1718
  popular: false,
1869
1719
  betterAuthConfig: {
1870
1720
  import: "",
1871
- clientSideProvider: '"salesforce"',
1872
- socialProvider: `salesforce({
1873
- clientId: process.env.SALESFORCE_CLIENT_ID!,
1874
- clientSecret: process.env.SALESFORCE_CLIENT_SECRET!,
1875
- environment: process.env.SALESFORCE_ENVIRONMENT || "login",
1721
+ clientSideProvider: '"zoom"',
1722
+ socialProvider: `zoom({
1723
+ clientId: process.env.ZOOM_CLIENT_ID!,
1724
+ clientSecret: process.env.ZOOM_CLIENT_SECRET!,
1876
1725
  })`,
1877
- scopes: []
1726
+ scopes: ["user:read:user"]
1878
1727
  },
1879
1728
  env: [
1880
1729
  {
1881
- name: "SALESFORCE_CLIENT_ID",
1882
- type: "server",
1883
- description: "Salesforce Connected App Consumer Key"
1884
- },
1885
- {
1886
- name: "SALESFORCE_CLIENT_SECRET",
1730
+ name: "ZOOM_CLIENT_ID",
1887
1731
  type: "server",
1888
- description: "Salesforce Connected App Consumer Secret"
1732
+ description: "Zoom OAuth Client ID"
1889
1733
  },
1890
1734
  {
1891
- name: "SALESFORCE_ENVIRONMENT",
1735
+ name: "ZOOM_CLIENT_SECRET",
1892
1736
  type: "server",
1893
- description: 'Salesforce environment: "login" (production) or "test" (sandbox) (default: login)'
1737
+ description: "Zoom OAuth Client Secret"
1894
1738
  }
1895
1739
  ],
1896
1740
  docs: {
1897
- provider: "https://developer.salesforce.com/",
1741
+ provider: "https://marketplace.zoom.us/",
1898
1742
  betterAuth: "https://www.better-auth.com/docs/authentication/social"
1899
1743
  },
1900
1744
  requiresExtraConfig: true,
1901
- extraConfigNotes: 'Salesforce supports environment field: "login" for production, "test" for sandbox.',
1745
+ extraConfigNotes: "You MUST include the user:read:user scope for Zoom.",
1902
1746
  readme: {
1903
- title: "Salesforce OAuth Setup",
1904
- content: `## Salesforce OAuth Setup
1747
+ title: "Zoom OAuth Setup",
1748
+ content: `## Zoom OAuth Setup
1905
1749
 
1906
- 1. Create a Connected App in Salesforce Setup
1907
- 2. Enable OAuth Settings
1908
- 3. Add callback URL: \`http://localhost:3000/api/auth/callback/salesforce\` (update for production)
1909
- 4. Copy the Consumer Key and Consumer Secret to your \`.env\` file
1910
- 5. Set SALESFORCE_ENVIRONMENT to "login" for production or "test" for sandbox
1750
+ 1. Create an app at https://marketplace.zoom.us/
1751
+ 2. Choose OAuth as the app type
1752
+ 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/zoom\` (update for production)
1753
+ 4. Copy the Client ID and Client Secret to your \`.env\` file
1754
+ 5. **Important**: Make sure to include the \`user:read:user\` scope
1911
1755
 
1912
1756
  For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
1913
1757
  }
1914
- },
1915
- slack: {
1916
- id: "slack",
1917
- name: "Slack",
1918
- envPrefix: "SLACK",
1919
- clientIdVar: "SLACK_CLIENT_ID",
1920
- clientSecretVar: "SLACK_CLIENT_SECRET",
1921
- popular: false,
1922
- betterAuthConfig: {
1923
- import: "",
1924
- clientSideProvider: '"slack"',
1925
- socialProvider: `slack({
1926
- clientId: process.env.SLACK_CLIENT_ID!,
1927
- clientSecret: process.env.SLACK_CLIENT_SECRET!,
1928
- })`,
1929
- scopes: []
1930
- },
1931
- env: [
1932
- {
1933
- name: "SLACK_CLIENT_ID",
1934
- type: "server",
1935
- description: "Slack App Client ID"
1936
- },
1937
- {
1938
- name: "SLACK_CLIENT_SECRET",
1939
- type: "server",
1940
- description: "Slack App Client Secret"
1941
- }
1942
- ],
1943
- docs: {
1944
- provider: "https://api.slack.com/apps",
1945
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1946
- },
1947
- requiresExtraConfig: false,
1948
- extraConfigNotes: "Supports optional team field for workspace restrictions.",
1949
- readme: {
1950
- title: "Slack OAuth Setup",
1951
- content: `## Slack OAuth Setup
1758
+ }
1759
+ };
1760
+ function getProvider(id) {
1761
+ return OAUTH_PROVIDERS[id];
1762
+ }
1763
+ function getPopularProviders() {
1764
+ return Object.values(OAUTH_PROVIDERS).filter(
1765
+ (provider) => provider.popular === true
1766
+ );
1767
+ }
1768
+ function getAdditionalProviders() {
1769
+ return Object.values(OAUTH_PROVIDERS).filter(
1770
+ (provider) => provider.popular !== true
1771
+ );
1772
+ }
1773
+
1774
+ // src/installers/string-utils.ts
1775
+ function detectIndentation(line) {
1776
+ const match = line.match(/^(\s*)/);
1777
+ return match ? match[1] : "";
1778
+ }
1779
+ async function replacePlaceholder(filePath, placeholder, content, options) {
1780
+ const fileContent = await fs4.readFile(filePath, "utf-8");
1781
+ if (!fileContent.includes(placeholder)) {
1782
+ if (options?.graceful) {
1783
+ console.warn(
1784
+ `Warning: Placeholder "${placeholder}" not found in file: ${filePath}. Skipping replacement.`
1785
+ );
1786
+ return;
1787
+ }
1788
+ throw new Error(
1789
+ `Placeholder "${placeholder}" not found in file: ${filePath}`
1790
+ );
1791
+ }
1792
+ if (options?.inline) {
1793
+ const updatedContent = fileContent.replace(placeholder, content);
1794
+ await fs4.writeFile(filePath, updatedContent, "utf-8");
1795
+ return;
1796
+ }
1797
+ const lines = fileContent.split("\n");
1798
+ const updatedLines = [];
1799
+ for (const line of lines) {
1800
+ if (line.includes(placeholder)) {
1801
+ if (content === "" || content.startsWith("__REMOVE_")) {
1802
+ continue;
1803
+ }
1804
+ const indentation = detectIndentation(line);
1805
+ const indentedContent = content.split("\n").map((contentLine, index) => {
1806
+ if (index === 0) {
1807
+ return indentation + contentLine;
1808
+ }
1809
+ return contentLine ? indentation + contentLine : "";
1810
+ }).join("\n");
1811
+ updatedLines.push(indentedContent);
1812
+ } else {
1813
+ updatedLines.push(line);
1814
+ }
1815
+ }
1816
+ await fs4.writeFile(filePath, updatedLines.join("\n"), "utf-8");
1817
+ }
1818
+ function generateCredentialsValue(enabled) {
1819
+ return `credentials={${enabled}}`;
1820
+ }
1821
+ function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
1822
+ const parts = [];
1823
+ parts.push(`emailAndPassword: {
1824
+ enabled: ${emailPasswordEnabled}
1825
+ },`);
1826
+ if (oauthProviders.length > 0) {
1827
+ const providersObject = oauthProviders.map((providerId) => {
1828
+ const provider = getProvider(providerId);
1829
+ if (!provider) {
1830
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1831
+ }
1832
+ const configLines = [
1833
+ `clientId: process.env.${provider.envPrefix}_CLIENT_ID!,`,
1834
+ `clientSecret: process.env.${provider.envPrefix}_CLIENT_SECRET!,`
1835
+ ];
1836
+ if (providerId === "figma") {
1837
+ configLines.push(`clientKey: process.env.FIGMA_CLIENT_KEY!,`);
1838
+ }
1839
+ return `${providerId}: {
1840
+ ${configLines.join("\n ")}
1841
+ }`;
1842
+ }).join(",\n ");
1843
+ parts.push(`socialProviders: {
1844
+ ${providersObject}
1845
+ },`);
1846
+ }
1847
+ return parts.join("\n ");
1848
+ }
1849
+ function generateEnvTsServerSchema(providers) {
1850
+ if (providers.length === 0) {
1851
+ return "";
1852
+ }
1853
+ const schemas = providers.map((providerId) => {
1854
+ const provider = getProvider(providerId);
1855
+ if (!provider) {
1856
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1857
+ }
1858
+ const lines = [];
1859
+ lines.push(`${provider.envPrefix}_CLIENT_ID: z.string(),`);
1860
+ lines.push(`${provider.envPrefix}_CLIENT_SECRET: z.string(),`);
1861
+ if (providerId === "figma") {
1862
+ lines.push(`FIGMA_CLIENT_KEY: z.string(),`);
1863
+ }
1864
+ return lines.join("\n ");
1865
+ }).join("\n ");
1866
+ return schemas;
1867
+ }
1868
+ function generateEnvTsRuntimeMapping(providers) {
1869
+ if (providers.length === 0) {
1870
+ return "";
1871
+ }
1872
+ const mappings = providers.map((providerId) => {
1873
+ const provider = getProvider(providerId);
1874
+ if (!provider) {
1875
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1876
+ }
1877
+ const lines = [];
1878
+ lines.push(`${provider.envPrefix}_CLIENT_ID: process.env.${provider.envPrefix}_CLIENT_ID,`);
1879
+ lines.push(`${provider.envPrefix}_CLIENT_SECRET: process.env.${provider.envPrefix}_CLIENT_SECRET,`);
1880
+ if (providerId === "figma") {
1881
+ lines.push(`FIGMA_CLIENT_KEY: process.env.FIGMA_CLIENT_KEY,`);
1882
+ }
1883
+ return lines.join("\n ");
1884
+ }).join("\n ");
1885
+ return mappings;
1886
+ }
1887
+ function generateOAuthUIProvidersBlock(providers) {
1888
+ if (providers.length === 0) {
1889
+ return "";
1890
+ }
1891
+ const providerList = providers.map((id) => `"${id}"`).join(", ");
1892
+ return `social={{
1893
+ providers: [${providerList}]
1894
+ }}`;
1895
+ }
1896
+ function generateEnvVarsBlock(providers, framework) {
1897
+ if (providers.length === 0) {
1898
+ return "";
1899
+ }
1900
+ const envVars = providers.flatMap((providerId) => {
1901
+ const provider = getProvider(providerId);
1902
+ if (!provider) {
1903
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1904
+ }
1905
+ if (provider.env && provider.env.length > 0) {
1906
+ return provider.env.map((envVar) => {
1907
+ let prefix = "";
1908
+ if (envVar.type === "client") {
1909
+ prefix = framework === "nextjs" ? "NEXT_PUBLIC_" : "VITE_";
1910
+ }
1911
+ return `# ${envVar.description}
1912
+ ${prefix}${envVar.name}=`;
1913
+ });
1914
+ } else {
1915
+ return [
1916
+ `${provider.clientIdVar}=`,
1917
+ `${provider.clientSecretVar}=`
1918
+ ];
1919
+ }
1920
+ });
1921
+ return envVars.join("\n");
1922
+ }
1923
+ function generateReadmeSection(providers) {
1924
+ if (providers.length === 0) {
1925
+ return "";
1926
+ }
1927
+ const sections = providers.map((providerId) => {
1928
+ const provider = getProvider(providerId);
1929
+ if (!provider) {
1930
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
1931
+ }
1932
+ if (!provider.readme) {
1933
+ throw new Error(
1934
+ `Provider ${providerId} missing readme metadata`
1935
+ );
1936
+ }
1937
+ return provider.readme.content;
1938
+ }).filter(Boolean);
1939
+ if (sections.length === 0) {
1940
+ return "";
1941
+ }
1942
+ return `# OAuth Provider Setup
1952
1943
 
1953
- 1. Create a Slack app at https://api.slack.com/apps
1954
- 2. Add OAuth & Permissions and configure redirect URLs
1955
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/slack\` (update for production)
1956
- 4. Copy the Client ID and Client Secret to your \`.env\` file
1944
+ ${sections.join("\n\n---\n\n")}`;
1945
+ }
1946
+ function assertAndReplace(filePath, content, oldBlock, newBlock, label) {
1947
+ if (!content.includes(oldBlock)) {
1948
+ throw new Error(
1949
+ `configureOrganizations: expected pattern "${label}" not found in ${filePath} \u2014 template drift?`
1950
+ );
1951
+ }
1952
+ return content.replace(oldBlock, newBlock);
1953
+ }
1954
+ function stripOrgFromGetAuthCall(filePath, content) {
1955
+ const importPattern = /import \{ TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS \} from "~\/db\/constants";?/;
1956
+ const importMatch = content.match(importPattern);
1957
+ if (!importMatch) {
1958
+ throw new Error(
1959
+ `configureOrganizations: expected the TABLE_SLUG_ORGANIZATIONS db/constants import in ${filePath} \u2014 template drift?`
1960
+ );
1961
+ }
1962
+ let result = assertAndReplace(
1963
+ filePath,
1964
+ content,
1965
+ importMatch[0],
1966
+ importMatch[0].replace("TABLE_SLUG_ORGANIZATIONS, ", ""),
1967
+ "TABLE_SLUG_ORGANIZATIONS db/constants import"
1968
+ );
1969
+ const orgPropPattern = /\n[ \t]*orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,/;
1970
+ const orgPropMatch = result.match(orgPropPattern);
1971
+ if (!orgPropMatch) {
1972
+ throw new Error(
1973
+ `configureOrganizations: expected an "orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS," line in ${filePath} \u2014 template drift?`
1974
+ );
1975
+ }
1976
+ result = assertAndReplace(filePath, result, orgPropMatch[0], "", "orgCollectionSlug prop");
1977
+ result = assertAndReplace(
1978
+ filePath,
1979
+ result,
1980
+ "resolveOrgs: true,",
1981
+ "resolveOrgs: false,",
1982
+ "resolveOrgs: true"
1983
+ );
1984
+ return result;
1985
+ }
1986
+ function stripOrgFromAuthApi(filePath, content) {
1987
+ let result = assertAndReplace(
1988
+ filePath,
1989
+ content,
1990
+ 'import { get } from "@vexcms/core/server";\nimport { v } from "convex/values";\n\nimport {\n type OrganizationID,\n type SessionID,\n TABLE_SLUG_ORGANIZATIONS,\n TABLE_SLUG_SESSIONS,\n TABLE_SLUG_USERS,\n type UserID,\n} from "~/db/constants";',
1991
+ 'import { get } from "@vexcms/core/server";\n\nimport { type SessionID, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, type UserID } from "~/db/constants";',
1992
+ "auth/api.ts imports"
1993
+ );
1994
+ result = assertAndReplace(
1995
+ filePath,
1996
+ result,
1997
+ "export const getUserOrg = query({\n args: {\n noOrgs: v.optional(v.boolean()),\n },\n handler: async (ctx, args) => {\n const empty = { user: null, organization: undefined };\n const identity = await ctx.auth.getUserIdentity();\n if (!identity) {\n return empty;\n }\n const session = await get({\n ctx,\n id: identity.sessionId as SessionID,\n collection: TABLE_SLUG_SESSIONS,\n });\n if (!session) {\n return empty;\n }\n const user = await get({ ctx, id: identity.subject as UserID, collection: TABLE_SLUG_USERS });\n if (!user) {\n return empty;\n }\n if (!session.activeOrganizationId || args.noOrgs) {\n return { user, organization: undefined };\n }\n const organization = await get({\n ctx,\n id: session.activeOrganizationId as OrganizationID,\n collection: TABLE_SLUG_ORGANIZATIONS,\n });\n return { user, organization: organization ?? undefined };\n },\n});",
1998
+ "export const getUserOrg = query({\n args: {},\n handler: async (ctx) => {\n const empty = { user: null, organization: undefined };\n const identity = await ctx.auth.getUserIdentity();\n if (!identity) {\n return empty;\n }\n const session = await get({\n ctx,\n id: identity.sessionId as SessionID,\n collection: TABLE_SLUG_SESSIONS,\n });\n if (!session) {\n return empty;\n }\n const user = await get({ ctx, id: identity.subject as UserID, collection: TABLE_SLUG_USERS });\n if (!user) {\n return empty;\n }\n return { user, organization: undefined };\n },\n});",
1999
+ "auth/api.ts getUserOrg body"
2000
+ );
2001
+ return result;
2002
+ }
2003
+ function stripOrgFromAccess(filePath, content) {
2004
+ let result = assertAndReplace(
2005
+ filePath,
2006
+ content,
2007
+ 'import {\n TABLE_SLUG_ORGANIZATIONS,\n TABLE_SLUG_USERS,\n USER_ROLES,\n} from "~/db/constants"',
2008
+ 'import { TABLE_SLUG_USERS, USER_ROLES } from "~/db/constants"',
2009
+ "access.ts db/constants import"
2010
+ );
2011
+ result = assertAndReplace(
2012
+ filePath,
2013
+ result,
2014
+ "\n orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,",
2015
+ "",
2016
+ "access.ts orgCollectionSlug prop"
2017
+ );
2018
+ return result;
2019
+ }
2020
+ function stripOrgFromAuthContext(filePath, content) {
2021
+ let result = assertAndReplace(
2022
+ filePath,
2023
+ content,
2024
+ 'import { type OrganizationDoc, type UserDoc } from "~/db/constants";',
2025
+ 'import { type UserDoc } from "~/db/constants";',
2026
+ "AuthContext.tsx db/constants import"
2027
+ );
2028
+ result = assertAndReplace(
2029
+ filePath,
2030
+ result,
2031
+ "export interface CurrentAuth {\n organization?: OrganizationDoc;\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n}",
2032
+ "export interface CurrentAuth {\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n}",
2033
+ "AuthContext.tsx CurrentAuth interface"
2034
+ );
2035
+ result = assertAndReplace(
2036
+ filePath,
2037
+ result,
2038
+ " value: {\n organization?: OrganizationDoc;\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n };",
2039
+ " value: {\n user: null | UserDoc; // null \u2192 unauthenticated \u2192 no roles \u2192 deny\n };",
2040
+ "AuthContext.tsx AuthProvider value prop"
2041
+ );
2042
+ result = assertAndReplace(
2043
+ filePath,
2044
+ result,
2045
+ "/** @returns the current caller `{ user, organization }` from the server layout. */",
2046
+ "/** @returns the current caller `{ user }` from the server layout. */",
2047
+ "AuthContext.tsx useAuth JSDoc"
2048
+ );
2049
+ return result;
2050
+ }
2051
+ function stripOrgFromHasPermission(filePath, content) {
2052
+ return assertAndReplace(
2053
+ filePath,
2054
+ content,
2055
+ " const { user, organization } = useAuth();\n return hasPermissionCore({\n access,\n user,\n organization,\n ...props,\n });",
2056
+ " const { user } = useAuth();\n return hasPermissionCore({\n access,\n user,\n ...props,\n });",
2057
+ "hasPermission.ts useAuth destructure + hasPermissionCore call"
2058
+ );
2059
+ }
2060
+ function stripOrgFromDbConstants(filePath, content) {
2061
+ return assertAndReplace(
2062
+ filePath,
2063
+ content,
2064
+ '\nexport const TABLE_SLUG_ORGANIZATIONS = "organization" as const\nexport type OrganizationDoc = Doc<typeof TABLE_SLUG_ORGANIZATIONS>\nexport type OrganizationID = Id<typeof TABLE_SLUG_ORGANIZATIONS>\n',
2065
+ "",
2066
+ "db/constants/index.ts organization table block"
2067
+ );
2068
+ }
1957
2069
 
1958
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2070
+ // src/installers/base.ts
2071
+ var VexFrameworkInstaller = class {
2072
+ /**
2073
+ * Binds this installer to the destination directory and package name used
2074
+ * throughout scaffolding (template copy target, package.json, README, etc).
2075
+ *
2076
+ * @param targetPath - Absolute path of the directory the project is created in.
2077
+ * @param projectName - npm package name recorded for subclasses; not read directly by the base class.
2078
+ */
2079
+ constructor(targetPath, projectName) {
2080
+ this.targetPath = targetPath;
2081
+ this.projectName = projectName;
2082
+ }
2083
+ targetPath;
2084
+ projectName;
2085
+ /**
2086
+ * Copy base template files to target directory
2087
+ */
2088
+ async copyBaseFiles() {
2089
+ await copyTemplate(this.frameworkName, this.targetPath);
2090
+ }
2091
+ /**
2092
+ * Apply a template overlay on top of the base template.
2093
+ * Merges overlay files onto the already-copied base — existing files
2094
+ * not in the overlay are left untouched.
2095
+ *
2096
+ * @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
2097
+ */
2098
+ async applyTemplateOverlay(overlay) {
2099
+ const { dirname: dirname2 } = await import("path");
2100
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
2101
+ const __filename = fileURLToPath2(import.meta.url);
2102
+ const __dirname = dirname2(__filename);
2103
+ let overlayDir = path2.join(__dirname, "../../templates", overlay);
2104
+ if (!await fs5.pathExists(overlayDir)) {
2105
+ overlayDir = path2.join(__dirname, "../templates", overlay);
1959
2106
  }
1960
- },
1961
- tiktok: {
1962
- id: "tiktok",
1963
- name: "TikTok",
1964
- envPrefix: "TIKTOK",
1965
- clientIdVar: "TIKTOK_CLIENT_KEY",
1966
- clientSecretVar: "TIKTOK_CLIENT_SECRET",
1967
- popular: false,
1968
- betterAuthConfig: {
1969
- import: "",
1970
- clientSideProvider: '"tiktok"',
1971
- socialProvider: `tiktok({
1972
- clientKey: process.env.TIKTOK_CLIENT_KEY!,
1973
- clientSecret: process.env.TIKTOK_CLIENT_SECRET!,
1974
- })`,
1975
- scopes: []
1976
- },
1977
- env: [
1978
- {
1979
- name: "TIKTOK_CLIENT_KEY",
1980
- type: "server",
1981
- description: "TikTok Client Key (not Client ID)"
1982
- },
1983
- {
1984
- name: "TIKTOK_CLIENT_SECRET",
1985
- type: "server",
1986
- description: "TikTok Client Secret"
2107
+ await overlayTemplate({ overlayDir, targetDir: this.targetPath });
2108
+ }
2109
+ /**
2110
+ * Update the package.json name field, the vex:update script (to match the
2111
+ * detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
2112
+ *
2113
+ * @param name - npm package name to write into `package.json`'s `name` field
2114
+ * and to substitute for `{{PROJECT_NAME}}` in `README.md`.
2115
+ */
2116
+ async updatePackageName(name) {
2117
+ const pkgPath = path2.join(this.targetPath, "package.json");
2118
+ const pkg = await fs5.readJson(pkgPath);
2119
+ pkg.name = name;
2120
+ const pm = this.detectPackageManager();
2121
+ if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
2122
+ const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
2123
+ pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
2124
+ }
2125
+ await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
2126
+ const readmePath = path2.join(this.targetPath, "README.md");
2127
+ if (await fs5.pathExists(readmePath)) {
2128
+ const readme = await fs5.readFile(readmePath, "utf-8");
2129
+ await fs5.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
2130
+ }
2131
+ }
2132
+ /**
2133
+ * Configure the dev server port.
2134
+ * Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
2135
+ *
2136
+ * @param port - Dev server port number to write into the `dev` script and `.env.local`.
2137
+ */
2138
+ async configurePort(port) {
2139
+ const pkgPath = path2.join(this.targetPath, "package.json");
2140
+ const pkg = await fs5.readJson(pkgPath);
2141
+ if (pkg.scripts?.dev) {
2142
+ if (pkg.scripts.dev.includes("--port")) {
2143
+ pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
2144
+ } else {
2145
+ pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
1987
2146
  }
1988
- ],
1989
- docs: {
1990
- provider: "https://developers.tiktok.com/",
1991
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
1992
- },
1993
- requiresExtraConfig: true,
1994
- extraConfigNotes: "TikTok uses clientKey instead of clientId. Make sure to use the correct field name.",
1995
- readme: {
1996
- title: "TikTok OAuth Setup",
1997
- content: `## TikTok OAuth Setup
1998
-
1999
- 1. Register an app at https://developers.tiktok.com/
2000
- 2. Configure Login Kit
2001
- 3. Add redirect URI: \`http://localhost:3000/api/auth/callback/tiktok\` (update for production)
2002
- 4. Copy the Client Key (not Client ID) and Client Secret to your \`.env\` file
2003
-
2004
- **Important**: TikTok uses clientKey instead of clientId.
2005
-
2006
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2007
2147
  }
2008
- },
2009
- vercel: {
2010
- id: "vercel",
2011
- name: "Vercel",
2012
- envPrefix: "VERCEL",
2013
- clientIdVar: "VERCEL_CLIENT_ID",
2014
- clientSecretVar: "VERCEL_CLIENT_SECRET",
2015
- popular: false,
2016
- betterAuthConfig: {
2017
- import: "",
2018
- clientSideProvider: '"vercel"',
2019
- socialProvider: `vercel({
2020
- clientId: process.env.VERCEL_CLIENT_ID!,
2021
- clientSecret: process.env.VERCEL_CLIENT_SECRET!,
2022
- })`,
2023
- scopes: []
2024
- },
2025
- env: [
2026
- {
2027
- name: "VERCEL_CLIENT_ID",
2028
- type: "server",
2029
- description: "Vercel OAuth Client ID"
2030
- },
2031
- {
2032
- name: "VERCEL_CLIENT_SECRET",
2033
- type: "server",
2034
- description: "Vercel OAuth Client Secret"
2148
+ await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
2149
+ const siteUrl = `http://localhost:${port}`;
2150
+ const envLocalPath = path2.join(this.targetPath, ".env.local");
2151
+ const envContent = [
2152
+ `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
2153
+ `SITE_URL=${siteUrl}`,
2154
+ `BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
2155
+ `NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
2156
+ `NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
2157
+ `CONVEX_DEPLOYMENT=`,
2158
+ ""
2159
+ ].join("\n");
2160
+ await fs5.writeFile(envLocalPath, envContent);
2161
+ }
2162
+ /**
2163
+ * Rewrites the scaffolded `package.json` for life inside a host pnpm
2164
+ * workspace (`--monorepo`). `@vexcms/*` dependencies become
2165
+ * `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
2166
+ * catalog also pins becomes `catalog:`; everything else keeps its
2167
+ * literal, catalog-resolved version. No-op when `options.monorepo` is
2168
+ * false or `options.workspaceRoot` is unset.
2169
+ *
2170
+ * @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
2171
+ */
2172
+ async applyMonorepoRewrite(options) {
2173
+ if (!options.monorepo || !options.workspaceRoot) return;
2174
+ const pkgPath = path2.join(this.targetPath, "package.json");
2175
+ const manifest = await fs5.readJson(pkgPath);
2176
+ const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
2177
+ const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
2178
+ await fs5.writeJson(pkgPath, rewritten, { spaces: 2 });
2179
+ }
2180
+ /**
2181
+ * Detect the package manager used to invoke the CLI
2182
+ *
2183
+ * @returns The package manager inferred from `npm_config_user_agent`, defaulting to `"npm"` when it can't be determined.
2184
+ */
2185
+ detectPackageManager() {
2186
+ const userAgent = process.env.npm_config_user_agent;
2187
+ if (userAgent) {
2188
+ if (userAgent.includes("pnpm")) return "pnpm";
2189
+ if (userAgent.includes("yarn")) return "yarn";
2190
+ if (userAgent.includes("bun")) return "bun";
2191
+ }
2192
+ return "npm";
2193
+ }
2194
+ /**
2195
+ * Install project dependencies using detected package manager
2196
+ */
2197
+ async installDependencies() {
2198
+ const packageManager = this.detectPackageManager();
2199
+ const spinner = ora(`Installing dependencies with ${packageManager}...`).start();
2200
+ try {
2201
+ await execa(packageManager, ["install"], {
2202
+ cwd: this.targetPath,
2203
+ stdio: "pipe"
2204
+ });
2205
+ spinner.succeed("Dependencies installed successfully");
2206
+ } catch (error) {
2207
+ spinner.fail("Failed to install dependencies");
2208
+ throw new Error(
2209
+ `Dependency installation failed: ${error instanceof Error ? error.message : "Unknown error"}`
2210
+ );
2211
+ }
2212
+ }
2213
+ /**
2214
+ * Format code using project's formatter
2215
+ */
2216
+ async formatCode() {
2217
+ const packageManager = this.detectPackageManager();
2218
+ const spinner = ora("Formatting code...").start();
2219
+ try {
2220
+ const result = await execa(packageManager, ["run", "format"], {
2221
+ cwd: this.targetPath,
2222
+ stdio: "pipe",
2223
+ reject: false
2224
+ });
2225
+ if (result.exitCode === 0) {
2226
+ spinner.succeed("Code formatted successfully");
2227
+ } else {
2228
+ spinner.warn("Formatting completed with warnings");
2035
2229
  }
2036
- ],
2037
- docs: {
2038
- provider: "https://vercel.com/account/integrations",
2039
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2040
- },
2041
- requiresExtraConfig: false,
2042
- extraConfigNotes: "Uses PKCE for enhanced security.",
2043
- readme: {
2044
- title: "Vercel OAuth Setup",
2045
- content: `## Vercel OAuth Setup
2046
-
2047
- 1. Create an integration at https://vercel.com/account/integrations
2048
- 2. Configure OAuth settings
2049
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/vercel\` (update for production)
2050
- 4. Copy the Client ID and Client Secret to your \`.env\` file
2051
-
2052
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2230
+ } catch (error) {
2231
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2232
+ spinner.warn(`Failed to format code: ${errorMessage}`);
2053
2233
  }
2054
- },
2055
- vk: {
2056
- id: "vk",
2057
- name: "VK",
2058
- envPrefix: "VK",
2059
- clientIdVar: "VK_CLIENT_ID",
2060
- clientSecretVar: "VK_CLIENT_SECRET",
2061
- popular: false,
2062
- betterAuthConfig: {
2063
- import: "",
2064
- clientSideProvider: '"vk"',
2065
- socialProvider: `vk({
2066
- clientId: process.env.VK_CLIENT_ID!,
2067
- clientSecret: process.env.VK_CLIENT_SECRET!,
2068
- })`,
2069
- scopes: []
2070
- },
2071
- env: [
2072
- {
2073
- name: "VK_CLIENT_ID",
2074
- type: "server",
2075
- description: "VK Application ID"
2076
- },
2077
- {
2078
- name: "VK_CLIENT_SECRET",
2079
- type: "server",
2080
- description: "VK Secure Key"
2234
+ }
2235
+ /**
2236
+ * Lint and fix code using project's ESLint configuration
2237
+ */
2238
+ async lintCode() {
2239
+ const packageManager = this.detectPackageManager();
2240
+ const spinner = ora("Linting and fixing code...").start();
2241
+ try {
2242
+ const args2 = packageManager === "pnpm" ? ["lint", "--fix"] : ["run", "lint", "--", "--fix"];
2243
+ const result = await execa(packageManager, args2, {
2244
+ cwd: this.targetPath,
2245
+ stdio: "pipe",
2246
+ reject: false
2247
+ });
2248
+ if (result.exitCode === 0) {
2249
+ spinner.succeed("Code linted and fixed successfully");
2250
+ } else {
2251
+ spinner.warn("Linting completed with warnings");
2081
2252
  }
2082
- ],
2083
- docs: {
2084
- provider: "https://vk.com/apps?act=manage",
2085
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2086
- },
2087
- requiresExtraConfig: false,
2088
- extraConfigNotes: "",
2089
- readme: {
2090
- title: "VK OAuth Setup",
2091
- content: `## VK OAuth Setup
2092
-
2093
- 1. Create an app at https://vk.com/apps?act=manage
2094
- 2. Configure OAuth settings in the app
2095
- 3. Add authorized redirect URI: \`http://localhost:3000/api/auth/callback/vk\` (update for production)
2096
- 4. Copy the Application ID and Secure Key to your \`.env\` file
2097
-
2098
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2253
+ } catch (error) {
2254
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2255
+ spinner.warn(`Failed to lint code: ${errorMessage}`);
2099
2256
  }
2100
- },
2101
- zoom: {
2102
- id: "zoom",
2103
- name: "Zoom",
2104
- envPrefix: "ZOOM",
2105
- clientIdVar: "ZOOM_CLIENT_ID",
2106
- clientSecretVar: "ZOOM_CLIENT_SECRET",
2107
- popular: false,
2108
- betterAuthConfig: {
2109
- import: "",
2110
- clientSideProvider: '"zoom"',
2111
- socialProvider: `zoom({
2112
- clientId: process.env.ZOOM_CLIENT_ID!,
2113
- clientSecret: process.env.ZOOM_CLIENT_SECRET!,
2114
- })`,
2115
- scopes: ["user:read:user"]
2116
- },
2117
- env: [
2118
- {
2119
- name: "ZOOM_CLIENT_ID",
2120
- type: "server",
2121
- description: "Zoom OAuth Client ID"
2122
- },
2123
- {
2124
- name: "ZOOM_CLIENT_SECRET",
2125
- type: "server",
2126
- description: "Zoom OAuth Client Secret"
2257
+ }
2258
+ /**
2259
+ * Initialize Git repository in target directory
2260
+ */
2261
+ async initGitRepo() {
2262
+ const spinner = ora("Initializing Git repository...").start();
2263
+ try {
2264
+ try {
2265
+ await execa("git", ["--version"], { stdio: "pipe" });
2266
+ } catch {
2267
+ spinner.fail("Git is not installed");
2268
+ throw new Error("Git is not installed. Please install Git to initialize a repository.");
2127
2269
  }
2128
- ],
2129
- docs: {
2130
- provider: "https://marketplace.zoom.us/",
2131
- betterAuth: "https://www.better-auth.com/docs/authentication/social"
2132
- },
2133
- requiresExtraConfig: true,
2134
- extraConfigNotes: "You MUST include the user:read:user scope for Zoom.",
2135
- readme: {
2136
- title: "Zoom OAuth Setup",
2137
- content: `## Zoom OAuth Setup
2138
-
2139
- 1. Create an app at https://marketplace.zoom.us/
2140
- 2. Choose OAuth as the app type
2141
- 3. Add redirect URL: \`http://localhost:3000/api/auth/callback/zoom\` (update for production)
2142
- 4. Copy the Client ID and Client Secret to your \`.env\` file
2143
- 5. **Important**: Make sure to include the \`user:read:user\` scope
2144
-
2145
- For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
2270
+ await execa("git", ["init"], { cwd: this.targetPath, stdio: "pipe" });
2271
+ await execa("git", ["add", "."], { cwd: this.targetPath, stdio: "pipe" });
2272
+ await execa(
2273
+ "git",
2274
+ ["commit", "-m", "Initial commit from create-vexcms"],
2275
+ { cwd: this.targetPath, stdio: "pipe" }
2276
+ );
2277
+ spinner.succeed("Git repository initialized");
2278
+ } catch (error) {
2279
+ spinner.fail("Failed to initialize Git repository");
2280
+ throw new Error(
2281
+ `Git initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
2282
+ );
2146
2283
  }
2147
2284
  }
2148
- };
2149
- function getProvider(id) {
2150
- return OAUTH_PROVIDERS[id];
2151
- }
2152
- function getPopularProviders() {
2153
- return Object.values(OAUTH_PROVIDERS).filter(
2154
- (provider) => provider.popular === true
2155
- );
2156
- }
2157
- function getAdditionalProviders() {
2158
- return Object.values(OAUTH_PROVIDERS).filter(
2159
- (provider) => provider.popular !== true
2160
- );
2161
- }
2162
-
2163
- // src/installers/string-utils.ts
2164
- function detectIndentation(line) {
2165
- const match = line.match(/^(\s*)/);
2166
- return match ? match[1] : "";
2167
- }
2168
- async function replacePlaceholder(filePath, placeholder, content, options) {
2169
- const fileContent = await fs5.readFile(filePath, "utf-8");
2170
- if (!fileContent.includes(placeholder)) {
2171
- if (options?.graceful) {
2172
- console.warn(
2173
- `Warning: Placeholder "${placeholder}" not found in file: ${filePath}. Skipping replacement.`
2285
+ /**
2286
+ * Generate a secure random secret for Better Auth
2287
+ *
2288
+ * @returns A 64-character hex string (32 random bytes) suitable for `BETTER_AUTH_SECRET`.
2289
+ */
2290
+ generateAuthSecret() {
2291
+ return crypto.randomBytes(32).toString("hex");
2292
+ }
2293
+ /**
2294
+ * Write the generated auth secret to .env.example
2295
+ */
2296
+ async writeAuthSecret() {
2297
+ const envPath = path2.join(this.targetPath, ".env.example");
2298
+ const secret = this.generateAuthSecret();
2299
+ if (await fs5.pathExists(envPath)) {
2300
+ let content = await fs5.readFile(envPath, "utf-8");
2301
+ content = content.replace(
2302
+ /BETTER_AUTH_SECRET=.*/,
2303
+ `BETTER_AUTH_SECRET=${secret}`
2174
2304
  );
2175
- return;
2305
+ await fs5.writeFile(envPath, content);
2306
+ }
2307
+ }
2308
+ /**
2309
+ * Configure the Better Auth organizations plugin.
2310
+ * When enabled, replaces placeholders with organization import and plugin,
2311
+ * and leaves every org-aware file (`createGetAuth` call sites, `access.ts`,
2312
+ * `AuthContext.tsx`, `hasPermission.ts`, `convex/auth/api.ts`,
2313
+ * `db/constants/index.ts`) untouched.
2314
+ * When disabled, removes the placeholder lines AND strips organization
2315
+ * wiring from those same files down to the org-free shape — the
2316
+ * templates ship org-enabled by default (so `--orgs` needs no surgery),
2317
+ * so the default (orgs-declined) path has to actively cut it.
2318
+ *
2319
+ * @param enabled - Whether the organizations (multi-tenant) plugin should stay wired.
2320
+ */
2321
+ async configureOrganizations(enabled) {
2322
+ const pluginsPath = path2.join(this.targetPath, "convex/auth/plugins/index.ts");
2323
+ if (await fs5.pathExists(pluginsPath)) {
2324
+ let content = await fs5.readFile(pluginsPath, "utf-8");
2325
+ if (enabled) {
2326
+ content = content.replace(
2327
+ "// {{ORGANIZATIONS_IMPORT}}",
2328
+ 'import { organization } from "better-auth/plugins"'
2329
+ );
2330
+ content = content.replace(
2331
+ " // {{ORGANIZATIONS_PLUGIN}}",
2332
+ " organization(),"
2333
+ );
2334
+ } else {
2335
+ content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
2336
+ content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
2337
+ }
2338
+ await fs5.writeFile(pluginsPath, content);
2339
+ }
2340
+ if (!enabled) {
2341
+ await this.stripOrganizationWiring();
2176
2342
  }
2177
- throw new Error(
2178
- `Placeholder "${placeholder}" not found in file: ${filePath}`
2179
- );
2180
- }
2181
- if (options?.inline) {
2182
- const updatedContent = fileContent.replace(placeholder, content);
2183
- await fs5.writeFile(filePath, updatedContent, "utf-8");
2184
- return;
2185
2343
  }
2186
- const lines = fileContent.split("\n");
2187
- const updatedLines = [];
2188
- for (const line of lines) {
2189
- if (line.includes(placeholder)) {
2190
- if (content === "" || content.startsWith("__REMOVE_")) {
2191
- continue;
2344
+ /**
2345
+ * Rewrites every scaffolded file that references organizations down to
2346
+ * its org-free shape. Called by `configureOrganizations(false)` — the
2347
+ * `--orgs`-declined default. Templates ship org-ENABLED (so `--orgs`
2348
+ * requires no surgery); this is what makes the default scaffold buildable
2349
+ * once the generated registry drops org tables on the first `vex dev`
2350
+ * regen. Every transform throws loudly when its expected pattern is
2351
+ * missing (template drift), rather than silently no-opping.
2352
+ *
2353
+ * `src/vexcms/api.ts` only exists in full (marketing-site overlay)
2354
+ * scaffolds — skipped, not required, for `--bare`.
2355
+ */
2356
+ async stripOrganizationWiring() {
2357
+ const requiredFiles = [
2358
+ { rel: "convex/vex.ts", transform: stripOrgFromGetAuthCall },
2359
+ { rel: "convex/vex/globals.ts", transform: stripOrgFromGetAuthCall },
2360
+ { rel: "convex/vex/media.ts", transform: stripOrgFromGetAuthCall },
2361
+ { rel: "convex/auth/api.ts", transform: stripOrgFromAuthApi },
2362
+ { rel: "src/auth/access.ts", transform: stripOrgFromAccess },
2363
+ { rel: "src/context/AuthContext.tsx", transform: stripOrgFromAuthContext },
2364
+ { rel: "src/auth/hasPermission.ts", transform: stripOrgFromHasPermission },
2365
+ { rel: "src/db/constants/index.ts", transform: stripOrgFromDbConstants }
2366
+ ];
2367
+ for (const file of requiredFiles) {
2368
+ const filePath = path2.join(this.targetPath, file.rel);
2369
+ if (!await fs5.pathExists(filePath)) {
2370
+ throw new Error(
2371
+ `configureOrganizations: required file "${file.rel}" is missing from the scaffold \u2014 template drift?`
2372
+ );
2192
2373
  }
2193
- const indentation = detectIndentation(line);
2194
- const indentedContent = content.split("\n").map((contentLine, index) => {
2195
- if (index === 0) {
2196
- return indentation + contentLine;
2197
- }
2198
- return contentLine ? indentation + contentLine : "";
2199
- }).join("\n");
2200
- updatedLines.push(indentedContent);
2201
- } else {
2202
- updatedLines.push(line);
2374
+ const content = await fs5.readFile(filePath, "utf-8");
2375
+ await fs5.writeFile(filePath, file.transform(filePath, content));
2376
+ }
2377
+ const apiPath = path2.join(this.targetPath, "src/vexcms/api.ts");
2378
+ if (await fs5.pathExists(apiPath)) {
2379
+ const content = await fs5.readFile(apiPath, "utf-8");
2380
+ await fs5.writeFile(apiPath, stripOrgFromGetAuthCall(apiPath, content));
2203
2381
  }
2204
2382
  }
2205
- await fs5.writeFile(filePath, updatedLines.join("\n"), "utf-8");
2206
- }
2207
- function generateCredentialsValue(enabled) {
2208
- return `credentials={${enabled}}`;
2209
- }
2210
- function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
2211
- const parts = [];
2212
- parts.push(`emailAndPassword: {
2213
- enabled: ${emailPasswordEnabled}
2214
- },`);
2215
- if (oauthProviders.length > 0) {
2216
- const providersObject = oauthProviders.map((providerId) => {
2217
- const provider = getProvider(providerId);
2218
- if (!provider) {
2219
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2220
- }
2221
- const configLines = [
2222
- `clientId: process.env.${provider.envPrefix}_CLIENT_ID!,`,
2223
- `clientSecret: process.env.${provider.envPrefix}_CLIENT_SECRET!,`
2224
- ];
2225
- if (providerId === "figma") {
2226
- configLines.push(`clientKey: process.env.FIGMA_CLIENT_KEY!,`);
2383
+ /**
2384
+ * Main orchestration method for project initialization
2385
+ *
2386
+ * @param options - Resolved project options from CLI flags and prompts, driving every scaffolding step (template copy, overlay, port, auth, README, git, install).
2387
+ */
2388
+ async initProject(options) {
2389
+ const copySpinner = ora("Copying template files...").start();
2390
+ try {
2391
+ await this.copyBaseFiles();
2392
+ copySpinner.succeed("Template files copied");
2393
+ } catch (error) {
2394
+ copySpinner.fail("Failed to copy template files");
2395
+ throw error;
2396
+ }
2397
+ if (!options.bare) {
2398
+ const overlaySpinner = ora("Applying marketing site template...").start();
2399
+ try {
2400
+ await this.applyTemplateOverlay("marketing-site");
2401
+ overlaySpinner.succeed("Marketing site template applied");
2402
+ } catch (error) {
2403
+ overlaySpinner.fail("Failed to apply template overlay");
2404
+ throw error;
2227
2405
  }
2228
- return `${providerId}: {
2229
- ${configLines.join("\n ")}
2230
- }`;
2231
- }).join(",\n ");
2232
- parts.push(`socialProviders: {
2233
- ${providersObject}
2234
- },`);
2235
- }
2236
- return parts.join("\n ");
2237
- }
2238
- function generateEnvTsServerSchema(providers) {
2239
- if (providers.length === 0) {
2240
- return "";
2241
- }
2242
- const schemas = providers.map((providerId) => {
2243
- const provider = getProvider(providerId);
2244
- if (!provider) {
2245
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2246
2406
  }
2247
- const lines = [];
2248
- lines.push(`${provider.envPrefix}_CLIENT_ID: z.string(),`);
2249
- lines.push(`${provider.envPrefix}_CLIENT_SECRET: z.string(),`);
2250
- if (providerId === "figma") {
2251
- lines.push(`FIGMA_CLIENT_KEY: z.string(),`);
2407
+ const nameSpinner = ora("Configuring project...").start();
2408
+ try {
2409
+ await this.updatePackageName(options.projectName);
2410
+ await this.configurePort(options.port);
2411
+ nameSpinner.succeed("Project configured");
2412
+ } catch (error) {
2413
+ nameSpinner.fail("Failed to configure project");
2414
+ throw error;
2252
2415
  }
2253
- return lines.join("\n ");
2254
- }).join("\n ");
2255
- return schemas;
2256
- }
2257
- function generateEnvTsRuntimeMapping(providers) {
2258
- if (providers.length === 0) {
2259
- return "";
2260
- }
2261
- const mappings = providers.map((providerId) => {
2262
- const provider = getProvider(providerId);
2263
- if (!provider) {
2264
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2416
+ const authSpinner = ora("Configuring authentication...").start();
2417
+ try {
2418
+ await this.updateOAuthConfig(options.oauthProviders, options.emailPasswordAuth);
2419
+ authSpinner.succeed(
2420
+ options.emailPasswordAuth || options.oauthProviders.length > 0 ? "Authentication configuration updated" : "Authentication placeholders cleaned up"
2421
+ );
2422
+ } catch (error) {
2423
+ authSpinner.fail("Failed to configure authentication");
2424
+ throw error;
2265
2425
  }
2266
- const lines = [];
2267
- lines.push(`${provider.envPrefix}_CLIENT_ID: process.env.${provider.envPrefix}_CLIENT_ID,`);
2268
- lines.push(`${provider.envPrefix}_CLIENT_SECRET: process.env.${provider.envPrefix}_CLIENT_SECRET,`);
2269
- if (providerId === "figma") {
2270
- lines.push(`FIGMA_CLIENT_KEY: process.env.FIGMA_CLIENT_KEY,`);
2426
+ const oauthUISpinner = ora("Configuring OAuth UI...").start();
2427
+ try {
2428
+ await this.updateOAuthUIConfig(options.oauthProviders, options.emailPasswordAuth);
2429
+ oauthUISpinner.succeed(
2430
+ options.oauthProviders.length > 0 ? "OAuth UI configuration updated" : "OAuth UI placeholders cleaned up"
2431
+ );
2432
+ } catch (error) {
2433
+ oauthUISpinner.fail("Failed to configure OAuth UI");
2434
+ throw error;
2271
2435
  }
2272
- return lines.join("\n ");
2273
- }).join("\n ");
2274
- return mappings;
2275
- }
2276
- function generateOAuthUIProvidersBlock(providers) {
2277
- if (providers.length === 0) {
2278
- return "";
2279
- }
2280
- const providerList = providers.map((id) => `"${id}"`).join(", ");
2281
- return `social={{
2282
- providers: [${providerList}]
2283
- }}`;
2284
- }
2285
- function generateEnvVarsBlock(providers, framework) {
2286
- if (providers.length === 0) {
2287
- return "";
2288
- }
2289
- const envVars = providers.flatMap((providerId) => {
2290
- const provider = getProvider(providerId);
2291
- if (!provider) {
2292
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2436
+ const envSpinner = ora("Updating .env.example...").start();
2437
+ try {
2438
+ await this.updateEnvExample(options.oauthProviders);
2439
+ envSpinner.succeed(
2440
+ options.oauthProviders.length > 0 ? ".env.example updated" : ".env.example placeholders cleaned up"
2441
+ );
2442
+ } catch (error) {
2443
+ envSpinner.fail("Failed to update .env.example");
2444
+ throw error;
2293
2445
  }
2294
- if (provider.env && provider.env.length > 0) {
2295
- return provider.env.map((envVar) => {
2296
- let prefix = "";
2297
- if (envVar.type === "client") {
2298
- prefix = framework === "nextjs" ? "NEXT_PUBLIC_" : "VITE_";
2299
- }
2300
- return `# ${envVar.description}
2301
- ${prefix}${envVar.name}=`;
2302
- });
2303
- } else {
2304
- return [
2305
- `${provider.clientIdVar}=`,
2306
- `${provider.clientSecretVar}=`
2307
- ];
2446
+ const envTsSpinner = ora("Updating typed env configuration...").start();
2447
+ try {
2448
+ await this.updateEnvTs(options.oauthProviders);
2449
+ envTsSpinner.succeed(
2450
+ options.oauthProviders.length > 0 ? "Typed env configuration updated" : "Typed env placeholders cleaned up"
2451
+ );
2452
+ } catch (error) {
2453
+ envTsSpinner.fail("Failed to update typed env configuration");
2454
+ throw error;
2308
2455
  }
2309
- });
2310
- return envVars.join("\n");
2311
- }
2312
- function generateReadmeSection(providers) {
2313
- if (providers.length === 0) {
2314
- return "";
2315
- }
2316
- const sections = providers.map((providerId) => {
2317
- const provider = getProvider(providerId);
2318
- if (!provider) {
2319
- throw new Error(`Unknown OAuth provider: ${providerId}`);
2456
+ const readmeSpinner = ora("Updating README...").start();
2457
+ try {
2458
+ await this.updateReadme(options.oauthProviders);
2459
+ readmeSpinner.succeed(
2460
+ options.oauthProviders.length > 0 ? "README updated" : "README placeholders cleaned up"
2461
+ );
2462
+ } catch (error) {
2463
+ readmeSpinner.fail("Failed to update README");
2464
+ throw error;
2320
2465
  }
2321
- if (!provider.readme) {
2322
- throw new Error(
2323
- `Provider ${providerId} missing readme metadata`
2466
+ const orgsSpinner = ora("Configuring organizations...").start();
2467
+ try {
2468
+ await this.configureOrganizations(options.orgs);
2469
+ orgsSpinner.succeed(
2470
+ options.orgs ? "Organizations plugin enabled" : "Organizations placeholders cleaned up"
2324
2471
  );
2472
+ } catch (error) {
2473
+ orgsSpinner.fail("Failed to configure organizations");
2474
+ throw error;
2475
+ }
2476
+ const secretSpinner = ora("Generating auth secret...").start();
2477
+ try {
2478
+ await this.writeAuthSecret();
2479
+ secretSpinner.succeed("Auth secret generated");
2480
+ } catch (error) {
2481
+ secretSpinner.fail("Failed to generate auth secret");
2482
+ throw error;
2483
+ }
2484
+ if (options.monorepo) {
2485
+ const monorepoSpinner = ora("Rewriting dependencies for the host workspace...").start();
2486
+ try {
2487
+ await this.applyMonorepoRewrite(options);
2488
+ monorepoSpinner.succeed("Dependencies rewritten for the host workspace");
2489
+ } catch (error) {
2490
+ monorepoSpinner.fail("Failed to rewrite dependencies for the host workspace");
2491
+ throw error;
2492
+ }
2493
+ }
2494
+ if (options.initGit && !options.monorepo) {
2495
+ await this.initGitRepo();
2496
+ }
2497
+ if (options.installDependencies && !options.monorepo) {
2498
+ await this.installDependencies();
2499
+ await this.lintCode();
2500
+ await this.formatCode();
2325
2501
  }
2326
- return provider.readme.content;
2327
- }).filter(Boolean);
2328
- if (sections.length === 0) {
2329
- return "";
2330
2502
  }
2331
- return `# OAuth Provider Setup
2332
-
2333
- ${sections.join("\n\n---\n\n")}`;
2334
- }
2503
+ };
2335
2504
 
2336
2505
  // src/installers/nextjs.ts
2506
+ import { join as join2 } from "path";
2337
2507
  var VexNextJSInstaller = class extends VexFrameworkInstaller {
2338
2508
  /**
2339
2509
  * Framework identifier used for template selection and logging.