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.
- package/README.md +11 -3
- package/dist/index.js +1183 -1013
- package/package.json +1 -1
- package/templates/base-nextjs/README.md +11 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/next.config.ts +7 -0
- package/templates/base-nextjs/package.json +6 -7
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/seed.ts +247 -6
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +37 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/layout.tsx +58 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +51 -0
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -44
- package/templates/marketing-site/src/vexcms/collections/pages.ts +3 -0
- /package/templates/marketing-site/src/app/(frontend)/{PageContent.tsx → (site)/PageContent.tsx} +0 -0
- /package/templates/marketing-site/src/app/(frontend)/{[slug] → (site)/[slug]}/page.tsx +0 -0
- /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
|
|
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/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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: "
|
|
1686
|
+
description: "VK Application ID"
|
|
1835
1687
|
},
|
|
1836
1688
|
{
|
|
1837
|
-
name: "
|
|
1689
|
+
name: "VK_CLIENT_SECRET",
|
|
1838
1690
|
type: "server",
|
|
1839
|
-
description: "
|
|
1691
|
+
description: "VK Secure Key"
|
|
1840
1692
|
}
|
|
1841
1693
|
],
|
|
1842
1694
|
docs: {
|
|
1843
|
-
provider: "https://
|
|
1695
|
+
provider: "https://vk.com/apps?act=manage",
|
|
1844
1696
|
betterAuth: "https://www.better-auth.com/docs/authentication/social"
|
|
1845
1697
|
},
|
|
1846
|
-
requiresExtraConfig:
|
|
1847
|
-
extraConfigNotes: "
|
|
1698
|
+
requiresExtraConfig: false,
|
|
1699
|
+
extraConfigNotes: "",
|
|
1848
1700
|
readme: {
|
|
1849
|
-
title: "
|
|
1850
|
-
content: `##
|
|
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
|
-
|
|
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
|
-
|
|
1863
|
-
id: "
|
|
1864
|
-
name: "
|
|
1865
|
-
envPrefix: "
|
|
1866
|
-
clientIdVar: "
|
|
1867
|
-
clientSecretVar: "
|
|
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: '"
|
|
1872
|
-
socialProvider: `
|
|
1873
|
-
clientId: process.env.
|
|
1874
|
-
clientSecret: process.env.
|
|
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: "
|
|
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: "
|
|
1732
|
+
description: "Zoom OAuth Client ID"
|
|
1889
1733
|
},
|
|
1890
1734
|
{
|
|
1891
|
-
name: "
|
|
1735
|
+
name: "ZOOM_CLIENT_SECRET",
|
|
1892
1736
|
type: "server",
|
|
1893
|
-
description:
|
|
1737
|
+
description: "Zoom OAuth Client Secret"
|
|
1894
1738
|
}
|
|
1895
1739
|
],
|
|
1896
1740
|
docs: {
|
|
1897
|
-
provider: "https://
|
|
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:
|
|
1745
|
+
extraConfigNotes: "You MUST include the user:read:user scope for Zoom.",
|
|
1902
1746
|
readme: {
|
|
1903
|
-
title: "
|
|
1904
|
-
content: `##
|
|
1747
|
+
title: "Zoom OAuth Setup",
|
|
1748
|
+
content: `## Zoom OAuth Setup
|
|
1905
1749
|
|
|
1906
|
-
1. Create
|
|
1907
|
-
2.
|
|
1908
|
-
3. Add
|
|
1909
|
-
4. Copy the
|
|
1910
|
-
5.
|
|
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
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
popular
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
},
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
2038
|
-
|
|
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
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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
|
-
|
|
2084
|
-
|
|
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
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
(
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
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
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
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
|
-
|
|
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.
|