create-vexcms 0.0.20 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +97 -12
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  85. package/templates/marketing-site/src/vex.config.ts +35 -0
  86. package/templates/marketing-site/src/vex.types.ts +634 -0
  87. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  88. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  90. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  92. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  93. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  96. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  97. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  98. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  100. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  102. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  110. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  111. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  112. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  113. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  114. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  115. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  116. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  117. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  118. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  119. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  120. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  121. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  122. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  123. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  124. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  125. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  126. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  127. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  128. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  129. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  130. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  131. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  132. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  133. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  134. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  135. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  136. package/templates/base-nextjs/src/db/types.ts +0 -6
  137. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  138. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  139. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  140. package/templates/base-nextjs/vex.config.ts +0 -25
  141. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  142. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  144. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  145. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  146. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  147. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  148. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  149. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  150. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  151. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  152. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  153. 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 path2 from "path";
8
- import fs5 from "fs-extra";
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 (error) {
35
+ } catch {
36
36
  return true;
37
37
  }
38
38
  }
39
- function resolveProjectName(input2, cwd) {
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 fs3 from "fs-extra";
124
+ import fs4 from "fs-extra";
80
125
  import { execa } from "execa";
81
126
  import ora from "ora";
82
127
  import crypto from "crypto";
83
- import path from "path";
128
+ import path2 from "path";
84
129
 
85
130
  // src/helpers/fileOperations.ts
86
- import fs2 from "fs-extra";
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 (!fs2.existsSync(templatePath)) {
139
+ if (!fs3.existsSync(templatePath)) {
95
140
  templatePath = join(__dirname, "../templates", templateDir);
96
141
  }
97
- await fs2.copy(templatePath, targetPath, {
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 fs2.pathExists(sourceGitignore)) {
109
- await fs2.copy(sourceGitignore, targetGitignore, {
153
+ if (await fs3.pathExists(sourceGitignore)) {
154
+ await fs3.copy(sourceGitignore, targetGitignore, {
110
155
  overwrite: false,
111
156
  errorOnExist: false
112
157
  });
@@ -119,21 +164,30 @@ 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 fs2.pathExists(source)) {
123
- await fs2.copy(source, target, { overwrite: false, errorOnExist: false });
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 fs2.copy(overlayDir, targetDir, { overwrite: true });
173
+ await fs3.copy(overlayDir, targetDir, { overwrite: true });
129
174
  }
130
175
 
131
176
  // src/installers/base.ts
132
177
  var VexFrameworkInstaller = class {
178
+ /**
179
+ * Binds this installer to the destination directory and package name used
180
+ * throughout scaffolding (template copy target, package.json, README, etc).
181
+ *
182
+ * @param targetPath - Absolute path of the directory the project is created in.
183
+ * @param projectName - npm package name recorded for subclasses; not read directly by the base class.
184
+ */
133
185
  constructor(targetPath, projectName) {
134
186
  this.targetPath = targetPath;
135
187
  this.projectName = projectName;
136
188
  }
189
+ targetPath;
190
+ projectName;
137
191
  /**
138
192
  * Copy base template files to target directory
139
193
  */
@@ -144,40 +198,52 @@ var VexFrameworkInstaller = class {
144
198
  * Apply a template overlay on top of the base template.
145
199
  * Merges overlay files onto the already-copied base — existing files
146
200
  * not in the overlay are left untouched.
201
+ *
202
+ * @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
147
203
  */
148
204
  async applyTemplateOverlay(overlay) {
149
205
  const { dirname: dirname2 } = await import("path");
150
206
  const { fileURLToPath: fileURLToPath2 } = await import("url");
151
207
  const __filename = fileURLToPath2(import.meta.url);
152
208
  const __dirname = dirname2(__filename);
153
- let overlayDir = path.join(__dirname, "../../templates", overlay);
154
- if (!await fs3.pathExists(overlayDir)) {
155
- overlayDir = path.join(__dirname, "../templates", overlay);
209
+ let overlayDir = path2.join(__dirname, "../../templates", overlay);
210
+ if (!await fs4.pathExists(overlayDir)) {
211
+ overlayDir = path2.join(__dirname, "../templates", overlay);
156
212
  }
157
213
  await overlayTemplate({ overlayDir, targetDir: this.targetPath });
158
214
  }
159
215
  /**
160
- * Update the package.json name field and vex:update script
161
- * to match the detected package manager.
216
+ * Update the package.json name field, the vex:update script (to match the
217
+ * detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
218
+ *
219
+ * @param name - npm package name to write into `package.json`'s `name` field
220
+ * and to substitute for `{{PROJECT_NAME}}` in `README.md`.
162
221
  */
163
222
  async updatePackageName(name) {
164
- const pkgPath = path.join(this.targetPath, "package.json");
165
- const pkg = await fs3.readJson(pkgPath);
223
+ const pkgPath = path2.join(this.targetPath, "package.json");
224
+ const pkg = await fs4.readJson(pkgPath);
166
225
  pkg.name = name;
167
226
  const pm = this.detectPackageManager();
168
227
  if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
169
228
  const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
170
229
  pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
171
230
  }
172
- await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
231
+ await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
232
+ const readmePath = path2.join(this.targetPath, "README.md");
233
+ if (await fs4.pathExists(readmePath)) {
234
+ const readme = await fs4.readFile(readmePath, "utf-8");
235
+ await fs4.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
236
+ }
173
237
  }
174
238
  /**
175
239
  * Configure the dev server port.
176
240
  * Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
241
+ *
242
+ * @param port - Dev server port number to write into the `dev` script and `.env.local`.
177
243
  */
178
244
  async configurePort(port) {
179
- const pkgPath = path.join(this.targetPath, "package.json");
180
- const pkg = await fs3.readJson(pkgPath);
245
+ const pkgPath = path2.join(this.targetPath, "package.json");
246
+ const pkg = await fs4.readJson(pkgPath);
181
247
  if (pkg.scripts?.dev) {
182
248
  if (pkg.scripts.dev.includes("--port")) {
183
249
  pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
@@ -185,15 +251,42 @@ var VexFrameworkInstaller = class {
185
251
  pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
186
252
  }
187
253
  }
188
- await fs3.writeJson(pkgPath, pkg, { spaces: 2 });
189
- const envLocalPath = path.join(this.targetPath, ".env.local");
190
- const envContent = `NEXT_PUBLIC_SITE_URL=http://localhost:${port}
191
- BETTER_AUTH_SECRET=""
192
- `;
193
- await fs3.writeFile(envLocalPath, envContent);
254
+ await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
255
+ const siteUrl = `http://localhost:${port}`;
256
+ const envLocalPath = path2.join(this.targetPath, ".env.local");
257
+ const envContent = [
258
+ `NEXT_PUBLIC_SITE_URL=${siteUrl}`,
259
+ `SITE_URL=${siteUrl}`,
260
+ `BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
261
+ `NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
262
+ `NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
263
+ `CONVEX_DEPLOYMENT=`,
264
+ ""
265
+ ].join("\n");
266
+ await fs4.writeFile(envLocalPath, envContent);
267
+ }
268
+ /**
269
+ * Rewrites the scaffolded `package.json` for life inside a host pnpm
270
+ * workspace (`--monorepo`). `@vexcms/*` dependencies become
271
+ * `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
272
+ * catalog also pins becomes `catalog:`; everything else keeps its
273
+ * literal, catalog-resolved version. No-op when `options.monorepo` is
274
+ * false or `options.workspaceRoot` is unset.
275
+ *
276
+ * @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
277
+ */
278
+ async applyMonorepoRewrite(options) {
279
+ if (!options.monorepo || !options.workspaceRoot) return;
280
+ const pkgPath = path2.join(this.targetPath, "package.json");
281
+ const manifest = await fs4.readJson(pkgPath);
282
+ const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
283
+ const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
284
+ await fs4.writeJson(pkgPath, rewritten, { spaces: 2 });
194
285
  }
195
286
  /**
196
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.
197
290
  */
198
291
  detectPackageManager() {
199
292
  const userAgent = process.env.npm_config_user_agent;
@@ -297,6 +390,8 @@ BETTER_AUTH_SECRET=""
297
390
  }
298
391
  /**
299
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`.
300
395
  */
301
396
  generateAuthSecret() {
302
397
  return crypto.randomBytes(32).toString("hex");
@@ -305,26 +400,28 @@ BETTER_AUTH_SECRET=""
305
400
  * Write the generated auth secret to .env.example
306
401
  */
307
402
  async writeAuthSecret() {
308
- const envPath = path.join(this.targetPath, ".env.example");
403
+ const envPath = path2.join(this.targetPath, ".env.example");
309
404
  const secret = this.generateAuthSecret();
310
- if (await fs3.pathExists(envPath)) {
311
- let content = await fs3.readFile(envPath, "utf-8");
405
+ if (await fs4.pathExists(envPath)) {
406
+ let content = await fs4.readFile(envPath, "utf-8");
312
407
  content = content.replace(
313
408
  /BETTER_AUTH_SECRET=.*/,
314
409
  `BETTER_AUTH_SECRET=${secret}`
315
410
  );
316
- await fs3.writeFile(envPath, content);
411
+ await fs4.writeFile(envPath, content);
317
412
  }
318
413
  }
319
414
  /**
320
415
  * Configure the Better Auth organizations plugin.
321
416
  * When enabled, replaces placeholders with organization import and plugin.
322
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`.
323
420
  */
324
421
  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");
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");
328
425
  if (enabled) {
329
426
  content = content.replace(
330
427
  "// {{ORGANIZATIONS_IMPORT}}",
@@ -338,10 +435,12 @@ BETTER_AUTH_SECRET=""
338
435
  content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
339
436
  content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
340
437
  }
341
- await fs3.writeFile(pluginsPath, content);
438
+ await fs4.writeFile(pluginsPath, content);
342
439
  }
343
440
  /**
344
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).
345
444
  */
346
445
  async initProject(options) {
347
446
  const copySpinner = ora("Copying template files...").start();
@@ -439,10 +538,20 @@ BETTER_AUTH_SECRET=""
439
538
  secretSpinner.fail("Failed to generate auth secret");
440
539
  throw error;
441
540
  }
442
- if (options.initGit) {
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) {
443
552
  await this.initGitRepo();
444
553
  }
445
- if (options.installDependencies) {
554
+ if (options.installDependencies && !options.monorepo) {
446
555
  await this.installDependencies();
447
556
  await this.lintCode();
448
557
  await this.formatCode();
@@ -454,7 +563,7 @@ BETTER_AUTH_SECRET=""
454
563
  import { join as join2 } from "path";
455
564
 
456
565
  // src/installers/string-utils.ts
457
- import fs4 from "fs-extra";
566
+ import fs5 from "fs-extra";
458
567
 
459
568
  // src/installers/providers.ts
460
569
  var OAUTH_PROVIDERS = {
@@ -2057,7 +2166,7 @@ function detectIndentation(line) {
2057
2166
  return match ? match[1] : "";
2058
2167
  }
2059
2168
  async function replacePlaceholder(filePath, placeholder, content, options) {
2060
- const fileContent = await fs4.readFile(filePath, "utf-8");
2169
+ const fileContent = await fs5.readFile(filePath, "utf-8");
2061
2170
  if (!fileContent.includes(placeholder)) {
2062
2171
  if (options?.graceful) {
2063
2172
  console.warn(
@@ -2071,7 +2180,7 @@ async function replacePlaceholder(filePath, placeholder, content, options) {
2071
2180
  }
2072
2181
  if (options?.inline) {
2073
2182
  const updatedContent = fileContent.replace(placeholder, content);
2074
- await fs4.writeFile(filePath, updatedContent, "utf-8");
2183
+ await fs5.writeFile(filePath, updatedContent, "utf-8");
2075
2184
  return;
2076
2185
  }
2077
2186
  const lines = fileContent.split("\n");
@@ -2093,7 +2202,7 @@ async function replacePlaceholder(filePath, placeholder, content, options) {
2093
2202
  updatedLines.push(line);
2094
2203
  }
2095
2204
  }
2096
- await fs4.writeFile(filePath, updatedLines.join("\n"), "utf-8");
2205
+ await fs5.writeFile(filePath, updatedLines.join("\n"), "utf-8");
2097
2206
  }
2098
2207
  function generateCredentialsValue(enabled) {
2099
2208
  return `credentials={${enabled}}`;
@@ -2226,12 +2335,20 @@ ${sections.join("\n\n---\n\n")}`;
2226
2335
 
2227
2336
  // src/installers/nextjs.ts
2228
2337
  var VexNextJSInstaller = class extends VexFrameworkInstaller {
2338
+ /**
2339
+ * Framework identifier used for template selection and logging.
2340
+ *
2341
+ * @returns The literal string `"nextjs"`.
2342
+ */
2229
2343
  get frameworkName() {
2230
2344
  return "nextjs";
2231
2345
  }
2232
2346
  /**
2233
2347
  * Update OAuth configuration in Convex auth options file
2234
2348
  * Target file: convex/auth/options.ts
2349
+ *
2350
+ * @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
2351
+ * @param emailPasswordEnabled - Whether email/password authentication is also enabled.
2235
2352
  */
2236
2353
  async updateOAuthConfig(selectedProviders, emailPasswordEnabled) {
2237
2354
  const authFilePath = join2(this.targetPath, "convex/auth/options.ts");
@@ -2254,6 +2371,9 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
2254
2371
  /**
2255
2372
  * Update OAuth UI configuration in auth client file
2256
2373
  * Target file: src/auth/client.tsx
2374
+ *
2375
+ * @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
2376
+ * @param emailPasswordEnabled - Whether email/password authentication is also enabled.
2257
2377
  */
2258
2378
  async updateOAuthUIConfig(selectedProviders, emailPasswordEnabled) {
2259
2379
  const providersFilePath = join2(this.targetPath, "src/auth/client.tsx");
@@ -2273,6 +2393,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
2273
2393
  /**
2274
2394
  * Update .env.example with OAuth environment variables
2275
2395
  * Target file: .env.example
2396
+ *
2397
+ * @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
2276
2398
  */
2277
2399
  async updateEnvExample(selectedProviders) {
2278
2400
  const envFilePath = join2(this.targetPath, ".env.example");
@@ -2286,6 +2408,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
2286
2408
  /**
2287
2409
  * Update README with OAuth provider setup guides
2288
2410
  * Target file: README.md
2411
+ *
2412
+ * @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
2289
2413
  */
2290
2414
  async updateReadme(selectedProviders) {
2291
2415
  const readmeFilePath = join2(this.targetPath, "README.md");
@@ -2300,6 +2424,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
2300
2424
  /**
2301
2425
  * Update env.mjs with OAuth provider environment variables
2302
2426
  * Target file: src/env.mjs
2427
+ *
2428
+ * @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
2303
2429
  */
2304
2430
  async updateEnvTs(selectedProviders) {
2305
2431
  const envFilePath = join2(this.targetPath, "src/env.mjs");
@@ -2331,7 +2457,7 @@ function createInstaller(props) {
2331
2457
  }
2332
2458
 
2333
2459
  // 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();
2460
+ 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
2461
  var args = program.args;
2336
2462
  var opts = program.opts();
2337
2463
  async function main() {
@@ -2339,15 +2465,19 @@ async function main() {
2339
2465
  console.log(chalk2.bold(" create-vexcms"));
2340
2466
  console.log();
2341
2467
  const bare = opts.bare ?? false;
2468
+ const yes = opts.yes ?? false;
2469
+ const monorepo = opts.monorepo ?? false;
2342
2470
  let inputArg;
2343
2471
  if (args[0]) {
2344
2472
  inputArg = args[0];
2473
+ } else if (yes) {
2474
+ inputArg = "my-vexcms-app";
2345
2475
  } else {
2346
2476
  inputArg = await input({
2347
2477
  message: "(1/8) What is your project named?",
2348
2478
  default: "my-vexcms-app",
2349
2479
  validate: (value) => {
2350
- const name = value.includes("/") ? path2.basename(value) : value;
2480
+ const name = value.includes("/") ? path3.basename(value) : value;
2351
2481
  if (name === ".") return true;
2352
2482
  const result = validateProjectName(name);
2353
2483
  if (result.valid) return true;
@@ -2356,52 +2486,80 @@ async function main() {
2356
2486
  });
2357
2487
  }
2358
2488
  inputArg = resolveProjectName(inputArg, process.cwd());
2359
- const projectName = inputArg === "." ? path2.basename(process.cwd()) : path2.basename(inputArg);
2489
+ const projectName = inputArg === "." ? path3.basename(process.cwd()) : path3.basename(inputArg);
2360
2490
  const validation = validateProjectName(projectName);
2361
2491
  if (!validation.valid) {
2362
2492
  displayInvalidNameError(projectName, validation.errors);
2363
2493
  process.exit(1);
2364
2494
  }
2365
- const targetDir = inputArg === "." ? process.cwd() : path2.resolve(process.cwd(), inputArg);
2366
- if (await checkDirectoryExists(targetDir)) {
2367
- if (!await isDirectoryEmpty(targetDir)) {
2368
- displayDirectoryNotEmptyError();
2495
+ let targetDir;
2496
+ let workspaceRoot = null;
2497
+ if (monorepo) {
2498
+ workspaceRoot = await findWorkspaceRoot({ cwd: process.cwd() });
2499
+ if (!workspaceRoot) {
2500
+ console.error(chalk2.red("\nError: --monorepo requires a pnpm workspace."));
2501
+ console.error(chalk2.yellow(`No pnpm-workspace.yaml was found walking up from ${process.cwd()}.`));
2502
+ console.error(chalk2.yellow("Run this command from inside a pnpm workspace, or drop --monorepo."));
2503
+ process.exit(1);
2504
+ }
2505
+ targetDir = path3.join(workspaceRoot, "apps", projectName);
2506
+ if (await checkDirectoryExists(targetDir)) {
2507
+ console.error(chalk2.red(`
2508
+ Error: 'apps/${projectName}' already exists under ${workspaceRoot}.`));
2509
+ console.error(chalk2.yellow("--monorepo refuses to scaffold over an existing directory."));
2369
2510
  process.exit(1);
2370
2511
  }
2512
+ } else {
2513
+ targetDir = inputArg === "." ? process.cwd() : path3.resolve(process.cwd(), inputArg);
2514
+ if (await checkDirectoryExists(targetDir)) {
2515
+ if (!await isDirectoryEmpty(targetDir)) {
2516
+ displayDirectoryNotEmptyError();
2517
+ process.exit(1);
2518
+ }
2519
+ }
2371
2520
  }
2372
2521
  let framework;
2373
- while (true) {
2374
- framework = await select({
2375
- message: "(2/8) Select a framework:",
2376
- choices: [
2377
- { name: "Next.js (Recommended)", value: "nextjs" },
2378
- { name: "TanStack Start (Coming Soon)", value: "tanstack" }
2379
- ]
2380
- });
2381
- if (framework === "tanstack") {
2382
- console.log(chalk2.yellow("\n TanStack Start support is coming soon! Please select Next.js for now.\n"));
2383
- continue;
2522
+ if (yes) {
2523
+ framework = "nextjs";
2524
+ } else {
2525
+ while (true) {
2526
+ framework = await select({
2527
+ message: "(2/8) Select a framework:",
2528
+ choices: [
2529
+ { name: "Next.js (Recommended)", value: "nextjs" },
2530
+ { name: "TanStack Start (Coming Soon)", value: "tanstack" }
2531
+ ]
2532
+ });
2533
+ if (framework === "tanstack") {
2534
+ console.log(chalk2.yellow("\n TanStack Start support is coming soon! Please select Next.js for now.\n"));
2535
+ continue;
2536
+ }
2537
+ break;
2384
2538
  }
2385
- break;
2386
2539
  }
2387
- const portInput = await input({
2388
- message: "(3/8) Dev server port:",
2389
- default: "3010",
2390
- validate: (value) => {
2391
- const num = parseInt(value, 10);
2392
- if (isNaN(num) || num < 1 || num > 65535) return "Must be a valid port number (1-65535)";
2393
- return true;
2394
- }
2395
- });
2396
- const port = parseInt(portInput, 10);
2397
- const emailPasswordAuth = await confirm({
2540
+ let port;
2541
+ if (yes) {
2542
+ port = 3010;
2543
+ } else {
2544
+ const portInput = await input({
2545
+ message: "(3/8) Dev server port:",
2546
+ default: "3010",
2547
+ validate: (value) => {
2548
+ const num = parseInt(value, 10);
2549
+ if (isNaN(num) || num < 1 || num > 65535) return "Must be a valid port number (1-65535)";
2550
+ return true;
2551
+ }
2552
+ });
2553
+ port = parseInt(portInput, 10);
2554
+ }
2555
+ const emailPasswordAuth = yes ? true : await confirm({
2398
2556
  message: "(4/8) Enable email/password authentication?",
2399
2557
  default: true
2400
2558
  });
2401
- const orgs = opts.orgs ?? await confirm({
2559
+ const orgs = opts.orgs ?? (yes ? false : await confirm({
2402
2560
  message: "(5/8) Enable multi-tenant (organizations)?",
2403
2561
  default: false
2404
- });
2562
+ }));
2405
2563
  const popularProviders = getPopularProviders();
2406
2564
  const additionalProviders = getAdditionalProviders();
2407
2565
  const allProviderChoices = [
@@ -2415,15 +2573,15 @@ async function main() {
2415
2573
  value: p.id
2416
2574
  }))
2417
2575
  ];
2418
- const oauthProviders = await checkbox({
2576
+ const oauthProviders = yes ? [] : await checkbox({
2419
2577
  message: "(6/8) Select OAuth providers (space to toggle, enter to confirm):",
2420
2578
  choices: allProviderChoices
2421
2579
  });
2422
- const initGit = await confirm({
2580
+ const initGit = yes ? true : await confirm({
2423
2581
  message: "(7/8) Initialize a Git repository?",
2424
2582
  default: true
2425
2583
  });
2426
- const installDependencies = await confirm({
2584
+ const installDependencies = yes ? false : await confirm({
2427
2585
  message: "(8/8) Install dependencies?",
2428
2586
  default: false
2429
2587
  });
@@ -2437,9 +2595,12 @@ async function main() {
2437
2595
  emailPasswordAuth,
2438
2596
  oauthProviders,
2439
2597
  initGit,
2440
- installDependencies
2598
+ installDependencies,
2599
+ monorepo,
2600
+ workspaceRoot,
2601
+ yes
2441
2602
  };
2442
- await fs5.ensureDir(targetDir);
2603
+ await fs6.ensureDir(targetDir);
2443
2604
  const installer = createInstaller({
2444
2605
  framework,
2445
2606
  projectDir: targetDir,
@@ -2449,6 +2610,10 @@ async function main() {
2449
2610
  await installer.initProject(options);
2450
2611
  console.log();
2451
2612
  displaySuccessMessage(options.projectName, targetDir, inputArg === ".");
2613
+ if (monorepo && workspaceRoot) {
2614
+ console.log(chalk2.cyan(` Run 'pnpm install' from ${workspaceRoot} to link the new workspace member.`));
2615
+ console.log();
2616
+ }
2452
2617
  }
2453
2618
  main().catch((error) => {
2454
2619
  console.error(chalk2.red("\nAn error occurred:"), error.message);