create-kofi-stack 1.1.0 → 1.2.1

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 (2) hide show
  1. package/dist/index.js +436 -112
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc3 from "picocolors";
5
+ import pc4 from "picocolors";
6
6
  import gradient from "gradient-string";
7
7
 
8
8
  // src/prompts/index.ts
@@ -713,9 +713,9 @@ ${extras.length > 0 ? `${pc.bold("Extras:")} ${extras.join(", ")}` : ""}`,
713
713
  }
714
714
 
715
715
  // src/generators/index.ts
716
- import path19 from "path";
717
- import * as p6 from "@clack/prompts";
718
- import pc2 from "picocolors";
716
+ import path20 from "path";
717
+ import * as p7 from "@clack/prompts";
718
+ import pc3 from "picocolors";
719
719
  import ora from "ora";
720
720
 
721
721
  // src/utils/fs.ts
@@ -735,6 +735,9 @@ async function writeJSON(filePath, data) {
735
735
  async function pathExists(filePath) {
736
736
  return fs.pathExists(filePath);
737
737
  }
738
+ async function readFile(filePath) {
739
+ return fs.readFile(filePath, "utf-8");
740
+ }
738
741
 
739
742
  // src/utils/git.ts
740
743
  import { execa } from "execa";
@@ -808,19 +811,19 @@ async function generatePackageJson(config, appDir) {
808
811
  next: "^16.0.0",
809
812
  react: "^19.0.0",
810
813
  "react-dom": "^19.0.0",
811
- "@convex-dev/better-auth": "^0.10.0",
812
- "better-auth": "^1.2.0",
813
- convex: "^1.17.0",
814
+ "@convex-dev/better-auth": "^0.10.9",
815
+ "better-auth": "^1.4.0",
816
+ convex: "^1.25.0",
814
817
  "@t3-oss/env-nextjs": "^0.11.0",
815
- zod: "^3.23.0",
818
+ zod: "^3.25.0",
816
819
  "date-fns": "^4.0.0",
817
820
  clsx: "^2.1.0",
818
821
  "tailwind-merge": "^2.5.0",
819
822
  "@hugeicons/react": "^0.3.0",
820
- "@base-ui-components/react": "^1.0.0-rc.0",
823
+ "@base-ui/react": "^1.0.0",
821
824
  "class-variance-authority": "^0.7.0",
822
825
  resend: "^4.0.0",
823
- "@react-email/components": "^0.0.25"
826
+ "@react-email/components": "^0.0.31"
824
827
  };
825
828
  const devDependencies = {
826
829
  typescript: "^5.0.0",
@@ -1992,8 +1995,8 @@ async function generateConvexPackageJson(convexDir) {
1992
1995
  main: "./convex/index.ts",
1993
1996
  types: "./convex/index.ts",
1994
1997
  dependencies: {
1995
- convex: "^1.17.0",
1996
- "@convex-dev/better-auth": "^0.10.0"
1998
+ convex: "^1.25.0",
1999
+ "@convex-dev/better-auth": "^0.10.9"
1997
2000
  },
1998
2001
  devDependencies: {
1999
2002
  typescript: "^5.0.0"
@@ -3151,7 +3154,7 @@ async function generateUIPackage(config, targetDir) {
3151
3154
  typecheck: "tsc --noEmit"
3152
3155
  },
3153
3156
  dependencies: {
3154
- "@base-ui-components/react": "^1.0.0-rc.0",
3157
+ "@base-ui/react": "^1.0.0",
3155
3158
  "@hugeicons/react": "^0.3.0",
3156
3159
  "class-variance-authority": "^0.7.0",
3157
3160
  clsx: "^2.1.0",
@@ -4114,17 +4117,23 @@ async function generatePayloadTsConfig(marketingDir) {
4114
4117
  await writeJSON(path15.join(marketingDir, "tsconfig.json"), tsConfig);
4115
4118
  }
4116
4119
  async function generatePayloadEnv(marketingDir) {
4117
- const envContent = `# Database
4120
+ const envContent = `# Database (Supabase PostgreSQL)
4118
4121
  DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres
4119
4122
 
4120
- # Payload
4121
- PAYLOAD_SECRET=
4123
+ # Payload CMS
4124
+ PAYLOAD_SECRET= # Generate with: openssl rand -base64 32
4122
4125
 
4123
- # S3 Storage (Supabase)
4124
- S3_BUCKET=
4126
+ # Scheduled Jobs
4127
+ CRON_SECRET= # Generate with: openssl rand -base64 32
4128
+
4129
+ # Draft Previews
4130
+ PREVIEW_SECRET= # Generate with: openssl rand -base64 32
4131
+
4132
+ # S3 Storage (Supabase Storage)
4133
+ S3_BUCKET=media
4125
4134
  S3_ACCESS_KEY_ID=
4126
4135
  S3_SECRET_ACCESS_KEY=
4127
- S3_REGION=
4136
+ S3_REGION=auto
4128
4137
  S3_ENDPOINT=https://[PROJECT].supabase.co/storage/v1/s3
4129
4138
  `;
4130
4139
  await writeFile(path15.join(marketingDir, ".env.example"), envContent);
@@ -4158,12 +4167,12 @@ async function generateFumadocsPackageJson(docsDir) {
4158
4167
  typecheck: "tsc --noEmit"
4159
4168
  },
