create-vexcms 0.0.20 → 0.1.0-alpha.3
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/LICENSE +202 -0
- package/README.md +98 -80
- package/dist/index.js +974 -639
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +59 -203
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +46 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- 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/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +12 -55
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- package/templates/marketing-site/vex.config.ts +0 -32
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk2 from "chalk";
|
|
6
6
|
import { input, select, confirm, checkbox } from "@inquirer/prompts";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path3 from "path";
|
|
8
|
+
import fs6 from "fs-extra";
|
|
9
9
|
|
|
10
10
|
// src/utils/validation.ts
|
|
11
11
|
import validateNpmPackageName from "validate-npm-package-name";
|
|
@@ -32,11 +32,11 @@ async function isDirectoryEmpty(dirPath) {
|
|
|
32
32
|
const files = await fs.readdir(dirPath);
|
|
33
33
|
const visibleFiles = files.filter((file) => !file.startsWith("."));
|
|
34
34
|
return visibleFiles.length === 0;
|
|
35
|
-
} catch
|
|
35
|
+
} catch {
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
function resolveProjectName(input2,
|
|
39
|
+
function resolveProjectName(input2, _cwd) {
|
|
40
40
|
return input2;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -75,15 +75,60 @@ function displaySuccessMessage(projectName, targetPath, isCurrentDir) {
|
|
|
75
75
|
console.log();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// src/helpers/monorepo.ts
|
|
79
|
+
import fs2 from "fs-extra";
|
|
80
|
+
import path from "path";
|
|
81
|
+
import { parse as parseYaml } from "yaml";
|
|
82
|
+
import sortPackageJson from "sort-package-json";
|
|
83
|
+
async function findWorkspaceRoot(props) {
|
|
84
|
+
const { cwd } = props;
|
|
85
|
+
let dir = path.resolve(cwd);
|
|
86
|
+
while (true) {
|
|
87
|
+
if (await fs2.pathExists(path.join(dir, "pnpm-workspace.yaml"))) {
|
|
88
|
+
return dir;
|
|
89
|
+
}
|
|
90
|
+
const parent = path.dirname(dir);
|
|
91
|
+
if (parent === dir) return null;
|
|
92
|
+
dir = parent;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readWorkspaceCatalog(props) {
|
|
96
|
+
const { workspaceRoot } = props;
|
|
97
|
+
const raw = await fs2.readFile(path.join(workspaceRoot, "pnpm-workspace.yaml"), "utf-8");
|
|
98
|
+
const parsed = parseYaml(raw);
|
|
99
|
+
return parsed.catalog ?? {};
|
|
100
|
+
}
|
|
101
|
+
var MONOREPO_DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
|
|
102
|
+
function rewriteManifestForMonorepo(props) {
|
|
103
|
+
const { manifest, catalog } = props;
|
|
104
|
+
const rewritten = { ...manifest };
|
|
105
|
+
for (const field of MONOREPO_DEPENDENCY_FIELDS) {
|
|
106
|
+
const deps = manifest[field];
|
|
107
|
+
if (!deps) continue;
|
|
108
|
+
const rewrittenDeps = {};
|
|
109
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
110
|
+
if (name.startsWith("@vexcms/")) {
|
|
111
|
+
rewrittenDeps[name] = "workspace:*";
|
|
112
|
+
} else if (name in catalog) {
|
|
113
|
+
rewrittenDeps[name] = "catalog:";
|
|
114
|
+
} else {
|
|
115
|
+
rewrittenDeps[name] = version;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
rewritten[field] = rewrittenDeps;
|
|
119
|
+
}
|
|
120
|
+
return sortPackageJson(rewritten);
|
|
121
|
+
}
|
|
122
|
+
|
|
78
123
|
// src/installers/base.ts
|
|
79
|
-
import
|
|
124
|
+
import fs5 from "fs-extra";
|
|
80
125
|
import { execa } from "execa";
|
|
81
126
|
import ora from "ora";
|
|
82
127
|
import crypto from "crypto";
|
|
83
|
-
import
|
|
128
|
+
import path2 from "path";
|
|
84
129
|
|
|
85
130
|
// src/helpers/fileOperations.ts
|
|
86
|
-
import
|
|
131
|
+
import fs3 from "fs-extra";
|
|
87
132
|
import { join, dirname } from "path";
|
|
88
133
|
import { fileURLToPath } from "url";
|
|
89
134
|
async function copyTemplate(framework, targetPath) {
|
|
@@ -91,10 +136,10 @@ async function copyTemplate(framework, targetPath) {
|
|
|
91
136
|
const __dirname = dirname(__filename);
|
|
92
137
|
const templateDir = framework === "tanstack" ? "base-tanstack" : "base-nextjs";
|
|
93
138
|
let templatePath = join(__dirname, "../../templates", templateDir);
|
|
94
|
-
if (!
|
|
139
|
+
if (!fs3.existsSync(templatePath)) {
|
|
95
140
|
templatePath = join(__dirname, "../templates", templateDir);
|
|
96
141
|
}
|
|
97
|
-
await
|
|
142
|
+
await fs3.copy(templatePath, targetPath, {
|
|
98
143
|
overwrite: false,
|
|
99
144
|
errorOnExist: false,
|
|
100
145
|
filter: (src) => {
|
|
@@ -105,8 +150,8 @@ async function copyTemplate(framework, targetPath) {
|
|
|
105
150
|
});
|
|
106
151
|
const sourceGitignore = join(templatePath, "_gitignore");
|
|
107
152
|
const targetGitignore = join(targetPath, ".gitignore");
|
|
108
|
-
if (await
|
|
109
|
-
await
|
|
153
|
+
if (await fs3.pathExists(sourceGitignore)) {
|
|
154
|
+
await fs3.copy(sourceGitignore, targetGitignore, {
|
|
110
155
|
overwrite: false,
|
|
111
156
|
errorOnExist: false
|
|
112
157
|
});
|
|
@@ -119,521 +164,196 @@ async function copyTemplate(framework, targetPath) {
|
|
|
119
164
|
for (const [from, to] of dotfileRenames) {
|
|
120
165
|
const source = join(templatePath, from);
|
|
121
166
|
const target = join(targetPath, to);
|
|
122
|
-
if (await
|
|
123
|
-
await
|
|
167
|
+
if (await fs3.pathExists(source)) {
|
|
168
|
+
await fs3.copy(source, target, { overwrite: false, errorOnExist: false });
|
|
124
169
|
}
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
async function overlayTemplate({ overlayDir, targetDir }) {
|
|
128
|
-
await
|
|
173
|
+
await fs3.copy(overlayDir, targetDir, { overwrite: true });
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
// src/installers/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const pkg = await fs3.readJson(pkgPath);
|
|
166
|
-
pkg.name = name;
|
|
167
|
-
const pm = this.detectPackageManager();
|
|
168
|
-
if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
|
|
169
|
-
const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
|
|
170
|
-
pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
|
|
171
|
-
}
|
|
172
|
-
await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Configure the dev server port.
|
|
176
|
-
* Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
|
|
177
|
-
*/
|
|
178
|
-
async configurePort(port) {
|
|
179
|
-
const pkgPath = path.join(this.targetPath, "package.json");
|
|
180
|
-
const pkg = await fs3.readJson(pkgPath);
|
|
181
|
-
if (pkg.scripts?.dev) {
|
|
182
|
-
if (pkg.scripts.dev.includes("--port")) {
|
|
183
|
-
pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
|
|
184
|
-
} else {
|
|
185
|
-
pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
|
|
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"
|
|
186
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).`
|
|
187
227
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
cwd: this.targetPath,
|
|
216
|
-
stdio: "pipe"
|
|
217
|
-
});
|
|
218
|
-
spinner.succeed("Dependencies installed successfully");
|
|
219
|
-
} catch (error) {
|
|
220
|
-
spinner.fail("Failed to install dependencies");
|
|
221
|
-
throw new Error(
|
|
222
|
-
`Dependency installation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Format code using project's formatter
|
|
228
|
-
*/
|
|
229
|
-
async formatCode() {
|
|
230
|
-
const packageManager = this.detectPackageManager();
|
|
231
|
-
const spinner = ora("Formatting code...").start();
|
|
232
|
-
try {
|
|
233
|
-
const result = await execa(packageManager, ["run", "format"], {
|
|
234
|
-
cwd: this.targetPath,
|
|
235
|
-
stdio: "pipe",
|
|
236
|
-
reject: false
|
|
237
|
-
});
|
|
238
|
-
if (result.exitCode === 0) {
|
|
239
|
-
spinner.succeed("Code formatted successfully");
|
|
240
|
-
} else {
|
|
241
|
-
spinner.warn("Formatting completed with warnings");
|
|
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"
|
|
242
255
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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).`
|
|
246
273
|
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
async initGitRepo() {
|
|
275
|
-
const spinner = ora("Initializing Git repository...").start();
|
|
276
|
-
try {
|
|
277
|
-
try {
|
|
278
|
-
await execa("git", ["--version"], { stdio: "pipe" });
|
|
279
|
-
} catch {
|
|
280
|
-
spinner.fail("Git is not installed");
|
|
281
|
-
throw new Error("Git is not installed. Please install Git to initialize a repository.");
|
|
282
|
-
}
|
|
283
|
-
await execa("git", ["init"], { cwd: this.targetPath, stdio: "pipe" });
|
|
284
|
-
await execa("git", ["add", "."], { cwd: this.targetPath, stdio: "pipe" });
|
|
285
|
-
await execa(
|
|
286
|
-
"git",
|
|
287
|
-
["commit", "-m", "Initial commit from create-vexcms"],
|
|
288
|
-
{ cwd: this.targetPath, stdio: "pipe" }
|
|
289
|
-
);
|
|
290
|
-
spinner.succeed("Git repository initialized");
|
|
291
|
-
} catch (error) {
|
|
292
|
-
spinner.fail("Failed to initialize Git repository");
|
|
293
|
-
throw new Error(
|
|
294
|
-
`Git initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Generate a secure random secret for Better Auth
|
|
300
|
-
*/
|
|
301
|
-
generateAuthSecret() {
|
|
302
|
-
return crypto.randomBytes(32).toString("hex");
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Write the generated auth secret to .env.example
|
|
306
|
-
*/
|
|
307
|
-
async writeAuthSecret() {
|
|
308
|
-
const envPath = path.join(this.targetPath, ".env.example");
|
|
309
|
-
const secret = this.generateAuthSecret();
|
|
310
|
-
if (await fs3.pathExists(envPath)) {
|
|
311
|
-
let content = await fs3.readFile(envPath, "utf-8");
|
|
312
|
-
content = content.replace(
|
|
313
|
-
/BETTER_AUTH_SECRET=.*/,
|
|
314
|
-
`BETTER_AUTH_SECRET=${secret}`
|
|
315
|
-
);
|
|
316
|
-
await fs3.writeFile(envPath, content);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Configure the Better Auth organizations plugin.
|
|
321
|
-
* When enabled, replaces placeholders with organization import and plugin.
|
|
322
|
-
* When disabled, removes the placeholder lines.
|
|
323
|
-
*/
|
|
324
|
-
async configureOrganizations(enabled) {
|
|
325
|
-
const pluginsPath = path.join(this.targetPath, "convex/auth/plugins/index.ts");
|
|
326
|
-
if (!await fs3.pathExists(pluginsPath)) return;
|
|
327
|
-
let content = await fs3.readFile(pluginsPath, "utf-8");
|
|
328
|
-
if (enabled) {
|
|
329
|
-
content = content.replace(
|
|
330
|
-
"// {{ORGANIZATIONS_IMPORT}}",
|
|
331
|
-
'import { organization } from "better-auth/plugins"'
|
|
332
|
-
);
|
|
333
|
-
content = content.replace(
|
|
334
|
-
" // {{ORGANIZATIONS_PLUGIN}}",
|
|
335
|
-
" organization(),"
|
|
336
|
-
);
|
|
337
|
-
} else {
|
|
338
|
-
content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
|
|
339
|
-
content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
|
|
340
|
-
}
|
|
341
|
-
await fs3.writeFile(pluginsPath, content);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Main orchestration method for project initialization
|
|
345
|
-
*/
|
|
346
|
-
async initProject(options) {
|
|
347
|
-
const copySpinner = ora("Copying template files...").start();
|
|
348
|
-
try {
|
|
349
|
-
await this.copyBaseFiles();
|
|
350
|
-
copySpinner.succeed("Template files copied");
|
|
351
|
-
} catch (error) {
|
|
352
|
-
copySpinner.fail("Failed to copy template files");
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
if (!options.bare) {
|
|
356
|
-
const overlaySpinner = ora("Applying marketing site template...").start();
|
|
357
|
-
try {
|
|
358
|
-
await this.applyTemplateOverlay("marketing-site");
|
|
359
|
-
overlaySpinner.succeed("Marketing site template applied");
|
|
360
|
-
} catch (error) {
|
|
361
|
-
overlaySpinner.fail("Failed to apply template overlay");
|
|
362
|
-
throw error;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const nameSpinner = ora("Configuring project...").start();
|
|
366
|
-
try {
|
|
367
|
-
await this.updatePackageName(options.projectName);
|
|
368
|
-
await this.configurePort(options.port);
|
|
369
|
-
nameSpinner.succeed("Project configured");
|
|
370
|
-
} catch (error) {
|
|
371
|
-
nameSpinner.fail("Failed to configure project");
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
374
|
-
const authSpinner = ora("Configuring authentication...").start();
|
|
375
|
-
try {
|
|
376
|
-
await this.updateOAuthConfig(options.oauthProviders, options.emailPasswordAuth);
|
|
377
|
-
authSpinner.succeed(
|
|
378
|
-
options.emailPasswordAuth || options.oauthProviders.length > 0 ? "Authentication configuration updated" : "Authentication placeholders cleaned up"
|
|
379
|
-
);
|
|
380
|
-
} catch (error) {
|
|
381
|
-
authSpinner.fail("Failed to configure authentication");
|
|
382
|
-
throw error;
|
|
383
|
-
}
|
|
384
|
-
const oauthUISpinner = ora("Configuring OAuth UI...").start();
|
|
385
|
-
try {
|
|
386
|
-
await this.updateOAuthUIConfig(options.oauthProviders, options.emailPasswordAuth);
|
|
387
|
-
oauthUISpinner.succeed(
|
|
388
|
-
options.oauthProviders.length > 0 ? "OAuth UI configuration updated" : "OAuth UI placeholders cleaned up"
|
|
389
|
-
);
|
|
390
|
-
} catch (error) {
|
|
391
|
-
oauthUISpinner.fail("Failed to configure OAuth UI");
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
const envSpinner = ora("Updating .env.example...").start();
|
|
395
|
-
try {
|
|
396
|
-
await this.updateEnvExample(options.oauthProviders);
|
|
397
|
-
envSpinner.succeed(
|
|
398
|
-
options.oauthProviders.length > 0 ? ".env.example updated" : ".env.example placeholders cleaned up"
|
|
399
|
-
);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
envSpinner.fail("Failed to update .env.example");
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
const envTsSpinner = ora("Updating typed env configuration...").start();
|
|
405
|
-
try {
|
|
406
|
-
await this.updateEnvTs(options.oauthProviders);
|
|
407
|
-
envTsSpinner.succeed(
|
|
408
|
-
options.oauthProviders.length > 0 ? "Typed env configuration updated" : "Typed env placeholders cleaned up"
|
|
409
|
-
);
|
|
410
|
-
} catch (error) {
|
|
411
|
-
envTsSpinner.fail("Failed to update typed env configuration");
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
const readmeSpinner = ora("Updating README...").start();
|
|
415
|
-
try {
|
|
416
|
-
await this.updateReadme(options.oauthProviders);
|
|
417
|
-
readmeSpinner.succeed(
|
|
418
|
-
options.oauthProviders.length > 0 ? "README updated" : "README placeholders cleaned up"
|
|
419
|
-
);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
readmeSpinner.fail("Failed to update README");
|
|
422
|
-
throw error;
|
|
423
|
-
}
|
|
424
|
-
const orgsSpinner = ora("Configuring organizations...").start();
|
|
425
|
-
try {
|
|
426
|
-
await this.configureOrganizations(options.orgs);
|
|
427
|
-
orgsSpinner.succeed(
|
|
428
|
-
options.orgs ? "Organizations plugin enabled" : "Organizations placeholders cleaned up"
|
|
429
|
-
);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
orgsSpinner.fail("Failed to configure organizations");
|
|
432
|
-
throw error;
|
|
433
|
-
}
|
|
434
|
-
const secretSpinner = ora("Generating auth secret...").start();
|
|
435
|
-
try {
|
|
436
|
-
await this.writeAuthSecret();
|
|
437
|
-
secretSpinner.succeed("Auth secret generated");
|
|
438
|
-
} catch (error) {
|
|
439
|
-
secretSpinner.fail("Failed to generate auth secret");
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
if (options.initGit) {
|
|
443
|
-
await this.initGitRepo();
|
|
444
|
-
}
|
|
445
|
-
if (options.installDependencies) {
|
|
446
|
-
await this.installDependencies();
|
|
447
|
-
await this.lintCode();
|
|
448
|
-
await this.formatCode();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// src/installers/nextjs.ts
|
|
454
|
-
import { join as join2 } from "path";
|
|
455
|
-
|
|
456
|
-
// src/installers/string-utils.ts
|
|
457
|
-
import fs4 from "fs-extra";
|
|
458
|
-
|
|
459
|
-
// src/installers/providers.ts
|
|
460
|
-
var OAUTH_PROVIDERS = {
|
|
461
|
-
// ========================================
|
|
462
|
-
// POPULAR PROVIDERS (10)
|
|
463
|
-
// ========================================
|
|
464
|
-
google: {
|
|
465
|
-
id: "google",
|
|
466
|
-
name: "Google",
|
|
467
|
-
envPrefix: "GOOGLE",
|
|
468
|
-
clientIdVar: "GOOGLE_CLIENT_ID",
|
|
469
|
-
clientSecretVar: "GOOGLE_CLIENT_SECRET",
|
|
470
|
-
popular: true,
|
|
471
|
-
betterAuthConfig: {
|
|
472
|
-
import: "",
|
|
473
|
-
clientSideProvider: '"google"',
|
|
474
|
-
socialProvider: `google({
|
|
475
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
476
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
477
|
-
})`,
|
|
478
|
-
scopes: []
|
|
479
|
-
},
|
|
480
|
-
env: [
|
|
481
|
-
{
|
|
482
|
-
name: "GOOGLE_CLIENT_ID",
|
|
483
|
-
type: "server",
|
|
484
|
-
description: "Google OAuth Client ID"
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
name: "GOOGLE_CLIENT_SECRET",
|
|
488
|
-
type: "server",
|
|
489
|
-
description: "Google OAuth Client Secret"
|
|
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"
|
|
490
301
|
}
|
|
491
302
|
],
|
|
492
303
|
docs: {
|
|
493
|
-
provider: "https://
|
|
304
|
+
provider: "https://discord.com/developers/applications",
|
|
494
305
|
betterAuth: "https://www.better-auth.com/docs/authentication/social"
|
|
495
306
|
},
|
|
496
307
|
requiresExtraConfig: false,
|
|
497
|
-
extraConfigNotes: "",
|
|
308
|
+
extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
|
|
498
309
|
readme: {
|
|
499
|
-
title: "
|
|
500
|
-
content: `##
|
|
310
|
+
title: "Discord OAuth Setup",
|
|
311
|
+
content: `## Discord OAuth Setup
|
|
501
312
|
|
|
502
|
-
1. Create
|
|
503
|
-
2.
|
|
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)
|
|
504
315
|
3. Copy the Client ID and Client Secret to your \`.env\` file
|
|
505
316
|
|
|
506
317
|
For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
|
|
507
318
|
}
|
|
508
319
|
},
|
|
509
|
-
|
|
510
|
-
id: "
|
|
511
|
-
name: "
|
|
512
|
-
envPrefix: "
|
|
513
|
-
clientIdVar: "
|
|
514
|
-
clientSecretVar: "
|
|
320
|
+
apple: {
|
|
321
|
+
id: "apple",
|
|
322
|
+
name: "Apple",
|
|
323
|
+
envPrefix: "APPLE",
|
|
324
|
+
clientIdVar: "APPLE_CLIENT_ID",
|
|
325
|
+
clientSecretVar: "APPLE_CLIENT_SECRET",
|
|
515
326
|
popular: true,
|
|
516
327
|
betterAuthConfig: {
|
|
517
328
|
import: "",
|
|
518
|
-
clientSideProvider: '"
|
|
519
|
-
socialProvider: `
|
|
520
|
-
clientId: process.env.
|
|
521
|
-
clientSecret: process.env.
|
|
329
|
+
clientSideProvider: '"apple"',
|
|
330
|
+
socialProvider: `apple({
|
|
331
|
+
clientId: process.env.APPLE_CLIENT_ID!,
|
|
332
|
+
clientSecret: process.env.APPLE_CLIENT_SECRET!,
|
|
522
333
|
})`,
|
|
523
|
-
scopes: [
|
|
334
|
+
scopes: []
|
|
524
335
|
},
|
|
525
336
|
env: [
|
|
526
337
|
{
|
|
527
|
-
name: "
|
|
338
|
+
name: "APPLE_CLIENT_ID",
|
|
528
339
|
type: "server",
|
|
529
|
-
description: "
|
|
340
|
+
description: "Apple Sign In Service ID"
|
|
530
341
|
},
|
|
531
342
|
{
|
|
532
|
-
name: "
|
|
343
|
+
name: "APPLE_CLIENT_SECRET",
|
|
533
344
|
type: "server",
|
|
534
|
-
description: "
|
|
345
|
+
description: "Apple Sign In Client Secret (JWT token)"
|
|
535
346
|
}
|
|
536
347
|
],
|
|
537
348
|
docs: {
|
|
538
|
-
provider: "https://
|
|
349
|
+
provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
|
|
539
350
|
betterAuth: "https://www.better-auth.com/docs/authentication/social"
|
|
540
351
|
},
|
|
541
352
|
requiresExtraConfig: true,
|
|
542
|
-
extraConfigNotes: "
|
|
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.",
|
|
543
354
|
readme: {
|
|
544
|
-
title: "
|
|
545
|
-
content: `##
|
|
546
|
-
|
|
547
|
-
1. Create a GitHub OAuth App at https://github.com/settings/developers
|
|
548
|
-
2. Set the Authorization callback URL to: \`http://localhost:3000/api/auth/callback/github\` (update for production)
|
|
549
|
-
3. Copy the Client ID and Client Secret to your \`.env\` file
|
|
550
|
-
4. **Important**: Include the \`user:email\` scope in your GitHub app permissions
|
|
551
|
-
|
|
552
|
-
For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
discord: {
|
|
556
|
-
id: "discord",
|
|
557
|
-
name: "Discord",
|
|
558
|
-
envPrefix: "DISCORD",
|
|
559
|
-
clientIdVar: "DISCORD_CLIENT_ID",
|
|
560
|
-
clientSecretVar: "DISCORD_CLIENT_SECRET",
|
|
561
|
-
popular: true,
|
|
562
|
-
betterAuthConfig: {
|
|
563
|
-
import: "",
|
|
564
|
-
clientSideProvider: '"discord"',
|
|
565
|
-
socialProvider: `discord({
|
|
566
|
-
clientId: process.env.DISCORD_CLIENT_ID!,
|
|
567
|
-
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
|
568
|
-
})`,
|
|
569
|
-
scopes: []
|
|
570
|
-
},
|
|
571
|
-
env: [
|
|
572
|
-
{
|
|
573
|
-
name: "DISCORD_CLIENT_ID",
|
|
574
|
-
type: "server",
|
|
575
|
-
description: "Discord OAuth Application Client ID"
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
name: "DISCORD_CLIENT_SECRET",
|
|
579
|
-
type: "server",
|
|
580
|
-
description: "Discord OAuth Application Client Secret"
|
|
581
|
-
}
|
|
582
|
-
],
|
|
583
|
-
docs: {
|
|
584
|
-
provider: "https://discord.com/developers/applications",
|
|
585
|
-
betterAuth: "https://www.better-auth.com/docs/authentication/social"
|
|
586
|
-
},
|
|
587
|
-
requiresExtraConfig: false,
|
|
588
|
-
extraConfigNotes: "You can optionally add a permissions field to request additional Discord permissions.",
|
|
589
|
-
readme: {
|
|
590
|
-
title: "Discord OAuth Setup",
|
|
591
|
-
content: `## Discord OAuth Setup
|
|
592
|
-
|
|
593
|
-
1. Create an application at https://discord.com/developers/applications
|
|
594
|
-
2. Add a redirect URL: \`http://localhost:3000/api/auth/callback/discord\` (update for production)
|
|
595
|
-
3. Copy the Client ID and Client Secret to your \`.env\` file
|
|
596
|
-
|
|
597
|
-
For more details, see the [Better Auth documentation](https://www.better-auth.com/docs/authentication/social).`
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
apple: {
|
|
601
|
-
id: "apple",
|
|
602
|
-
name: "Apple",
|
|
603
|
-
envPrefix: "APPLE",
|
|
604
|
-
clientIdVar: "APPLE_CLIENT_ID",
|
|
605
|
-
clientSecretVar: "APPLE_CLIENT_SECRET",
|
|
606
|
-
popular: true,
|
|
607
|
-
betterAuthConfig: {
|
|
608
|
-
import: "",
|
|
609
|
-
clientSideProvider: '"apple"',
|
|
610
|
-
socialProvider: `apple({
|
|
611
|
-
clientId: process.env.APPLE_CLIENT_ID!,
|
|
612
|
-
clientSecret: process.env.APPLE_CLIENT_SECRET!,
|
|
613
|
-
})`,
|
|
614
|
-
scopes: []
|
|
615
|
-
},
|
|
616
|
-
env: [
|
|
617
|
-
{
|
|
618
|
-
name: "APPLE_CLIENT_ID",
|
|
619
|
-
type: "server",
|
|
620
|
-
description: "Apple Sign In Service ID"
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
name: "APPLE_CLIENT_SECRET",
|
|
624
|
-
type: "server",
|
|
625
|
-
description: "Apple Sign In Client Secret (JWT token)"
|
|
626
|
-
}
|
|
627
|
-
],
|
|
628
|
-
docs: {
|
|
629
|
-
provider: "https://developer.apple.com/sign-in-with-apple/get-started/",
|
|
630
|
-
betterAuth: "https://www.better-auth.com/docs/authentication/social"
|
|
631
|
-
},
|
|
632
|
-
requiresExtraConfig: true,
|
|
633
|
-
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.",
|
|
634
|
-
readme: {
|
|
635
|
-
title: "Apple Sign In Setup",
|
|
636
|
-
content: `## Apple Sign In Setup
|
|
355
|
+
title: "Apple Sign In Setup",
|
|
356
|
+
content: `## Apple Sign In Setup
|
|
637
357
|
|
|
638
358
|
1. Create a Sign In with Apple service at https://developer.apple.com
|
|
639
359
|
2. Configure your Service ID and return URLs
|
|
@@ -2093,145 +1813,712 @@ async function replacePlaceholder(filePath, placeholder, content, options) {
|
|
|
2093
1813
|
updatedLines.push(line);
|
|
2094
1814
|
}
|
|
2095
1815
|
}
|
|
2096
|
-
await fs4.writeFile(filePath, updatedLines.join("\n"), "utf-8");
|
|
2097
|
-
}
|
|
2098
|
-
function generateCredentialsValue(enabled) {
|
|
2099
|
-
return `credentials={${enabled}}`;
|
|
2100
|
-
}
|
|
2101
|
-
function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
|
|
2102
|
-
const parts = [];
|
|
2103
|
-
parts.push(`emailAndPassword: {
|
|
2104
|
-
enabled: ${emailPasswordEnabled}
|
|
2105
|
-
},`);
|
|
2106
|
-
if (oauthProviders.length > 0) {
|
|
2107
|
-
const providersObject = oauthProviders.map((providerId) => {
|
|
2108
|
-
const provider = getProvider(providerId);
|
|
2109
|
-
if (!provider) {
|
|
2110
|
-
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
|
2111
|
-
}
|
|
2112
|
-
const configLines = [
|
|
2113
|
-
`clientId: process.env.${provider.envPrefix}_CLIENT_ID!,`,
|
|
2114
|
-
`clientSecret: process.env.${provider.envPrefix}_CLIENT_SECRET!,`
|
|
2115
|
-
];
|
|
2116
|
-
if (providerId === "figma") {
|
|
2117
|
-
configLines.push(`clientKey: process.env.FIGMA_CLIENT_KEY!,`);
|
|
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
|
|
1943
|
+
|
|
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
|
+
}
|
|
2069
|
+
|
|
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);
|
|
2106
|
+
}
|
|
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}`;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
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");
|
|
2229
|
+
}
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2232
|
+
spinner.warn(`Failed to format code: ${errorMessage}`);
|
|
2233
|
+
}
|
|
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");
|
|
2252
|
+
}
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2255
|
+
spinner.warn(`Failed to lint code: ${errorMessage}`);
|
|
2256
|
+
}
|
|
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.");
|
|
2269
|
+
}
|
|
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
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
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}`
|
|
2304
|
+
);
|
|
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?/, "");
|
|
2118
2337
|
}
|
|
2119
|
-
|
|
2120
|
-
${configLines.join("\n ")}
|
|
2121
|
-
}`;
|
|
2122
|
-
}).join(",\n ");
|
|
2123
|
-
parts.push(`socialProviders: {
|
|
2124
|
-
${providersObject}
|
|
2125
|
-
},`);
|
|
2126
|
-
}
|
|
2127
|
-
return parts.join("\n ");
|
|
2128
|
-
}
|
|
2129
|
-
function generateEnvTsServerSchema(providers) {
|
|
2130
|
-
if (providers.length === 0) {
|
|
2131
|
-
return "";
|
|
2132
|
-
}
|
|
2133
|
-
const schemas = providers.map((providerId) => {
|
|
2134
|
-
const provider = getProvider(providerId);
|
|
2135
|
-
if (!provider) {
|
|
2136
|
-
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
|
2338
|
+
await fs5.writeFile(pluginsPath, content);
|
|
2137
2339
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
lines.push(`${provider.envPrefix}_CLIENT_SECRET: z.string(),`);
|
|
2141
|
-
if (providerId === "figma") {
|
|
2142
|
-
lines.push(`FIGMA_CLIENT_KEY: z.string(),`);
|
|
2340
|
+
if (!enabled) {
|
|
2341
|
+
await this.stripOrganizationWiring();
|
|
2143
2342
|
}
|
|
2144
|
-
return lines.join("\n ");
|
|
2145
|
-
}).join("\n ");
|
|
2146
|
-
return schemas;
|
|
2147
|
-
}
|
|
2148
|
-
function generateEnvTsRuntimeMapping(providers) {
|
|
2149
|
-
if (providers.length === 0) {
|
|
2150
|
-
return "";
|
|
2151
2343
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
2375
|
+
await fs5.writeFile(filePath, file.transform(filePath, content));
|
|
2156
2376
|
}
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
lines.push(`FIGMA_CLIENT_KEY: process.env.FIGMA_CLIENT_KEY,`);
|
|
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));
|
|
2162
2381
|
}
|
|
2163
|
-
return lines.join("\n ");
|
|
2164
|
-
}).join("\n ");
|
|
2165
|
-
return mappings;
|
|
2166
|
-
}
|
|
2167
|
-
function generateOAuthUIProvidersBlock(providers) {
|
|
2168
|
-
if (providers.length === 0) {
|
|
2169
|
-
return "";
|
|
2170
|
-
}
|
|
2171
|
-
const providerList = providers.map((id) => `"${id}"`).join(", ");
|
|
2172
|
-
return `social={{
|
|
2173
|
-
providers: [${providerList}]
|
|
2174
|
-
}}`;
|
|
2175
|
-
}
|
|
2176
|
-
function generateEnvVarsBlock(providers, framework) {
|
|
2177
|
-
if (providers.length === 0) {
|
|
2178
|
-
return "";
|
|
2179
2382
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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;
|
|
2184
2396
|
}
|
|
2185
|
-
if (
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
}
|
|
2194
|
-
} else {
|
|
2195
|
-
return [
|
|
2196
|
-
`${provider.clientIdVar}=`,
|
|
2197
|
-
`${provider.clientSecretVar}=`
|
|
2198
|
-
];
|
|
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;
|
|
2405
|
+
}
|
|
2199
2406
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
const provider = getProvider(providerId);
|
|
2209
|
-
if (!provider) {
|
|
2210
|
-
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
|
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;
|
|
2211
2415
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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"
|
|
2215
2421
|
);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
authSpinner.fail("Failed to configure authentication");
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
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;
|
|
2435
|
+
}
|
|
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;
|
|
2445
|
+
}
|
|
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;
|
|
2455
|
+
}
|
|
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;
|
|
2465
|
+
}
|
|
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"
|
|
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();
|
|
2216
2501
|
}
|
|
2217
|
-
return provider.readme.content;
|
|
2218
|
-
}).filter(Boolean);
|
|
2219
|
-
if (sections.length === 0) {
|
|
2220
|
-
return "";
|
|
2221
2502
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
${sections.join("\n\n---\n\n")}`;
|
|
2225
|
-
}
|
|
2503
|
+
};
|
|
2226
2504
|
|
|
2227
2505
|
// src/installers/nextjs.ts
|
|
2506
|
+
import { join as join2 } from "path";
|
|
2228
2507
|
var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
2508
|
+
/**
|
|
2509
|
+
* Framework identifier used for template selection and logging.
|
|
2510
|
+
*
|
|
2511
|
+
* @returns The literal string `"nextjs"`.
|
|
2512
|
+
*/
|
|
2229
2513
|
get frameworkName() {
|
|
2230
2514
|
return "nextjs";
|
|
2231
2515
|
}
|
|
2232
2516
|
/**
|
|
2233
2517
|
* Update OAuth configuration in Convex auth options file
|
|
2234
2518
|
* Target file: convex/auth/options.ts
|
|
2519
|
+
*
|
|
2520
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2521
|
+
* @param emailPasswordEnabled - Whether email/password authentication is also enabled.
|
|
2235
2522
|
*/
|
|
2236
2523
|
async updateOAuthConfig(selectedProviders, emailPasswordEnabled) {
|
|
2237
2524
|
const authFilePath = join2(this.targetPath, "convex/auth/options.ts");
|
|
@@ -2254,6 +2541,9 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2254
2541
|
/**
|
|
2255
2542
|
* Update OAuth UI configuration in auth client file
|
|
2256
2543
|
* Target file: src/auth/client.tsx
|
|
2544
|
+
*
|
|
2545
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2546
|
+
* @param emailPasswordEnabled - Whether email/password authentication is also enabled.
|
|
2257
2547
|
*/
|
|
2258
2548
|
async updateOAuthUIConfig(selectedProviders, emailPasswordEnabled) {
|
|
2259
2549
|
const providersFilePath = join2(this.targetPath, "src/auth/client.tsx");
|
|
@@ -2273,6 +2563,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2273
2563
|
/**
|
|
2274
2564
|
* Update .env.example with OAuth environment variables
|
|
2275
2565
|
* Target file: .env.example
|
|
2566
|
+
*
|
|
2567
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2276
2568
|
*/
|
|
2277
2569
|
async updateEnvExample(selectedProviders) {
|
|
2278
2570
|
const envFilePath = join2(this.targetPath, ".env.example");
|
|
@@ -2286,6 +2578,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2286
2578
|
/**
|
|
2287
2579
|
* Update README with OAuth provider setup guides
|
|
2288
2580
|
* Target file: README.md
|
|
2581
|
+
*
|
|
2582
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2289
2583
|
*/
|
|
2290
2584
|
async updateReadme(selectedProviders) {
|
|
2291
2585
|
const readmeFilePath = join2(this.targetPath, "README.md");
|
|
@@ -2300,6 +2594,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2300
2594
|
/**
|
|
2301
2595
|
* Update env.mjs with OAuth provider environment variables
|
|
2302
2596
|
* Target file: src/env.mjs
|
|
2597
|
+
*
|
|
2598
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2303
2599
|
*/
|
|
2304
2600
|
async updateEnvTs(selectedProviders) {
|
|
2305
2601
|
const envFilePath = join2(this.targetPath, "src/env.mjs");
|
|
@@ -2331,7 +2627,7 @@ function createInstaller(props) {
|
|
|
2331
2627
|
}
|
|
2332
2628
|
|
|
2333
2629
|
// src/index.ts
|
|
2334
|
-
var program = new Command().name("create-vexcms").description("Scaffold a new VEX CMS project").argument("[project-name]", "Project directory name").option("--bare", "Skip marketing site collections, scaffold empty project").option("--orgs", "Enable multi-tenant organizations").version("0.0.2").parse();
|
|
2630
|
+
var program = new Command().name("create-vexcms").description("Scaffold a new VEX CMS project").argument("[project-name]", "Project directory name").option("--bare", "Skip marketing site collections, scaffold empty project").option("--orgs", "Enable multi-tenant organizations").option("--monorepo", "Scaffold into apps/<name> under the detected pnpm workspace root, rewriting dependencies to workspace/catalog protocols").option("--yes", "Accept every prompt default without rendering (for automation)").version("0.0.2").parse();
|
|
2335
2631
|
var args = program.args;
|
|
2336
2632
|
var opts = program.opts();
|
|
2337
2633
|
async function main() {
|
|
@@ -2339,15 +2635,19 @@ async function main() {
|
|
|
2339
2635
|
console.log(chalk2.bold(" create-vexcms"));
|
|
2340
2636
|
console.log();
|
|
2341
2637
|
const bare = opts.bare ?? false;
|
|
2638
|
+
const yes = opts.yes ?? false;
|
|
2639
|
+
const monorepo = opts.monorepo ?? false;
|
|
2342
2640
|
let inputArg;
|
|
2343
2641
|
if (args[0]) {
|
|
2344
2642
|
inputArg = args[0];
|
|
2643
|
+
} else if (yes) {
|
|
2644
|
+
inputArg = "my-vexcms-app";
|
|
2345
2645
|
} else {
|
|
2346
2646
|
inputArg = await input({
|
|
2347
2647
|
message: "(1/8) What is your project named?",
|
|
2348
2648
|
default: "my-vexcms-app",
|
|
2349
2649
|
validate: (value) => {
|
|
2350
|
-
const name = value.includes("/") ?
|
|
2650
|
+
const name = value.includes("/") ? path3.basename(value) : value;
|
|
2351
2651
|
if (name === ".") return true;
|
|
2352
2652
|
const result = validateProjectName(name);
|
|
2353
2653
|
if (result.valid) return true;
|
|
@@ -2356,52 +2656,80 @@ async function main() {
|
|
|
2356
2656
|
});
|
|
2357
2657
|
}
|
|
2358
2658
|
inputArg = resolveProjectName(inputArg, process.cwd());
|
|
2359
|
-
const projectName = inputArg === "." ?
|
|
2659
|
+
const projectName = inputArg === "." ? path3.basename(process.cwd()) : path3.basename(inputArg);
|
|
2360
2660
|
const validation = validateProjectName(projectName);
|
|
2361
2661
|
if (!validation.valid) {
|
|
2362
2662
|
displayInvalidNameError(projectName, validation.errors);
|
|
2363
2663
|
process.exit(1);
|
|
2364
2664
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2665
|
+
let targetDir;
|
|
2666
|
+
let workspaceRoot = null;
|
|
2667
|
+
if (monorepo) {
|
|
2668
|
+
workspaceRoot = await findWorkspaceRoot({ cwd: process.cwd() });
|
|
2669
|
+
if (!workspaceRoot) {
|
|
2670
|
+
console.error(chalk2.red("\nError: --monorepo requires a pnpm workspace."));
|
|
2671
|
+
console.error(chalk2.yellow(`No pnpm-workspace.yaml was found walking up from ${process.cwd()}.`));
|
|
2672
|
+
console.error(chalk2.yellow("Run this command from inside a pnpm workspace, or drop --monorepo."));
|
|
2369
2673
|
process.exit(1);
|
|
2370
2674
|
}
|
|
2675
|
+
targetDir = path3.join(workspaceRoot, "apps", projectName);
|
|
2676
|
+
if (await checkDirectoryExists(targetDir)) {
|
|
2677
|
+
console.error(chalk2.red(`
|
|
2678
|
+
Error: 'apps/${projectName}' already exists under ${workspaceRoot}.`));
|
|
2679
|
+
console.error(chalk2.yellow("--monorepo refuses to scaffold over an existing directory."));
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
} else {
|
|
2683
|
+
targetDir = inputArg === "." ? process.cwd() : path3.resolve(process.cwd(), inputArg);
|
|
2684
|
+
if (await checkDirectoryExists(targetDir)) {
|
|
2685
|
+
if (!await isDirectoryEmpty(targetDir)) {
|
|
2686
|
+
displayDirectoryNotEmptyError();
|
|
2687
|
+
process.exit(1);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2371
2690
|
}
|
|
2372
2691
|
let framework;
|
|
2373
|
-
|
|
2374
|
-
framework =
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2692
|
+
if (yes) {
|
|
2693
|
+
framework = "nextjs";
|
|
2694
|
+
} else {
|
|
2695
|
+
while (true) {
|
|
2696
|
+
framework = await select({
|
|
2697
|
+
message: "(2/8) Select a framework:",
|
|
2698
|
+
choices: [
|
|
2699
|
+
{ name: "Next.js (Recommended)", value: "nextjs" },
|
|
2700
|
+
{ name: "TanStack Start (Coming Soon)", value: "tanstack" }
|
|
2701
|
+
]
|
|
2702
|
+
});
|
|
2703
|
+
if (framework === "tanstack") {
|
|
2704
|
+
console.log(chalk2.yellow("\n TanStack Start support is coming soon! Please select Next.js for now.\n"));
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
break;
|
|
2384
2708
|
}
|
|
2385
|
-
break;
|
|
2386
2709
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2710
|
+
let port;
|
|
2711
|
+
if (yes) {
|
|
2712
|
+
port = 3010;
|
|
2713
|
+
} else {
|
|
2714
|
+
const portInput = await input({
|
|
2715
|
+
message: "(3/8) Dev server port:",
|
|
2716
|
+
default: "3010",
|
|
2717
|
+
validate: (value) => {
|
|
2718
|
+
const num = parseInt(value, 10);
|
|
2719
|
+
if (isNaN(num) || num < 1 || num > 65535) return "Must be a valid port number (1-65535)";
|
|
2720
|
+
return true;
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
port = parseInt(portInput, 10);
|
|
2724
|
+
}
|
|
2725
|
+
const emailPasswordAuth = yes ? true : await confirm({
|
|
2398
2726
|
message: "(4/8) Enable email/password authentication?",
|
|
2399
2727
|
default: true
|
|
2400
2728
|
});
|
|
2401
|
-
const orgs = opts.orgs ?? await confirm({
|
|
2729
|
+
const orgs = opts.orgs ?? (yes ? false : await confirm({
|
|
2402
2730
|
message: "(5/8) Enable multi-tenant (organizations)?",
|
|
2403
2731
|
default: false
|
|
2404
|
-
});
|
|
2732
|
+
}));
|
|
2405
2733
|
const popularProviders = getPopularProviders();
|
|
2406
2734
|
const additionalProviders = getAdditionalProviders();
|
|
2407
2735
|
const allProviderChoices = [
|
|
@@ -2415,15 +2743,15 @@ async function main() {
|
|
|
2415
2743
|
value: p.id
|
|
2416
2744
|
}))
|
|
2417
2745
|
];
|
|
2418
|
-
const oauthProviders = await checkbox({
|
|
2746
|
+
const oauthProviders = yes ? [] : await checkbox({
|
|
2419
2747
|
message: "(6/8) Select OAuth providers (space to toggle, enter to confirm):",
|
|
2420
2748
|
choices: allProviderChoices
|
|
2421
2749
|
});
|
|
2422
|
-
const initGit = await confirm({
|
|
2750
|
+
const initGit = yes ? true : await confirm({
|
|
2423
2751
|
message: "(7/8) Initialize a Git repository?",
|
|
2424
2752
|
default: true
|
|
2425
2753
|
});
|
|
2426
|
-
const installDependencies = await confirm({
|
|
2754
|
+
const installDependencies = yes ? false : await confirm({
|
|
2427
2755
|
message: "(8/8) Install dependencies?",
|
|
2428
2756
|
default: false
|
|
2429
2757
|
});
|
|
@@ -2437,9 +2765,12 @@ async function main() {
|
|
|
2437
2765
|
emailPasswordAuth,
|
|
2438
2766
|
oauthProviders,
|
|
2439
2767
|
initGit,
|
|
2440
|
-
installDependencies
|
|
2768
|
+
installDependencies,
|
|
2769
|
+
monorepo,
|
|
2770
|
+
workspaceRoot,
|
|
2771
|
+
yes
|
|
2441
2772
|
};
|
|
2442
|
-
await
|
|
2773
|
+
await fs6.ensureDir(targetDir);
|
|
2443
2774
|
const installer = createInstaller({
|
|
2444
2775
|
framework,
|
|
2445
2776
|
projectDir: targetDir,
|
|
@@ -2449,6 +2780,10 @@ async function main() {
|
|
|
2449
2780
|
await installer.initProject(options);
|
|
2450
2781
|
console.log();
|
|
2451
2782
|
displaySuccessMessage(options.projectName, targetDir, inputArg === ".");
|
|
2783
|
+
if (monorepo && workspaceRoot) {
|
|
2784
|
+
console.log(chalk2.cyan(` Run 'pnpm install' from ${workspaceRoot} to link the new workspace member.`));
|
|
2785
|
+
console.log();
|
|
2786
|
+
}
|
|
2452
2787
|
}
|
|
2453
2788
|
main().catch((error) => {
|
|
2454
2789
|
console.error(chalk2.red("\nAn error occurred:"), error.message);
|