@victusvinceere/saas-cli 0.1.0 → 0.1.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.
package/dist/index.mjs CHANGED
@@ -32,59 +32,114 @@ import chalk2 from "chalk";
32
32
  import ora from "ora";
33
33
  import inquirer from "inquirer";
34
34
  async function init(projectName, options = {}) {
35
- logger.log(chalk2.bold("\nLet's create your new SaaS project!\n"));
35
+ logger.log(chalk2.bold("\n\u{1F680} Let's create your new SaaS project!\n"));
36
+ const useDefaults = options.yes;
36
37
  let name = projectName;
37
38
  if (!name) {
38
- const answers = await inquirer.prompt([
39
+ if (useDefaults) {
40
+ name = "my-saas-app";
41
+ } else {
42
+ const answers = await inquirer.prompt([
43
+ {
44
+ type: "input",
45
+ name: "projectName",
46
+ message: "What is your project name?",
47
+ default: "my-saas-app",
48
+ validate: (input) => {
49
+ if (/^[a-z0-9-]+$/.test(input)) return true;
50
+ return "Project name can only contain lowercase letters, numbers, and hyphens";
51
+ }
52
+ }
53
+ ]);
54
+ name = answers.projectName;
55
+ }
56
+ }
57
+ let branding;
58
+ if (useDefaults) {
59
+ branding = {
60
+ productName: options.product || "MySaaS",
61
+ description: options.description || "The best SaaS for your needs",
62
+ primaryColor: options.color || "green"
63
+ };
64
+ } else {
65
+ branding = await inquirer.prompt([
39
66
  {
40
67
  type: "input",
41
- name: "projectName",
42
- message: "What is your project name?",
43
- default: "my-saas-app",
44
- validate: (input) => {
45
- if (/^[a-z0-9-]+$/.test(input)) return true;
46
- return "Project name can only contain lowercase letters, numbers, and hyphens";
47
- }
68
+ name: "productName",
69
+ message: "What is your product name? (e.g., MySaaS)",
70
+ default: options.product || "MySaaS"
71
+ },
72
+ {
73
+ type: "input",
74
+ name: "description",
75
+ message: "Short description of your product:",
76
+ default: options.description || "The best SaaS for your needs"
77
+ },
78
+ {
79
+ type: "list",
80
+ name: "primaryColor",
81
+ message: "Choose your primary color:",
82
+ choices: [
83
+ { name: "Green", value: "green" },
84
+ { name: "Blue", value: "blue" },
85
+ { name: "Purple", value: "purple" },
86
+ { name: "Orange", value: "orange" },
87
+ { name: "Red", value: "red" },
88
+ { name: "Indigo", value: "indigo" }
89
+ ],
90
+ default: options.color || "green"
91
+ }
92
+ ]);
93
+ }
94
+ let config;
95
+ if (useDefaults) {
96
+ config = {
97
+ modules: ["landing", "payments", "admin"],
98
+ providers: ["google", "email"],
99
+ database: "postgresql"
100
+ };
101
+ } else {
102
+ config = await inquirer.prompt([
103
+ {
104
+ type: "checkbox",
105
+ name: "modules",
106
+ message: "Which modules would you like to include?",
107
+ choices: [
108
+ { name: "Landing Pages", value: "landing", checked: true },
109
+ { name: "Payments (Lemon Squeezy)", value: "payments", checked: true },
110
+ { name: "Admin Panel", value: "admin", checked: true },
111
+ { name: "Blog (MDX)", value: "blog", checked: false }
112
+ ]
113
+ },
114
+ {
115
+ type: "checkbox",
116
+ name: "providers",
117
+ message: "Which auth providers do you want?",
118
+ choices: [
119
+ { name: "Google", value: "google", checked: true },
120
+ { name: "GitHub", value: "github", checked: false },
121
+ { name: "Email (Magic Links)", value: "email", checked: true }
122
+ ]
123
+ },
124
+ {
125
+ type: "list",
126
+ name: "database",
127
+ message: "Which database will you use?",
128
+ choices: [
129
+ { name: "PostgreSQL", value: "postgresql" },
130
+ { name: "MySQL", value: "mysql" },
131
+ { name: "SQLite", value: "sqlite" }
132
+ ],
133
+ default: "postgresql"
48
134
  }
49
135
  ]);
50
- name = answers.projectName;
51
136
  }
52
- const config = await inquirer.prompt([
53
- {
54
- type: "checkbox",
55
- name: "modules",
56
- message: "Which modules would you like to include?",
57
- choices: [
58
- { name: "Payments (Lemon Squeezy)", value: "payments", checked: true },
59
- { name: "Admin Panel", value: "admin", checked: true },
60
- { name: "Blog (MDX)", value: "blog", checked: false },
61
- { name: "Landing Pages", value: "landing", checked: true }
62
- ]
63
- },
64
- {
65
- type: "checkbox",
66
- name: "providers",
67
- message: "Which auth providers do you want?",
68
- choices: [
69
- { name: "Google", value: "google", checked: true },
70
- { name: "GitHub", value: "github", checked: false },
71
- { name: "Email (Magic Links)", value: "email", checked: true }
72
- ]
73
- },
74
- {
75
- type: "list",
76
- name: "database",
77
- message: "Which database will you use?",
78
- choices: [
79
- { name: "PostgreSQL", value: "postgresql" },
80
- { name: "MySQL", value: "mysql" },
81
- { name: "SQLite", value: "sqlite" }
82
- ],
83
- default: "postgresql"
84
- }
85
- ]);
86
137
  const projectDir = path.resolve(process.cwd(), name);
87
138
  if (await fs.pathExists(projectDir)) {
139
+ if (useDefaults) {
140
+ logger.error(`Directory ${name} already exists. Use a different name or remove the directory.`);
141
+ process.exit(1);
142
+ }
88
143
  const { overwrite } = await inquirer.prompt([
89
144
  {
90
145
  type: "confirm",
@@ -116,28 +171,43 @@ async function init(projectName, options = {}) {
116
171
  "db:studio": "prisma studio"
117
172
  },
118
173
  dependencies: {
119
- "@saas-kit/core": "^0.1.0",
174
+ "@victusvinceere/saas-core": "latest",
120
175
  ...config.modules.includes("payments") && {
121
- "@saas-kit/payments": "^0.1.0"
176
+ "@victusvinceere/saas-payments": "latest"
122
177
  },
123
178
  ...config.modules.includes("admin") && {
124
- "@saas-kit/admin": "^0.1.0"
179
+ "@victusvinceere/saas-admin": "latest"
180
+ },
181
+ ...config.modules.includes("blog") && {
182
+ "@victusvinceere/saas-blog": "latest"
125
183
  },
126
- ...config.modules.includes("blog") && { "@saas-kit/blog": "^0.1.0" },
127
184
  ...config.modules.includes("landing") && {
128
- "@saas-kit/landing": "^0.1.0"
185
+ "@victusvinceere/saas-landing": "latest"
129
186
  },
130
- next: "^14.0.0",
131
- react: "^18.0.0",
132
- "react-dom": "^18.0.0",
133
- "next-auth": "^5.0.0-beta.0"
187
+ next: "^15.0.0",
188
+ react: "^19.0.0",
189
+ "react-dom": "^19.0.0",
190
+ "next-auth": "^5.0.0-beta.30",
191
+ "@auth/prisma-adapter": "^2.0.0",
192
+ "@prisma/client": "^6.0.0",
193
+ "lucide-react": "^0.460.0",
194
+ "@radix-ui/react-accordion": "^1.2.0",
195
+ "@radix-ui/react-avatar": "^1.1.0",
196
+ "@radix-ui/react-dropdown-menu": "^2.1.0",
197
+ "@radix-ui/react-label": "^2.1.0",
198
+ "@radix-ui/react-select": "^2.1.0",
199
+ "@radix-ui/react-slot": "^1.1.0",
200
+ "class-variance-authority": "^0.7.0",
201
+ clsx: "^2.1.0",
202
+ "tailwind-merge": "^2.5.0",
203
+ "tailwindcss-animate": "^1.0.7"
134
204
  },
135
205
  devDependencies: {
136
- "@types/node": "^20",
137
- "@types/react": "^18",
138
- "@types/react-dom": "^18",
206
+ "@types/node": "^22",
207
+ "@types/react": "^19",
208
+ "@types/react-dom": "^19",
139
209
  typescript: "^5",
140
- prisma: "^5.0.0",
210
+ prisma: "^6.0.0",
141
211
  tailwindcss: "^3.4.0",
142
212
  postcss: "^8.4.0",
143
213
  autoprefixer: "^10.4.0"
@@ -146,10 +216,188 @@ async function init(projectName, options = {}) {
146
216
  await fs.writeJSON(path.join(projectDir, "package.json"), packageJson, {
147
217
  spaces: 2
148
218
  });
149
- await fs.ensureDir(path.join(projectDir, "src/app"));
150
- await fs.ensureDir(path.join(projectDir, "src/components"));
219
+ await fs.ensureDir(path.join(projectDir, "src/app/(auth)/login"));
220
+ await fs.ensureDir(path.join(projectDir, "src/app/(auth)/signup"));
221
+ await fs.ensureDir(path.join(projectDir, "src/app/(auth)/signout"));
222
+ await fs.ensureDir(path.join(projectDir, "src/app/api/waitlist"));
223
+ await fs.ensureDir(path.join(projectDir, "src/app/dashboard"));
224
+ await fs.ensureDir(path.join(projectDir, "src/components/ui"));
225
+ await fs.ensureDir(path.join(projectDir, "src/config"));
151
226
  await fs.ensureDir(path.join(projectDir, "src/lib"));
152
227
  await fs.ensureDir(path.join(projectDir, "prisma"));
228
+ await fs.ensureDir(path.join(projectDir, "public"));
229
+ const siteConfig = `export const siteConfig = {
230
+ // Branding
231
+ name: "${branding.productName}",
232
+ description: "${branding.description}",
233
+
234
+ // Theme
235
+ theme: {
236
+ primary: "${branding.primaryColor}",
237
+ },
238
+
239
+ // Navigation
240
+ navigation: [
241
+ { label: "Features", href: "#features" },
242
+ { label: "Pricing", href: "#pricing" },
243
+ { label: "FAQ", href: "#faq" },
244
+ ],
245
+
246
+ // Hero Section
247
+ hero: {
248
+ badge: "Now in Beta \xB7 Early Access",
249
+ title: "Your Amazing Product",
250
+ highlight: "Headline",
251
+ description: "${branding.description}",
252
+ cta: {
253
+ primary: { label: "Get Started", href: "#waitlist" },
254
+ secondary: { label: "Learn More", href: "#features" },
255
+ },
256
+ },
257
+
258
+ // Features
259
+ features: [
260
+ {
261
+ icon: "Zap",
262
+ title: "Lightning Fast",
263
+ description: "Built for speed and performance.",
264
+ },
265
+ {
266
+ icon: "Shield",
267
+ title: "Secure by Default",
268
+ description: "Enterprise-grade security out of the box.",
269
+ },
270
+ {
271
+ icon: "Clock",
272
+ title: "Save Time",
273
+ description: "Automate repetitive tasks and focus on what matters.",
274
+ },
275
+ {
276
+ icon: "CheckCircle",
277
+ title: "Easy to Use",
278
+ description: "Intuitive interface that anyone can master.",
279
+ },
280
+ ],
281
+
282
+ // Pricing
283
+ pricing: {
284
+ title: "Simple, Transparent Pricing",
285
+ subtitle: "Choose the plan that fits your needs",
286
+ plans: [
287
+ {
288
+ name: "Starter",
289
+ price: 29,
290
+ period: "mo",
291
+ description: "Perfect for individuals",
292
+ features: ["Feature 1", "Feature 2", "Feature 3", "Email support"],
293
+ cta: { label: "Get Started", href: "#waitlist" },
294
+ highlighted: false,
295
+ },
296
+ {
297
+ name: "Pro",
298
+ price: 79,
299
+ period: "mo",
300
+ description: "For growing teams",
301
+ features: ["Everything in Starter", "Feature 4", "Feature 5", "Priority support"],
302
+ cta: { label: "Get Started", href: "#waitlist" },
303
+ highlighted: true,
304
+ },
305
+ ],
306
+ },
307
+
308
+ // FAQ
309
+ faq: [
310
+ {
311
+ question: "What is ${branding.productName}?",
312
+ answer: "${branding.description}",
313
+ },
314
+ {
315
+ question: "How does it work?",
316
+ answer: "Sign up, configure your settings, and start using the platform immediately.",
317
+ },
318
+ {
319
+ question: "Can I cancel anytime?",
320
+ answer: "Yes, you can cancel your subscription at any time with no questions asked.",
321
+ },
322
+ {
323
+ question: "Do you offer refunds?",
324
+ answer: "Yes, we offer a 30-day money-back guarantee.",
325
+ },
326
+ ],
327
+
328
+ // Testimonials
329
+ testimonials: [
330
+ {
331
+ quote: "This product has completely transformed how we work!",
332
+ author: "Jane Smith",
333
+ role: "CEO",
334
+ company: "TechCorp",
335
+ initial: "J",
336
+ color: "from-${branding.primaryColor}-400 to-${branding.primaryColor}-600",
337
+ },
338
+ {
339
+ quote: "The best investment we've made this year.",
340
+ author: "John Doe",
341
+ role: "Product Manager",
342
+ company: "StartupXYZ",
343
+ initial: "J",
344
+ color: "from-blue-400 to-blue-600",
345
+ },
346
+ {
347
+ quote: "Simple, intuitive, and powerful.",
348
+ author: "Sarah Johnson",
349
+ role: "Founder",
350
+ company: "GrowthCo",
351
+ initial: "S",
352
+ color: "from-purple-400 to-purple-600",
353
+ },
354
+ ],
355
+
356
+ // Waitlist
357
+ waitlist: {
358
+ title: "Join the Waitlist",
359
+ description: "Be the first to know when we launch. Early adopters get exclusive pricing.",
360
+ badge: "Early Access",
361
+ submitText: "Join Waitlist",
362
+ successMessage: "You're on the list! We'll notify you when we launch.",
363
+ businessTypes: [
364
+ { value: "startup", label: "Startup" },
365
+ { value: "small_business", label: "Small Business" },
366
+ { value: "enterprise", label: "Enterprise" },
367
+ { value: "agency", label: "Agency" },
368
+ { value: "freelancer", label: "Freelancer" },
369
+ { value: "other", label: "Other" },
370
+ ],
371
+ },
372
+
373
+ // Footer
374
+ footer: {
375
+ copyright: "${branding.productName}",
376
+ links: [
377
+ { label: "Privacy Policy", href: "/privacy" },
378
+ { label: "Terms of Service", href: "/terms" },
379
+ { label: "Contact", href: "mailto:hello@example.com" },
380
+ ],
381
+ },
382
+
383
+ // Auth
384
+ auth: {
385
+ providers: [${config.providers.map((p) => `"${p}"`).join(", ")}],
386
+ redirectAfterLogin: "/dashboard",
387
+ },
388
+
389
+ // Links
390
+ links: {
391
+ demo: "/login",
392
+ login: "/login",
393
+ signup: "/signup",
394
+ dashboard: "/dashboard",
395
+ },
396
+ } as const;
397
+
398
+ export type SiteConfig = typeof siteConfig;
399
+ `;
400
+ await fs.writeFile(path.join(projectDir, "src/config/site.ts"), siteConfig);
153
401
  const tsconfig = {
154
402
  compilerOptions: {
155
403
  target: "ES2022",
@@ -175,7 +423,7 @@ async function init(projectName, options = {}) {
175
423
  spaces: 2
176
424
  });
177
425
  const envExample = `# Database
178
- DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
426
+ DATABASE_URL="${config.database === "sqlite" ? "file:./dev.db" : `${config.database}://user:password@localhost:5432/mydb`}"
179
427
 
180
428
  # NextAuth
181
429
  AUTH_SECRET="your-secret-here"
@@ -202,36 +450,17 @@ LEMONSQUEEZY_WEBHOOK_SECRET=""
202
450
  NEXT_PUBLIC_APP_URL="http://localhost:3000"
203
451
  `;
204
452
  await fs.writeFile(path.join(projectDir, ".env.example"), envExample);
205
- const configFile = `import { defineConfig } from "@saas-kit/core";
206
-
207
- export default defineConfig({
208
- app: {
209
- name: "${name}",
210
- url: process.env.NEXT_PUBLIC_APP_URL,
211
- description: "Your SaaS application",
212
- },
213
- auth: {
214
- providers: [${config.providers.map((p) => `"${p}"`).join(", ")}],
215
- pages: {
216
- signIn: "/login",
217
- error: "/auth-error",
218
- verifyRequest: "/verify-request",
219
- },
220
- },
221
- features: {
222
- payments: ${config.modules.includes("payments")},
223
- admin: ${config.modules.includes("admin")},
224
- blog: ${config.modules.includes("blog")},
225
- },
226
- theme: {
227
- defaultTheme: "system",
228
- enableSystem: true,
229
- },
230
- });
231
- `;
232
- await fs.writeFile(path.join(projectDir, "saas-kit.config.ts"), configFile);
233
- const layoutFile = `import { SaasKitProvider } from "@saas-kit/core/providers";
453
+ const layoutFile = `import type { Metadata } from "next";
454
+ import { Inter } from "next/font/google";
234
455
  import "./globals.css";
456
+ import { siteConfig } from "@/config/site";
457
+
458
+ const inter = Inter({ subsets: ["latin"] });
459
+
460
+ export const metadata: Metadata = {
461
+ title: siteConfig.name,
462
+ description: siteConfig.description,
463
+ };
235
464
 
236
465
  export default function RootLayout({
237
466
  children,
@@ -240,42 +469,148 @@ export default function RootLayout({
240
469
  }) {
241
470
  return (
242
471
  <html lang="en" suppressHydrationWarning>
243
- <body>
244
- <SaasKitProvider>
245
- {children}
246
- </SaasKitProvider>
472
+ <body className={inter.className}>
473
+ {children}
247
474
  </body>
248
475
  </html>
249
476
  );
250
477
  }
251
478
  `;
252
- await fs.writeFile(
253
- path.join(projectDir, "src/app/layout.tsx"),
254
- layoutFile
255
- );
479
+ await fs.writeFile(path.join(projectDir, "src/app/layout.tsx"), layoutFile);
256
480
  const globalsCss = `@tailwind base;
257
481
  @tailwind components;
258
482
  @tailwind utilities;
259
483
 
260
- :root {
261
- --background: 0 0% 100%;
262
- --foreground: 222.2 47.4% 11.2%;
263
- --primary: 222.2 47.4% 11.2%;
264
- --primary-foreground: 210 40% 98%;
484
+ @layer base {
485
+ :root {
486
+ --background: 0 0% 100%;
487
+ --foreground: 222.2 84% 4.9%;
488
+ --card: 0 0% 100%;
489
+ --card-foreground: 222.2 84% 4.9%;
490
+ --popover: 0 0% 100%;
491
+ --popover-foreground: 222.2 84% 4.9%;
492
+ --primary: 142.1 76.2% 36.3%;
493
+ --primary-foreground: 355.7 100% 97.3%;
494
+ --secondary: 210 40% 96.1%;
495
+ --secondary-foreground: 222.2 47.4% 11.2%;
496
+ --muted: 210 40% 96.1%;
497
+ --muted-foreground: 215.4 16.3% 46.9%;
498
+ --accent: 210 40% 96.1%;
499
+ --accent-foreground: 222.2 47.4% 11.2%;
500
+ --destructive: 0 84.2% 60.2%;
501
+ --destructive-foreground: 210 40% 98%;
502
+ --border: 214.3 31.8% 91.4%;
503
+ --input: 214.3 31.8% 91.4%;
504
+ --ring: 142.1 76.2% 36.3%;
505
+ --radius: 0.5rem;
506
+ }
507
+
508
+ .dark {
509
+ --background: 222.2 84% 4.9%;
510
+ --foreground: 210 40% 98%;
511
+ --card: 222.2 84% 4.9%;
512
+ --card-foreground: 210 40% 98%;
513
+ --popover: 222.2 84% 4.9%;
514
+ --popover-foreground: 210 40% 98%;
515
+ --primary: 142.1 76.2% 36.3%;
516
+ --primary-foreground: 355.7 100% 97.3%;
517
+ --secondary: 217.2 32.6% 17.5%;
518
+ --secondary-foreground: 210 40% 98%;
519
+ --muted: 217.2 32.6% 17.5%;
520
+ --muted-foreground: 215 20.2% 65.1%;
521
+ --accent: 217.2 32.6% 17.5%;
522
+ --accent-foreground: 210 40% 98%;
523
+ --destructive: 0 62.8% 30.6%;
524
+ --destructive-foreground: 210 40% 98%;
525
+ --border: 217.2 32.6% 17.5%;
526
+ --input: 217.2 32.6% 17.5%;
527
+ --ring: 142.1 76.2% 36.3%;
528
+ }
265
529
  }
266
530
 
267
- .dark {
268
- --background: 224 71% 4%;
269
- --foreground: 213 31% 91%;
270
- --primary: 210 40% 98%;
271
- --primary-foreground: 222.2 47.4% 1.2%;
531
+ @layer base {
532
+ * {
533
+ @apply border-border;
534
+ }
535
+ body {
536
+ @apply bg-background text-foreground;
537
+ }
272
538
  }
273
539
  `;
274
- await fs.writeFile(
275
- path.join(projectDir, "src/app/globals.css"),
276
- globalsCss
277
- );
278
- const pageTsx = `export default function Home() {
540
+ await fs.writeFile(path.join(projectDir, "src/app/globals.css"), globalsCss);
541
+ const hasLanding = config.modules.includes("landing");
542
+ const pageTsx = hasLanding ? `"use client";
543
+
544
+ import { siteConfig } from "@/config/site";
545
+ import {
546
+ Navbar,
547
+ Hero,
548
+ Features,
549
+ Pricing,
550
+ Faq,
551
+ Testimonials,
552
+ Waitlist,
553
+ Footer,
554
+ } from "@victusvinceere/saas-landing/components";
555
+
556
+ export default function Home() {
557
+ return (
558
+ <div className="min-h-screen">
559
+ <Navbar
560
+ brand={siteConfig.name}
561
+ links={siteConfig.navigation}
562
+ cta={{ label: "Get Started", href: "#waitlist" }}
563
+ />
564
+
565
+ <Hero
566
+ badge={siteConfig.hero.badge}
567
+ title={siteConfig.hero.title}
568
+ highlight={siteConfig.hero.highlight}
569
+ description={siteConfig.hero.description}
570
+ primaryCta={siteConfig.hero.cta.primary}
571
+ secondaryCta={siteConfig.hero.cta.secondary}
572
+ />
573
+
574
+ <Features
575
+ title="Features"
576
+ subtitle="Everything you need to succeed"
577
+ features={siteConfig.features}
578
+ />
579
+
580
+ <Pricing
581
+ title={siteConfig.pricing.title}
582
+ subtitle={siteConfig.pricing.subtitle}
583
+ plans={siteConfig.pricing.plans}
584
+ />
585
+
586
+ <Testimonials
587
+ title="What Our Customers Say"
588
+ testimonials={siteConfig.testimonials}
589
+ />
590
+
591
+ <Faq
592
+ title="Frequently Asked Questions"
593
+ items={siteConfig.faq}
594
+ />
595
+
596
+ <Waitlist
597
+ title={siteConfig.waitlist.title}
598
+ description={siteConfig.waitlist.description}
599
+ badge={siteConfig.waitlist.badge}
600
+ submitText={siteConfig.waitlist.submitText}
601
+ successMessage={siteConfig.waitlist.successMessage}
602
+ businessTypes={siteConfig.waitlist.businessTypes}
603
+ />
604
+
605
+ <Footer
606
+ brand={siteConfig.name}
607
+ links={siteConfig.footer.links}
608
+ copyright={\`\xA9 \${new Date().getFullYear()} \${siteConfig.footer.copyright}. All rights reserved.\`}
609
+ />
610
+ </div>
611
+ );
612
+ }
613
+ ` : `export default function Home() {
279
614
  return (
280
615
  <main className="flex min-h-screen flex-col items-center justify-center p-24">
281
616
  <h1 className="text-4xl font-bold">Welcome to ${name}</h1>
@@ -287,17 +622,390 @@ export default function RootLayout({
287
622
  }
288
623
  `;
289
624
  await fs.writeFile(path.join(projectDir, "src/app/page.tsx"), pageTsx);
625
+ const loginPage = `"use client";
626
+
627
+ import { signIn } from "next-auth/react";
628
+ import { useState } from "react";
629
+ import Link from "next/link";
630
+ import { siteConfig } from "@/config/site";
631
+
632
+ export default function LoginPage() {
633
+ const [email, setEmail] = useState("");
634
+ const [loading, setLoading] = useState(false);
635
+ const [emailSent, setEmailSent] = useState(false);
636
+
637
+ const handleMagicLink = async (e: React.FormEvent) => {
638
+ e.preventDefault();
639
+ setLoading(true);
640
+ await signIn("resend", { email, callbackUrl: siteConfig.auth.redirectAfterLogin });
641
+ setEmailSent(true);
642
+ setLoading(false);
643
+ };
644
+
645
+ if (emailSent) {
646
+ return (
647
+ <div className="flex min-h-screen items-center justify-center p-4">
648
+ <div className="w-full max-w-md text-center">
649
+ <h1 className="text-2xl font-bold mb-4">Check your email</h1>
650
+ <p className="text-muted-foreground">
651
+ We sent a magic link to <strong>{email}</strong>
652
+ </p>
653
+ </div>
654
+ </div>
655
+ );
656
+ }
657
+
658
+ return (
659
+ <div className="flex min-h-screen items-center justify-center p-4">
660
+ <div className="w-full max-w-md space-y-6">
661
+ <div className="text-center">
662
+ <h1 className="text-2xl font-bold">Welcome back</h1>
663
+ <p className="text-muted-foreground">Sign in to your account</p>
664
+ </div>
665
+
666
+ <div className="space-y-4">
667
+ <button
668
+ onClick={() => signIn("google", { callbackUrl: siteConfig.auth.redirectAfterLogin })}
669
+ className="w-full flex items-center justify-center gap-2 rounded-lg border px-4 py-3 hover:bg-muted"
670
+ >
671
+ <svg className="h-5 w-5" viewBox="0 0 24 24">
672
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
673
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
674
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
675
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
676
+ </svg>
677
+ Continue with Google
678
+ </button>
679
+
680
+ <div className="relative">
681
+ <div className="absolute inset-0 flex items-center">
682
+ <span className="w-full border-t" />
683
+ </div>
684
+ <div className="relative flex justify-center text-xs uppercase">
685
+ <span className="bg-background px-2 text-muted-foreground">Or continue with</span>
686
+ </div>
687
+ </div>
688
+
689
+ <form onSubmit={handleMagicLink} className="space-y-4">
690
+ <input
691
+ type="email"
692
+ placeholder="email@example.com"
693
+ value={email}
694
+ onChange={(e) => setEmail(e.target.value)}
695
+ required
696
+ className="w-full rounded-lg border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary"
697
+ />
698
+ <button
699
+ type="submit"
700
+ disabled={loading}
701
+ className="w-full rounded-lg bg-primary px-4 py-3 text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
702
+ >
703
+ {loading ? "Sending..." : "Send Magic Link"}
704
+ </button>
705
+ </form>
706
+ </div>
707
+
708
+ <p className="text-center text-sm text-muted-foreground">
709
+ Don't have an account?{" "}
710
+ <Link href="/signup" className="text-primary hover:underline">
711
+ Sign up
712
+ </Link>
713
+ </p>
714
+ </div>
715
+ </div>
716
+ );
717
+ }
718
+ `;
719
+ await fs.writeFile(
720
+ path.join(projectDir, "src/app/(auth)/login/page.tsx"),
721
+ loginPage
722
+ );
723
+ const signupPage = `"use client";
724
+
725
+ import { signIn } from "next-auth/react";
726
+ import { useState } from "react";
727
+ import Link from "next/link";
728
+ import { siteConfig } from "@/config/site";
729
+
730
+ export default function SignupPage() {
731
+ const [email, setEmail] = useState("");
732
+ const [loading, setLoading] = useState(false);
733
+ const [emailSent, setEmailSent] = useState(false);
734
+
735
+ const handleMagicLink = async (e: React.FormEvent) => {
736
+ e.preventDefault();
737
+ setLoading(true);
738
+ await signIn("resend", { email, callbackUrl: siteConfig.auth.redirectAfterLogin });
739
+ setEmailSent(true);
740
+ setLoading(false);
741
+ };
742
+
743
+ if (emailSent) {
744
+ return (
745
+ <div className="flex min-h-screen items-center justify-center p-4">
746
+ <div className="w-full max-w-md text-center">
747
+ <h1 className="text-2xl font-bold mb-4">Check your email</h1>
748
+ <p className="text-muted-foreground">
749
+ We sent a magic link to <strong>{email}</strong>
750
+ </p>
751
+ </div>
752
+ </div>
753
+ );
754
+ }
755
+
756
+ return (
757
+ <div className="flex min-h-screen items-center justify-center p-4">
758
+ <div className="w-full max-w-md space-y-6">
759
+ <div className="text-center">
760
+ <h1 className="text-2xl font-bold">Create an account</h1>
761
+ <p className="text-muted-foreground">Get started with {siteConfig.name}</p>
762
+ </div>
763
+
764
+ <div className="space-y-4">
765
+ <button
766
+ onClick={() => signIn("google", { callbackUrl: siteConfig.auth.redirectAfterLogin })}
767
+ className="w-full flex items-center justify-center gap-2 rounded-lg border px-4 py-3 hover:bg-muted"
768
+ >
769
+ <svg className="h-5 w-5" viewBox="0 0 24 24">
770
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
771
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
772
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
773
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
774
+ </svg>
775
+ Continue with Google
776
+ </button>
777
+
778
+ <div className="relative">
779
+ <div className="absolute inset-0 flex items-center">
780
+ <span className="w-full border-t" />
781
+ </div>
782
+ <div className="relative flex justify-center text-xs uppercase">
783
+ <span className="bg-background px-2 text-muted-foreground">Or continue with</span>
784
+ </div>
785
+ </div>
786
+
787
+ <form onSubmit={handleMagicLink} className="space-y-4">
788
+ <input
789
+ type="email"
790
+ placeholder="email@example.com"
791
+ value={email}
792
+ onChange={(e) => setEmail(e.target.value)}
793
+ required
794
+ className="w-full rounded-lg border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary"
795
+ />
796
+ <button
797
+ type="submit"
798
+ disabled={loading}
799
+ className="w-full rounded-lg bg-primary px-4 py-3 text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
800
+ >
801
+ {loading ? "Sending..." : "Send Magic Link"}
802
+ </button>
803
+ </form>
804
+ </div>
805
+
806
+ <p className="text-center text-sm text-muted-foreground">
807
+ Already have an account?{" "}
808
+ <Link href="/login" className="text-primary hover:underline">
809
+ Sign in
810
+ </Link>
811
+ </p>
812
+ </div>
813
+ </div>
814
+ );
815
+ }
816
+ `;
817
+ await fs.writeFile(
818
+ path.join(projectDir, "src/app/(auth)/signup/page.tsx"),
819
+ signupPage
820
+ );
821
+ const signoutPage = `"use client";
822
+
823
+ import { signOut } from "next-auth/react";
824
+ import Link from "next/link";
825
+ import { siteConfig } from "@/config/site";
826
+
827
+ export default function SignOutPage() {
828
+ return (
829
+ <div className="flex min-h-screen items-center justify-center p-4">
830
+ <div className="w-full max-w-md space-y-6 text-center">
831
+ <div>
832
+ <h1 className="text-2xl font-bold">Sign out</h1>
833
+ <p className="text-muted-foreground mt-2">
834
+ Are you sure you want to sign out?
835
+ </p>
836
+ </div>
837
+
838
+ <div className="space-y-3">
839
+ <button
840
+ onClick={() => signOut({ callbackUrl: "/" })}
841
+ className="w-full rounded-lg bg-destructive px-4 py-3 text-destructive-foreground hover:bg-destructive/90"
842
+ >
843
+ Yes, sign me out
844
+ </button>
845
+ <Link
846
+ href={siteConfig.links.dashboard}
847
+ className="block w-full rounded-lg border px-4 py-3 hover:bg-muted"
848
+ >
849
+ Cancel, go back
850
+ </Link>
851
+ </div>
852
+ </div>
853
+ </div>
854
+ );
855
+ }
856
+ `;
857
+ await fs.writeFile(
858
+ path.join(projectDir, "src/app/(auth)/signout/page.tsx"),
859
+ signoutPage
860
+ );
861
+ const dashboardPage = `export default function DashboardPage() {
862
+ return (
863
+ <div className="p-8">
864
+ <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
865
+ <p className="text-muted-foreground">
866
+ Welcome to your dashboard. Start building your product here.
867
+ </p>
868
+ </div>
869
+ );
870
+ }
871
+ `;
872
+ await fs.writeFile(
873
+ path.join(projectDir, "src/app/dashboard/page.tsx"),
874
+ dashboardPage
875
+ );
876
+ const dashboardLayout = `import Link from "next/link";
877
+
878
+ export default function DashboardLayout({
879
+ children,
880
+ }: {
881
+ children: React.ReactNode;
882
+ }) {
883
+ return (
884
+ <div className="min-h-screen bg-muted/30">
885
+ <nav className="border-b bg-background px-6 py-4">
886
+ <div className="flex items-center justify-between">
887
+ <span className="text-xl font-bold">Dashboard</span>
888
+ <Link
889
+ href="/signout"
890
+ className="text-sm text-muted-foreground hover:text-foreground"
891
+ >
892
+ Sign out
893
+ </Link>
894
+ </div>
895
+ </nav>
896
+ <main>{children}</main>
897
+ </div>
898
+ );
899
+ }
900
+ `;
901
+ await fs.writeFile(
902
+ path.join(projectDir, "src/app/dashboard/layout.tsx"),
903
+ dashboardLayout
904
+ );
905
+ const waitlistRoute = `import { NextResponse } from "next/server";
906
+
907
+ export async function POST(request: Request) {
908
+ try {
909
+ const body = await request.json();
910
+ const { email, name, businessType } = body;
911
+
912
+ if (!email || !name) {
913
+ return NextResponse.json(
914
+ { error: "Email and name are required" },
915
+ { status: 400 }
916
+ );
917
+ }
918
+
919
+ // TODO: Save to database
920
+ // await prisma.waitlistEntry.create({
921
+ // data: { email, name, businessType },
922
+ // });
923
+
924
+ console.log("Waitlist signup:", { email, name, businessType });
925
+
926
+ return NextResponse.json({
927
+ message: "Successfully joined the waitlist!",
928
+ });
929
+ } catch (error) {
930
+ console.error("Waitlist signup error:", error);
931
+ return NextResponse.json(
932
+ { error: "Something went wrong" },
933
+ { status: 500 }
934
+ );
935
+ }
936
+ }
937
+ `;
938
+ await fs.writeFile(
939
+ path.join(projectDir, "src/app/api/waitlist/route.ts"),
940
+ waitlistRoute
941
+ );
942
+ const utilsTs = `import { type ClassValue, clsx } from "clsx";
943
+ import { twMerge } from "tailwind-merge";
944
+
945
+ export function cn(...inputs: ClassValue[]) {
946
+ return twMerge(clsx(inputs));
947
+ }
948
+ `;
949
+ await fs.writeFile(path.join(projectDir, "src/lib/utils.ts"), utilsTs);
290
950
  const tailwindConfig = `/** @type {import('tailwindcss').Config} */
291
951
  module.exports = {
292
952
  darkMode: ["class"],
293
953
  content: [
294
954
  "./src/**/*.{js,ts,jsx,tsx,mdx}",
295
- "./node_modules/@saas-kit/*/dist/**/*.{js,ts,jsx,tsx}",
955
+ "./node_modules/@victusvinceere/saas-*/dist/**/*.{js,ts,jsx,tsx}",
296
956
  ],
297
957
  theme: {
298
- extend: {},
958
+ container: {
959
+ center: true,
960
+ padding: "2rem",
961
+ screens: {
962
+ "2xl": "1400px",
963
+ },
964
+ },
965
+ extend: {
966
+ colors: {
967
+ border: "hsl(var(--border))",
968
+ input: "hsl(var(--input))",
969
+ ring: "hsl(var(--ring))",
970
+ background: "hsl(var(--background))",
971
+ foreground: "hsl(var(--foreground))",
972
+ primary: {
973
+ DEFAULT: "hsl(var(--primary))",
974
+ foreground: "hsl(var(--primary-foreground))",
975
+ },
976
+ secondary: {
977
+ DEFAULT: "hsl(var(--secondary))",
978
+ foreground: "hsl(var(--secondary-foreground))",
979
+ },
980
+ destructive: {
981
+ DEFAULT: "hsl(var(--destructive))",
982
+ foreground: "hsl(var(--destructive-foreground))",
983
+ },
984
+ muted: {
985
+ DEFAULT: "hsl(var(--muted))",
986
+ foreground: "hsl(var(--muted-foreground))",
987
+ },
988
+ accent: {
989
+ DEFAULT: "hsl(var(--accent))",
990
+ foreground: "hsl(var(--accent-foreground))",
991
+ },
992
+ popover: {
993
+ DEFAULT: "hsl(var(--popover))",
994
+ foreground: "hsl(var(--popover-foreground))",
995
+ },
996
+ card: {
997
+ DEFAULT: "hsl(var(--card))",
998
+ foreground: "hsl(var(--card-foreground))",
999
+ },
1000
+ },
1001
+ borderRadius: {
1002
+ lg: "var(--radius)",
1003
+ md: "calc(var(--radius) - 2px)",
1004
+ sm: "calc(var(--radius) - 4px)",
1005
+ },
1006
+ },
299
1007
  },
300
- plugins: [],
1008
+ plugins: [require("tailwindcss-animate")],
301
1009
  };
302
1010
  `;
303
1011
  await fs.writeFile(
@@ -317,9 +1025,13 @@ module.exports = {
317
1025
  );
318
1026
  const nextConfig = `/** @type {import('next').NextConfig} */
319
1027
  const nextConfig = {
320
- experimental: {
321
- serverActions: true,
322
- },
1028
+ transpilePackages: [
1029
+ "@victusvinceere/saas-core",
1030
+ "@victusvinceere/saas-landing",
1031
+ "@victusvinceere/saas-admin",
1032
+ "@victusvinceere/saas-payments",
1033
+ "@victusvinceere/saas-blog",
1034
+ ],
323
1035
  };
324
1036
 
325
1037
  module.exports = nextConfig;
@@ -361,17 +1073,97 @@ yarn-error.log*
361
1073
  next-env.d.ts
362
1074
  `;
363
1075
  await fs.writeFile(path.join(projectDir, ".gitignore"), gitignore);
1076
+ const prismaSchema = `generator client {
1077
+ provider = "prisma-client-js"
1078
+ }
1079
+
1080
+ datasource db {
1081
+ provider = "${config.database}"
1082
+ url = env("DATABASE_URL")
1083
+ }
1084
+
1085
+ model Account {
1086
+ id String @id @default(cuid())
1087
+ userId String
1088
+ type String
1089
+ provider String
1090
+ providerAccountId String
1091
+ refresh_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
1092
+ access_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
1093
+ expires_at Int?
1094
+ token_type String?
1095
+ scope String?
1096
+ id_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
1097
+ session_state String?
1098
+
1099
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1100
+
1101
+ @@unique([provider, providerAccountId])
1102
+ }
1103
+
1104
+ model Session {
1105
+ id String @id @default(cuid())
1106
+ sessionToken String @unique
1107
+ userId String
1108
+ expires DateTime
1109
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1110
+ }
1111
+
1112
+ model User {
1113
+ id String @id @default(cuid())
1114
+ name String?
1115
+ email String? @unique
1116
+ emailVerified DateTime?
1117
+ image String?
1118
+ role String @default("USER")
1119
+ accounts Account[]
1120
+ sessions Session[]
1121
+ createdAt DateTime @default(now())
1122
+ updatedAt DateTime @updatedAt
1123
+ }
1124
+
1125
+ model VerificationToken {
1126
+ identifier String
1127
+ token String @unique
1128
+ expires DateTime
1129
+
1130
+ @@unique([identifier, token])
1131
+ }
1132
+
1133
+ model WaitlistEntry {
1134
+ id String @id @default(cuid())
1135
+ email String @unique
1136
+ name String
1137
+ businessType String?
1138
+ createdAt DateTime @default(now())
1139
+ }
1140
+ `;
1141
+ await fs.writeFile(
1142
+ path.join(projectDir, "prisma/schema.prisma"),
1143
+ prismaSchema
1144
+ );
364
1145
  spinner.succeed("Project created successfully!");
365
1146
  logger.log("");
366
- logger.log(chalk2.bold("Next steps:"));
1147
+ logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1148
+ logger.log(chalk2.bold(" Next steps:"));
1149
+ logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
367
1150
  logger.log("");
368
1151
  logger.log(chalk2.cyan(` cd ${name}`));
369
- logger.log(chalk2.cyan(" npm install"));
1152
+ logger.log(chalk2.cyan(" pnpm install"));
370
1153
  logger.log(chalk2.cyan(" cp .env.example .env.local"));
371
- logger.log(chalk2.cyan(" # Edit .env.local with your credentials"));
372
- logger.log(chalk2.cyan(" npm run dev"));
1154
+ logger.log(chalk2.gray(" # Edit .env.local with your credentials"));
1155
+ logger.log(chalk2.cyan(" pnpm db:push"));
1156
+ logger.log(chalk2.cyan(" pnpm dev"));
1157
+ logger.log("");
1158
+ logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1159
+ logger.log(chalk2.bold(" Customization:"));
1160
+ logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1161
+ logger.log("");
1162
+ logger.log(chalk2.gray(" \u2022 Edit src/config/site.ts to customize content"));
1163
+ logger.log(chalk2.gray(" \u2022 Update src/app/page.tsx to modify landing page"));
1164
+ logger.log(chalk2.gray(" \u2022 Build features in src/app/dashboard/"));
373
1165
  logger.log("");
374
- logger.success("Happy building!");
1166
+ logger.success("Happy building! \u{1F680}");
375
1167
  } catch (error) {
376
1168
  spinner.fail("Failed to create project");
377
1169
  logger.error(String(error));
@@ -747,7 +1539,7 @@ export const POST = createWebhookHandler({
747
1539
  // src/index.ts
748
1540
  var program = new Command();
749
1541
  program.name("saas-kit").description("CLI tool for scaffolding SaaS Kit projects").version("0.1.0");
750
- program.command("init [name]").description("Create a new SaaS Kit project").option("-t, --template <template>", "Use a specific template").action((name, options) => {
1542
+ program.command("init [name]").description("Create a new SaaS Kit project").option("-t, --template <template>", "Use a specific template").option("-y, --yes", "Use defaults without prompting").option("--product <name>", "Product name").option("--description <desc>", "Product description").option("--color <color>", "Primary color (green, blue, purple, orange, red, indigo)").action((name, options) => {
751
1543
  printBanner();
752
1544
  init(name, options);
753
1545
  });