4160
4169
  dependencies: {
4161
- next: "^15.0.0",
4170
+ next: "^16.0.0",
4162
4171
  react: "^19.0.0",
4163
4172
  "react-dom": "^19.0.0",
4164
- "fumadocs-core": "^14.0.0",
4165
- "fumadocs-mdx": "^10.0.0",
4166
- "fumadocs-ui": "^14.0.0",
4173
+ "fumadocs-core": "^16.0.0",
4174
+ "fumadocs-mdx": "^14.0.0",
4175
+ "fumadocs-ui": "^16.0.0",
4167
4176
  "@repo/ui": "workspace:*"
4168
4177
  },
4169
4178
  devDependencies: {
@@ -4341,48 +4350,11 @@ export default function HomePage() {
4341
4350
  }
4342
4351
  `;
4343
4352
  await writeFile(path16.join(docsDir, "src/app/page.tsx"), homePageContent);
4344
- const globalCssContent = `@import 'fumadocs-ui/style.css';
4345
- @import "tailwindcss";
4346
-
4347
- :root {
4348
- --fd-background: 0 0% 100%;
4349
- --fd-foreground: 0 0% 3.9%;
4350
- --fd-muted: 0 0% 96.1%;
4351
- --fd-muted-foreground: 0 0% 45.1%;
4352
- --fd-popover: 0 0% 100%;
4353
- --fd-popover-foreground: 0 0% 3.9%;
4354
- --fd-card: 0 0% 100%;
4355
- --fd-card-foreground: 0 0% 3.9%;
4356
- --fd-border: 0 0% 89.8%;
4357
- --fd-input: 0 0% 89.8%;
4358
- --fd-primary: 0 0% 9%;
4359
- --fd-primary-foreground: 0 0% 98%;
4360
- --fd-secondary: 0 0% 96.1%;
4361
- --fd-secondary-foreground: 0 0% 9%;
4362
- --fd-accent: 0 0% 96.1%;
4363
- --fd-accent-foreground: 0 0% 9%;
4364
- --fd-ring: 0 0% 63.9%;
4365
- }
4353
+ const globalCssContent = `@import 'tailwindcss';
4354
+ @import 'fumadocs-ui/css/neutral.css';
4355
+ @import 'fumadocs-ui/css/preset.css';
4366
4356
 
4367
- .dark {
4368
- --fd-background: 0 0% 3.9%;
4369
- --fd-foreground: 0 0% 98%;
4370
- --fd-muted: 0 0% 14.9%;
4371
- --fd-muted-foreground: 0 0% 63.9%;
4372
- --fd-popover: 0 0% 3.9%;
4373
- --fd-popover-foreground: 0 0% 98%;
4374
- --fd-card: 0 0% 3.9%;
4375
- --fd-card-foreground: 0 0% 98%;
4376
- --fd-border: 0 0% 14.9%;
4377
- --fd-input: 0 0% 14.9%;
4378
- --fd-primary: 0 0% 98%;
4379
- --fd-primary-foreground: 0 0% 9%;
4380
- --fd-secondary: 0 0% 14.9%;
4381
- --fd-secondary-foreground: 0 0% 98%;
4382
- --fd-accent: 0 0% 14.9%;
4383
- --fd-accent-foreground: 0 0% 98%;
4384
- --fd-ring: 0 0% 14.9%;
4385
- }
4357
+ @source '../node_modules/fumadocs-ui/dist/**/*.js';
4386
4358
  `;
4387
4359
  await writeFile(path16.join(docsDir, "src/app/global.css"), globalCssContent);
4388
4360
  const sourceConfigContent = `import { defineCollections, defineConfig } from 'fumadocs-mdx/config'
@@ -4508,7 +4480,7 @@ ${config.name} uses Better-Auth with Convex for authentication.
4508
4480
 
4509
4481
  - Email/Password (always enabled)
4510
4482
  - Google OAuth (always enabled)
4511
- ${config.auth.providers.map((p7) => `- ${p7.charAt(0).toUpperCase() + p7.slice(1)}`).join("\n")}
4483
+ ${config.auth.providers.map((p8) => `- ${p8.charAt(0).toUpperCase() + p8.slice(1)}`).join("\n")}
4512
4484
 
4513
4485
  ## Configuration
4514
4486
 
@@ -6745,19 +6717,370 @@ export function cn(...inputs: ClassValue[]) {
6745
6717
  await writeFile(path18.join(appDir, "src/lib/utils.ts"), content);
6746
6718
  }
6747
6719
 
6720
+ // src/setup/env-wizard.ts
6721
+ import * as p6 from "@clack/prompts";
6722
+ import pc2 from "picocolors";
6723
+ import { exec } from "child_process";
6724
+ import { promisify } from "util";
6725
+ import path19 from "path";
6726
+ var execAsync = promisify(exec);
6727
+ async function runEnvSetupWizard(config) {
6728
+ console.log();
6729
+ p6.intro(pc2.bgMagenta(pc2.black(" Environment Setup ")));
6730
+ const shouldSetup = await p6.confirm({
6731
+ message: "Would you like to set up environment variables now?",
6732
+ initialValue: true
6733
+ });
6734
+ if (p6.isCancel(shouldSetup) || !shouldSetup) {
6735
+ p6.log.info("Skipping environment setup. You can configure .env.local manually later.");
6736
+ return;
6737
+ }
6738
+ const envValues = {};
6739
+ await setupSecrets(config, envValues);
6740
+ await setupConvex(config, envValues);
6741
+ if (config.marketingSite === "payload") {
6742
+ await setupSupabase(config, envValues);
6743
+ }
6744
+ await showOAuthSetupGuide(config);
6745
+ await showRemainingEnvGuide(config);
6746
+ await writeEnvFiles(config, envValues);
6747
+ p6.outro(pc2.green("Environment setup complete!"));
6748
+ }
6749
+ async function setupSecrets(config, envValues) {
6750
+ p6.log.step("Generating secure secrets...");
6751
+ try {
6752
+ const { stdout: authSecret } = await execAsync("openssl rand -base64 32");
6753
+ envValues.BETTER_AUTH_SECRET = authSecret.trim();
6754
+ p6.log.success("Generated BETTER_AUTH_SECRET");
6755
+ if (config.marketingSite === "payload") {
6756
+ const { stdout: payloadSecret } = await execAsync("openssl rand -base64 32");
6757
+ envValues.PAYLOAD_SECRET = payloadSecret.trim();
6758
+ p6.log.success("Generated PAYLOAD_SECRET");
6759
+ const { stdout: cronSecret } = await execAsync("openssl rand -base64 32");
6760
+ envValues.CRON_SECRET = cronSecret.trim();
6761
+ p6.log.success("Generated CRON_SECRET");
6762
+ const { stdout: previewSecret } = await execAsync("openssl rand -base64 32");
6763
+ envValues.PREVIEW_SECRET = previewSecret.trim();
6764
+ p6.log.success("Generated PREVIEW_SECRET");
6765
+ }
6766
+ } catch (error) {
6767
+ p6.log.warn("Could not generate secrets automatically. Please generate them manually:");
6768
+ p6.log.info("Run: openssl rand -base64 32");
6769
+ }
6770
+ }
6771
+ async function setupConvex(config, envValues) {
6772
+ console.log();
6773
+ p6.log.step("Convex Setup");
6774
+ const convexChoice = await p6.select({
6775
+ message: "How would you like to set up Convex?",
6776
+ options: [
6777
+ {
6778
+ value: "new",
6779
+ label: "Create a new Convex project",
6780
+ hint: "Will open Convex dashboard in browser"
6781
+ },
6782
+ {
6783
+ value: "existing",
6784
+ label: "Connect to an existing Convex project",
6785
+ hint: "Enter your Convex deployment URL"
6786
+ },
6787
+ {
6788
+ value: "skip",
6789
+ label: "Skip for now",
6790
+ hint: "Configure manually later"
6791
+ }
6792
+ ]
6793
+ });
6794
+ if (p6.isCancel(convexChoice) || convexChoice === "skip") {
6795
+ p6.log.info("Skipping Convex setup. Run `pnpm convex dev` to set up later.");
6796
+ return;
6797
+ }
6798
+ if (convexChoice === "new") {
6799
+ p6.log.info(`
6800
+ ${pc2.bold("To create a new Convex project:")}
6801
+
6802
+ 1. Run ${pc2.cyan("pnpm convex dev")} in your project directory
6803
+ 2. It will open your browser to create a new project
6804
+ 3. Follow the prompts to set up your Convex project
6805
+ 4. The CLI will automatically add the deployment URL to .env.local
6806
+
6807
+ ${pc2.dim("Note: Make sure you have a Convex account at https://convex.dev")}
6808
+ `);
6809
+ } else if (convexChoice === "existing") {
6810
+ const deploymentName = await p6.text({
6811
+ message: "Enter your Convex deployment name (e.g., my-app-123):",
6812
+ placeholder: "my-app-123",
6813
+ validate: (value) => {
6814
+ if (!value) return "Deployment name is required";
6815
+ return void 0;
6816
+ }
6817
+ });
6818
+ if (!p6.isCancel(deploymentName)) {
6819
+ envValues.CONVEX_DEPLOYMENT = `prod:${deploymentName}`;
6820
+ envValues.NEXT_PUBLIC_CONVEX_URL = `https://${deploymentName}.convex.cloud`;
6821
+ p6.log.success("Convex deployment configured");
6822
+ }
6823
+ }
6824
+ }
6825
+ async function setupSupabase(config, envValues) {
6826
+ console.log();
6827
+ p6.log.step("Supabase Database Setup (for Payload CMS)");
6828
+ const supabaseChoice = await p6.select({
6829
+ message: "How would you like to set up Supabase?",
6830
+ options: [
6831
+ {
6832
+ value: "guide",
6833
+ label: "Show setup guide",
6834
+ hint: "Step-by-step instructions"
6835
+ },
6836
+ {
6837
+ value: "existing",
6838
+ label: "Enter existing Supabase credentials",
6839
+ hint: "I already have a Supabase project"
6840
+ },
6841
+ {
6842
+ value: "skip",
6843
+ label: "Skip for now",
6844
+ hint: "Configure manually later"
6845
+ }
6846
+ ]
6847
+ });
6848
+ if (p6.isCancel(supabaseChoice) || supabaseChoice === "skip") {
6849
+ p6.log.info("Skipping Supabase setup. Configure DATABASE_URL in .env.local later.");
6850
+ return;
6851
+ }
6852
+ if (supabaseChoice === "guide") {
6853
+ p6.log.info(`
6854
+ ${pc2.bold("To set up Supabase for Payload CMS:")}
6855
+
6856
+ 1. Go to ${pc2.cyan("https://supabase.com")} and create/sign in to your account
6857
+ 2. Create a new project (or select an existing one)
6858
+ 3. Go to ${pc2.bold("Project Settings > Database")}
6859
+ 4. Under ${pc2.bold("Connection string")}, select ${pc2.bold("URI")} tab
6860
+ 5. Copy the connection string (starts with postgresql://)
6861
+ 6. Replace ${pc2.dim("[YOUR-PASSWORD]")} with your database password
6862
+
6863
+ ${pc2.bold("For S3 Storage (images/media):")}
6864
+ 1. Go to ${pc2.bold("Project Settings > Storage")}
6865
+ 2. Create a new bucket called "media"
6866
+ 3. Go to ${pc2.bold("Project Settings > API")}
6867
+ 4. Copy the ${pc2.bold("service_role key")} for S3_SECRET_ACCESS_KEY
6868
+ 5. Note your project reference for the S3 endpoint
6869
+
6870
+ ${pc2.dim("Add these to your apps/marketing/.env.local file")}
6871
+ `);
6872
+ const enterNow = await p6.confirm({
6873
+ message: "Would you like to enter your Supabase credentials now?",
6874
+ initialValue: false
6875
+ });
6876
+ if (!p6.isCancel(enterNow) && enterNow) {
6877
+ await collectSupabaseCredentials(envValues);
6878
+ }
6879
+ } else if (supabaseChoice === "existing") {
6880
+ await collectSupabaseCredentials(envValues);
6881
+ }
6882
+ }
6883
+ async function collectSupabaseCredentials(envValues) {
6884
+ const databaseUrl = await p6.text({
6885
+ message: "Enter your DATABASE_URL (PostgreSQL connection string):",
6886
+ placeholder: "postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres",
6887
+ validate: (value) => {
6888
+ if (!value) return "Database URL is required";
6889
+ if (!value.startsWith("postgresql://")) return "Must be a PostgreSQL connection string";
6890
+ return void 0;
6891
+ }
6892
+ });
6893
+ if (!p6.isCancel(databaseUrl)) {
6894
+ envValues.DATABASE_URL = databaseUrl;
6895
+ p6.log.success("Database URL configured");
6896
+ }
6897
+ const setupS3 = await p6.confirm({
6898
+ message: "Would you like to configure S3 storage for media uploads?",
6899
+ initialValue: false
6900
+ });
6901
+ if (!p6.isCancel(setupS3) && setupS3) {
6902
+ const projectRef = await p6.text({
6903
+ message: "Enter your Supabase project reference (e.g., abcdefghijklmnop):",
6904
+ placeholder: "abcdefghijklmnop"
6905
+ });
6906
+ if (!p6.isCancel(projectRef)) {
6907
+ envValues.S3_ENDPOINT = `https://${projectRef}.supabase.co/storage/v1/s3`;
6908
+ envValues.S3_REGION = "auto";
6909
+ envValues.S3_BUCKET = "media";
6910
+ const accessKeyId = await p6.text({
6911
+ message: "Enter your S3 Access Key ID (from Supabase API settings):",
6912
+ placeholder: "your-access-key-id"
6913
+ });
6914
+ if (!p6.isCancel(accessKeyId)) {
6915
+ envValues.S3_ACCESS_KEY_ID = accessKeyId;
6916
+ }
6917
+ const secretKey = await p6.password({
6918
+ message: "Enter your S3 Secret Access Key (service_role key):"
6919
+ });
6920
+ if (!p6.isCancel(secretKey)) {
6921
+ envValues.S3_SECRET_ACCESS_KEY = secretKey;
6922
+ }
6923
+ p6.log.success("S3 storage configured");
6924
+ }
6925
+ }
6926
+ }
6927
+ async function showOAuthSetupGuide(config) {
6928
+ console.log();
6929
+ p6.log.step("OAuth Provider Setup");
6930
+ const allProviders = ["google", ...config.auth.providers];
6931
+ p6.log.info(`
6932
+ ${pc2.bold("Configure OAuth providers for authentication:")}
6933
+
6934
+ ${pc2.cyan("Google OAuth")} (required):
6935
+ 1. Go to ${pc2.dim("https://console.cloud.google.com")}
6936
+ 2. Create a new project or select existing
6937
+ 3. Go to APIs & Services > Credentials
6938
+ 4. Create OAuth 2.0 Client ID (Web application)
6939
+ 5. Add authorized redirect: ${pc2.dim("http://localhost:3000/api/auth/callback/google")}
6940
+ 6. Copy Client ID and Client Secret to .env.local
6941
+ `);
6942
+ for (const provider of config.auth.providers) {
6943
+ const guides = {
6944
+ github: `${pc2.cyan("GitHub OAuth")}:
6945
+ 1. Go to ${pc2.dim("https://github.com/settings/developers")}
6946
+ 2. Create a new OAuth App
6947
+ 3. Set callback URL: ${pc2.dim("http://localhost:3000/api/auth/callback/github")}`,
6948
+ discord: `${pc2.cyan("Discord OAuth")}:
6949
+ 1. Go to ${pc2.dim("https://discord.com/developers/applications")}
6950
+ 2. Create a new application
6951
+ 3. Go to OAuth2 and add redirect: ${pc2.dim("http://localhost:3000/api/auth/callback/discord")}`,
6952
+ twitter: `${pc2.cyan("Twitter/X OAuth")}:
6953
+ 1. Go to ${pc2.dim("https://developer.twitter.com/en/portal/dashboard")}
6954
+ 2. Create a project and app
6955
+ 3. Set callback URL: ${pc2.dim("http://localhost:3000/api/auth/callback/twitter")}`,
6956
+ apple: `${pc2.cyan("Apple OAuth")}:
6957
+ 1. Go to ${pc2.dim("https://developer.apple.com")}
6958
+ 2. Create an App ID and Service ID
6959
+ 3. Configure Sign in with Apple`,
6960
+ microsoft: `${pc2.cyan("Microsoft OAuth")}:
6961
+ 1. Go to ${pc2.dim("https://portal.azure.com")}
6962
+ 2. Register a new application in Azure AD
6963
+ 3. Add redirect: ${pc2.dim("http://localhost:3000/api/auth/callback/microsoft")}`,
6964
+ linkedin: `${pc2.cyan("LinkedIn OAuth")}:
6965
+ 1. Go to ${pc2.dim("https://www.linkedin.com/developers")}
6966
+ 2. Create a new app
6967
+ 3. Add redirect: ${pc2.dim("http://localhost:3000/api/auth/callback/linkedin")}`
6968
+ };
6969
+ if (guides[provider]) {
6970
+ console.log(guides[provider]);
6971
+ console.log();
6972
+ }
6973
+ }
6974
+ }
6975
+ async function showRemainingEnvGuide(config) {
6976
+ console.log();
6977
+ p6.log.step("Additional Configuration");
6978
+ const guides = [];
6979
+ guides.push(`${pc2.cyan("Resend (Email)")}:
6980
+ 1. Sign up at ${pc2.dim("https://resend.com")}
6981
+ 2. Create an API key
6982
+ 3. Add RESEND_API_KEY to .env.local`);
6983
+ if (config.integrations.analytics === "posthog") {
6984
+ guides.push(`${pc2.cyan("PostHog Analytics")}:
6985
+ 1. Sign up at ${pc2.dim("https://posthog.com")}
6986
+ 2. Create a project and get your API key
6987
+ 3. Add NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST`);
6988
+ }
6989
+ if (config.integrations.uploads === "uploadthing") {
6990
+ guides.push(`${pc2.cyan("Uploadthing")}:
6991
+ 1. Sign up at ${pc2.dim("https://uploadthing.com")}
6992
+ 2. Create an app and get your credentials
6993
+ 3. Add UPLOADTHING_SECRET and UPLOADTHING_APP_ID`);
6994
+ }
6995
+ if (config.integrations.uploads === "s3") {
6996
+ guides.push(`${pc2.cyan("AWS S3")}:
6997
+ 1. Create an S3 bucket in AWS Console
6998
+ 2. Create an IAM user with S3 access
6999
+ 3. Add AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BUCKET_NAME`);
7000
+ }
7001
+ if (config.integrations.rateLimiting === "arcjet") {
7002
+ guides.push(`${pc2.cyan("Arcjet")}:
7003
+ 1. Sign up at ${pc2.dim("https://arcjet.com")}
7004
+ 2. Create a site and get your API key
7005
+ 3. Add ARCJET_KEY to .env.local`);
7006
+ }
7007
+ if (config.integrations.rateLimiting === "upstash") {
7008
+ guides.push(`${pc2.cyan("Upstash Redis")}:
7009
+ 1. Sign up at ${pc2.dim("https://upstash.com")}
7010
+ 2. Create a Redis database
7011
+ 3. Add UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN`);
7012
+ }
7013
+ if (config.integrations.monitoring === "sentry") {
7014
+ guides.push(`${pc2.cyan("Sentry")}:
7015
+ 1. Sign up at ${pc2.dim("https://sentry.io")}
7016
+ 2. Create a project (Next.js)
7017
+ 3. Add SENTRY_DSN and SENTRY_AUTH_TOKEN`);
7018
+ }
7019
+ if (guides.length > 0) {
7020
+ p6.log.info(`
7021
+ ${pc2.bold("Configure these services to complete your setup:")}
7022
+
7023
+ ${guides.join("\n\n")}
7024
+ `);
7025
+ }
7026
+ }
7027
+ async function writeEnvFiles(config, envValues) {
7028
+ if (Object.keys(envValues).length === 0) {
7029
+ return;
7030
+ }
7031
+ p6.log.step("Writing environment files...");
7032
+ const webEnvPath = config.structure === "monorepo" ? path19.join(config.targetDir, "apps/web/.env.local") : path19.join(config.targetDir, ".env.local");
7033
+ if (await pathExists(webEnvPath)) {
7034
+ let content = await readFile(webEnvPath);
7035
+ for (const [key, value] of Object.entries(envValues)) {
7036
+ if (["PAYLOAD_SECRET", "DATABASE_URL", "S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "CRON_SECRET", "PREVIEW_SECRET"].includes(key)) {
7037
+ continue;
7038
+ }
7039
+ const regex = new RegExp(`^${key}=.*$`, "m");
7040
+ if (regex.test(content)) {
7041
+ content = content.replace(regex, `${key}=${value}`);
7042
+ } else {
7043
+ content += `
7044
+ ${key}=${value}`;
7045
+ }
7046
+ }
7047
+ await writeFile(webEnvPath, content);
7048
+ p6.log.success(`Updated ${config.structure === "monorepo" ? "apps/web/.env.local" : ".env.local"}`);
7049
+ }
7050
+ if (config.marketingSite === "payload" && config.structure === "monorepo") {
7051
+ const marketingEnvPath = path19.join(config.targetDir, "apps/marketing/.env.local");
7052
+ if (await pathExists(marketingEnvPath)) {
7053
+ let content = await readFile(marketingEnvPath);
7054
+ const payloadEnvKeys = ["PAYLOAD_SECRET", "DATABASE_URL", "S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "CRON_SECRET", "PREVIEW_SECRET"];
7055
+ for (const [key, value] of Object.entries(envValues)) {
7056
+ if (!payloadEnvKeys.includes(key)) continue;
7057
+ const regex = new RegExp(`^${key}=.*$`, "m");
7058
+ if (regex.test(content)) {
7059
+ content = content.replace(regex, `${key}=${value}`);
7060
+ } else {
7061
+ content += `
7062
+ ${key}=${value}`;
7063
+ }
7064
+ }
7065
+ await writeFile(marketingEnvPath, content);
7066
+ p6.log.success("Updated apps/marketing/.env.local");
7067
+ }
7068
+ }
7069
+ }
7070
+
6748
7071
  // src/generators/index.ts
6749
7072
  async function generateProject(config) {
6750
7073
  const spinner = ora();
6751
7074
  if (!await isPnpmInstalled()) {
6752
- p6.cancel("pnpm is not installed. Please install pnpm first: npm install -g pnpm");
7075
+ p7.cancel("pnpm is not installed. Please install pnpm first: npm install -g pnpm");
6753
7076
  process.exit(1);
6754
7077
  }
6755
7078
  if (await pathExists(config.targetDir)) {
6756
- p6.cancel(`Directory ${config.name} already exists`);
7079
+ p7.cancel(`Directory ${config.name} already exists`);
6757
7080
  process.exit(1);
6758
7081
  }
6759
7082
  console.log();
6760
- p6.log.info(`Creating project in ${pc2.cyan(config.targetDir)}`);
7083
+ p7.log.info(`Creating project in ${pc3.cyan(config.targetDir)}`);
6761
7084
  console.log();
6762
7085
  try {
6763
7086
  if (config.structure === "monorepo") {
@@ -6766,6 +7089,7 @@ async function generateProject(config) {
6766
7089
  await generateSingleProject(config, spinner);
6767
7090
  }
6768
7091
  await runPostGenerationSteps(config, spinner);
7092
+ await runEnvSetupWizard(config);
6769
7093
  displaySuccessMessage(config);
6770
7094
  } catch (error) {
6771
7095
  spinner.fail("An error occurred during project generation");
@@ -6785,7 +7109,7 @@ async function generateSingleProject(config, spinner) {
6785
7109
  await generateShadcn(config, targetDir);
6786
7110
  spinner.succeed("shadcn/ui configured");
6787
7111
  spinner.start("Setting up Convex...");
6788
- await generateConvex(config, path19.join(targetDir, "convex"));
7112
+ await generateConvex(config, path20.join(targetDir, "convex"));
6789
7113
  spinner.succeed("Convex configured");
6790
7114
  spinner.start("Configuring Better-Auth...");
6791
7115
  await generateBetterAuth(config, targetDir);
@@ -6828,7 +7152,7 @@ async function generateMonorepoProject(config, spinner) {
6828
7152
  spinner.start("Creating shared UI package...");
6829
7153
  await generateUIPackage(config, targetDir);
6830
7154
  spinner.succeed("Shared UI package created");
6831
- const webDir = path19.join(targetDir, "apps/web");
7155
+ const webDir = path20.join(targetDir, "apps/web");
6832
7156
  spinner.start("Generating web application...");
6833
7157
  await generateBaseNextjs(config, webDir);
6834
7158
  await generateTailwind(config, webDir);
@@ -6838,10 +7162,10 @@ async function generateMonorepoProject(config, spinner) {
6838
7162
  await generateEmail(config, webDir);
6839
7163
  await updateWebTsConfig(webDir);
6840
7164
  spinner.succeed("Web application generated");
6841
- const backendDir = path19.join(targetDir, "packages/backend");
7165
+ const backendDir = path20.join(targetDir, "packages/backend");
6842
7166
  spinner.start("Setting up Convex backend...");
6843
7167
  await ensureDir(backendDir);
6844
- await generateConvex(config, path19.join(backendDir, "convex"));
7168
+ await generateConvex(config, path20.join(backendDir, "convex"));
6845
7169
  spinner.succeed("Convex backend configured");
6846
7170
  if (config.integrations.analytics !== "none") {
6847
7171
  spinner.start(`Setting up ${config.integrations.analytics} analytics...`);
@@ -6864,7 +7188,7 @@ async function generateMonorepoProject(config, spinner) {
6864
7188
  spinner.succeed(`${config.integrations.monitoring} monitoring configured`);
6865
7189
  }
6866
7190
  if (config.marketingSite !== "none") {
6867
- const marketingDir = path19.join(targetDir, "apps/marketing");
7191
+ const marketingDir = path20.join(targetDir, "apps/marketing");
6868
7192
  spinner.start(`Generating ${config.marketingSite} marketing site...`);
6869
7193
  if (config.marketingSite === "payload") {
6870
7194
  await generatePayload(config, marketingDir);
@@ -6876,7 +7200,7 @@ async function generateMonorepoProject(config, spinner) {
6876
7200
  spinner.succeed(`${config.marketingSite} marketing site generated`);
6877
7201
  }
6878
7202
  if (config.includeDocs) {
6879
- const docsDir = path19.join(targetDir, "apps/docs");
7203
+ const docsDir = path20.join(targetDir, "apps/docs");
6880
7204
  spinner.start("Generating Fumadocs documentation site...");
6881
7205
  await generateFumadocs(config, docsDir);
6882
7206
  spinner.succeed("Fumadocs documentation site generated");
@@ -6897,7 +7221,7 @@ async function runPostGenerationSteps(config, spinner) {
6897
7221
  }
6898
7222
  spinner.start("Installing shadcn/ui components...");
6899
7223
  try {
6900
- const shadcnDir = config.structure === "monorepo" ? path19.join(targetDir, "packages/ui") : targetDir;
7224
+ const shadcnDir = config.structure === "monorepo" ? path20.join(targetDir, "packages/ui") : targetDir;
6901
7225
  await runShadcnAdd(shadcnDir);
6902
7226
  spinner.succeed("shadcn/ui components installed");
6903
7227
  } catch (error) {
@@ -6957,7 +7281,7 @@ async function generateBiomeConfig(targetDir) {
6957
7281
  }
6958
7282
  };
6959
7283
  await writeFile(
6960
- path19.join(targetDir, "biome.json"),
7284
+ path20.join(targetDir, "biome.json"),
6961
7285
  JSON.stringify(biomeJson, null, 2)
6962
7286
  );
6963
7287
  }
@@ -7008,13 +7332,13 @@ yarn-error.log*
7008
7332
  *.pem
7009
7333
  .cache
7010
7334
  `;
7011
- await writeFile(path19.join(targetDir, ".gitignore"), gitignoreContent);
7335
+ await writeFile(path20.join(targetDir, ".gitignore"), gitignoreContent);
7012
7336
  }
7013
7337
  async function generateHuskyHooks(targetDir) {
7014
7338
  const preCommitContent = `pnpm lint-staged
7015
7339
  `;
7016
- await ensureDir(path19.join(targetDir, ".husky"));
7017
- await writeFile(path19.join(targetDir, ".husky/pre-commit"), preCommitContent);
7340
+ await ensureDir(path20.join(targetDir, ".husky"));
7341
+ await writeFile(path20.join(targetDir, ".husky/pre-commit"), preCommitContent);
7018
7342
  }
7019
7343
  async function updateWebTsConfig(webDir) {
7020
7344
  const tsConfig = {
@@ -7028,14 +7352,14 @@ async function updateWebTsConfig(webDir) {
7028
7352
  exclude: ["node_modules"]
7029
7353
  };
7030
7354
  await writeFile(
7031
- path19.join(webDir, "tsconfig.json"),
7355
+ path20.join(webDir, "tsconfig.json"),
7032
7356
  JSON.stringify(tsConfig, null, 2)
7033
7357
  );
7034
7358
  }
7035
7359
  async function generateNextjsMarketing(config, marketingDir) {
7036
- await ensureDir(path19.join(marketingDir, "src/app"));
7037
- await ensureDir(path19.join(marketingDir, "src/components"));
7038
- await ensureDir(path19.join(marketingDir, "public"));
7360
+ await ensureDir(path20.join(marketingDir, "src/app"));
7361
+ await ensureDir(path20.join(marketingDir, "src/components"));
7362
+ await ensureDir(path20.join(marketingDir, "public"));
7039
7363
  const packageJson = {
7040
7364
  name: "@repo/marketing",
7041
7365
  version: "0.1.0",
@@ -7066,7 +7390,7 @@ async function generateNextjsMarketing(config, marketingDir) {
7066
7390
  }
7067
7391
  };
7068
7392
  await writeFile(
7069
- path19.join(marketingDir, "package.json"),
7393
+ path20.join(marketingDir, "package.json"),
7070
7394
  JSON.stringify(packageJson, null, 2)
7071
7395
  );
7072
7396
  const homePageContent = `export default function HomePage() {
@@ -7089,7 +7413,7 @@ async function generateNextjsMarketing(config, marketingDir) {
7089
7413
  }
7090
7414
  `;
7091
7415
  await writeFile(
7092
- path19.join(marketingDir, "src/app/page.tsx"),
7416
+ path20.join(marketingDir, "src/app/page.tsx"),
7093
7417
  homePageContent
7094
7418
  );
7095
7419
  const layoutContent = `import type { Metadata } from 'next'
@@ -7113,7 +7437,7 @@ export default function RootLayout({
7113
7437
  }
7114
7438
  `;
7115
7439
  await writeFile(
7116
- path19.join(marketingDir, "src/app/layout.tsx"),
7440
+ path20.join(marketingDir, "src/app/layout.tsx"),
7117
7441
  layoutContent
7118
7442
  );
7119
7443
  const globalsCss = `@import "tailwindcss";
@@ -7125,7 +7449,7 @@ export default function RootLayout({
7125
7449
  }
7126
7450
  `;
7127
7451
  await writeFile(
7128
- path19.join(marketingDir, "src/app/globals.css"),
7452
+ path20.join(marketingDir, "src/app/globals.css"),
7129
7453
  globalsCss
7130
7454
  );
7131
7455
  const tsConfig = {
@@ -7139,7 +7463,7 @@ export default function RootLayout({
7139
7463
  exclude: ["node_modules"]
7140
7464
  };
7141
7465
  await writeFile(
7142
- path19.join(marketingDir, "tsconfig.json"),
7466
+ path20.join(marketingDir, "tsconfig.json"),
7143
7467
  JSON.stringify(tsConfig, null, 2)
7144
7468
  );
7145
7469
  const nextConfig = `import type { NextConfig } from 'next'
@@ -7150,14 +7474,14 @@ const nextConfig: NextConfig = {
7150
7474
 
7151
7475
  export default nextConfig
7152
7476
  `;
7153
- await writeFile(path19.join(marketingDir, "next.config.ts"), nextConfig);
7477
+ await writeFile(path20.join(marketingDir, "next.config.ts"), nextConfig);
7154
7478
  const postcssConfig = `export default {
7155
7479
  plugins: {
7156
7480
  '@tailwindcss/postcss': {},
7157
7481
  },
7158
7482
  }
7159
7483
  `;
7160
- await writeFile(path19.join(marketingDir, "postcss.config.mjs"), postcssConfig);
7484
+ await writeFile(path20.join(marketingDir, "postcss.config.mjs"), postcssConfig);
7161
7485
  }
7162
7486
  function displaySuccessMessage(config) {
7163
7487
  const apps = ["web"];
@@ -7171,37 +7495,37 @@ function displaySuccessMessage(config) {
7171
7495
  apps.push("design-system");
7172
7496
  }
7173
7497
  console.log();
7174
- p6.outro(pc2.green("Project created successfully!"));
7498
+ p7.outro(pc3.green("Project created successfully!"));
7175
7499
  console.log();
7176
- console.log(pc2.bold("Next steps:"));
7500
+ console.log(pc3.bold("Next steps:"));
7177
7501
  console.log();
7178
- console.log(` ${pc2.cyan("cd")} ${config.name}`);
7502
+ console.log(` ${pc3.cyan("cd")} ${config.name}`);
7179
7503
  console.log();
7180
- console.log(` ${pc2.dim("# Set up your environment variables")}`);
7181
- console.log(` ${pc2.cyan("cp")} .env.example .env.local`);
7504
+ console.log(` ${pc3.dim("# Set up your environment variables")}`);
7505
+ console.log(` ${pc3.cyan("cp")} .env.example .env.local`);
7182
7506
  console.log();
7183
- console.log(` ${pc2.dim("# Start Convex development server")}`);
7184
- console.log(` ${pc2.cyan("pnpm")} convex dev`);
7507
+ console.log(` ${pc3.dim("# Start Convex development server")}`);
7508
+ console.log(` ${pc3.cyan("pnpm")} convex dev`);
7185
7509
  console.log();
7186
- console.log(` ${pc2.dim("# In another terminal, start the app")}`);
7187
- console.log(` ${pc2.cyan("pnpm")} dev`);
7510
+ console.log(` ${pc3.dim("# In another terminal, start the app")}`);
7511
+ console.log(` ${pc3.cyan("pnpm")} dev`);
7188
7512
  if (config.marketingSite === "payload") {
7189
7513
  console.log();
7190
- console.log(pc2.dim("# If using Payload CMS:"));
7191
- console.log(pc2.dim("# 1. Create a Supabase project at https://supabase.com"));
7192
- console.log(pc2.dim("# 2. Copy your database connection strings to .env.local"));
7193
- console.log(pc2.dim("# 3. Run migrations: pnpm --filter marketing db:push"));
7514
+ console.log(pc3.dim("# If using Payload CMS:"));
7515
+ console.log(pc3.dim("# 1. Create a Supabase project at https://supabase.com"));
7516
+ console.log(pc3.dim("# 2. Copy your database connection strings to .env.local"));
7517
+ console.log(pc3.dim("# 3. Run migrations: pnpm --filter marketing db:push"));
7194
7518
  }
7195
7519
  console.log();
7196
- console.log(pc2.bold("Documentation:"));
7197
- console.log(` ${pc2.dim("-")} Convex: ${pc2.cyan("https://docs.convex.dev")}`);
7198
- console.log(` ${pc2.dim("-")} Better-Auth: ${pc2.cyan("https://www.better-auth.com")}`);
7199
- console.log(` ${pc2.dim("-")} shadcn/ui: ${pc2.cyan("https://ui.shadcn.com")}`);
7520
+ console.log(pc3.bold("Documentation:"));
7521
+ console.log(` ${pc3.dim("-")} Convex: ${pc3.cyan("https://docs.convex.dev")}`);
7522
+ console.log(` ${pc3.dim("-")} Better-Auth: ${pc3.cyan("https://www.better-auth.com")}`);
7523
+ console.log(` ${pc3.dim("-")} shadcn/ui: ${pc3.cyan("https://ui.shadcn.com")}`);
7200
7524
  if (config.marketingSite === "payload") {
7201
- console.log(` ${pc2.dim("-")} Payload CMS: ${pc2.cyan("https://payloadcms.com/docs")}`);
7525
+ console.log(` ${pc3.dim("-")} Payload CMS: ${pc3.cyan("https://payloadcms.com/docs")}`);
7202
7526
  }
7203
7527
  if (config.includeDocs) {
7204
- console.log(` ${pc2.dim("-")} Fumadocs: ${pc2.cyan("https://fumadocs.vercel.app")}`);
7528
+ console.log(` ${pc3.dim("-")} Fumadocs: ${pc3.cyan("https://fumadocs.vercel.app")}`);
7205
7529
  }
7206
7530
  console.log();
7207
7531
  }
@@ -7219,7 +7543,7 @@ var kofiGradient = gradient(["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"]);
7219
7543
  function printBanner() {
7220
7544
  console.log(kofiGradient(BANNER));
7221
7545
  console.log(
7222
- pc3.dim(" Scaffold opinionated full-stack projects with ease\n")
7546
+ pc4.dim(" Scaffold opinionated full-stack projects with ease\n")
7223
7547
  );
7224
7548
  }
7225
7549
  var program = new Command();
@@ -7253,10 +7577,10 @@ program.name("create-kofi-stack").description(
7253
7577
  await generateProject(config);
7254
7578
  } catch (error) {
7255
7579
  if (error instanceof Error && error.message.includes("cancelled")) {
7256
- console.log(pc3.yellow("\nOperation cancelled."));
7580
+ console.log(pc4.yellow("\nOperation cancelled."));
7257
7581
  process.exit(0);
7258
7582
  }
7259
- console.error(pc3.red("\nAn error occurred:"), error);
7583
+ console.error(pc4.red("\nAn error occurred:"), error);
7260
7584
  process.exit(1);
7261
7585
  }
7262
7586
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kofi-stack",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Scaffold opinionated full-stack projects with Next.js, Convex, Better-Auth, and more",
5
5
  "type": "module",
6
6
  "bin": {