@shipindays/shipindays 0.1.19 → 0.2.0
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/index.js +98 -47
- package/package.json +7 -2
- package/templates/blocks/database/drizzle-supabase/drizzle.config.ts +10 -0
- package/templates/blocks/database/drizzle-supabase/package.json +16 -0
- package/templates/blocks/database/prisma-supabase/package.json +14 -0
- package/templates/blocks/database/prisma-supabase/prisma/schema.prisma +94 -0
- package/templates/blocks/payments/dodopayments/package.json +1 -1
- package/templates/blocks/payments/dodopayments/lib/payments/index.ts +0 -14
- package/templates/blocks/payments/stripe/lib/payments/index.ts +0 -31
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import fs from "fs-extra";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
+
import figlet from "figlet";
|
|
10
|
+
import gradient from "gradient-string";
|
|
9
11
|
|
|
10
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
|
|
@@ -14,8 +16,14 @@ const BASE_DIR = path.join(TEMPLATES_DIR, "base");
|
|
|
14
16
|
const BLOCKS_DIR = path.join(TEMPLATES_DIR, "blocks");
|
|
15
17
|
|
|
16
18
|
function printBanner() {
|
|
19
|
+
const bannerText = figlet.textSync("Ship In Days", {
|
|
20
|
+
font: "Slant",
|
|
21
|
+
});
|
|
22
|
+
|
|
17
23
|
console.log("\n");
|
|
18
|
-
|
|
24
|
+
|
|
25
|
+
// This creates a smooth transition from Orange to Purple
|
|
26
|
+
console.log(gradient(["#FF8C00", "#8A2BE2"])(bannerText));
|
|
19
27
|
console.log(chalk.dim(" Ship your SaaS in days, not months."));
|
|
20
28
|
console.log(chalk.dim(" https://shipindays.nikhilsai.in\n"));
|
|
21
29
|
}
|
|
@@ -54,14 +62,38 @@ const PAYMENT_PROVIDERS = {
|
|
|
54
62
|
},
|
|
55
63
|
};
|
|
56
64
|
|
|
65
|
+
// DATABASE providers constants
|
|
66
|
+
const DATABASE_PROVIDERS = {
|
|
67
|
+
drizzle: {
|
|
68
|
+
label: "Drizzle ORM + Supabase PostgreSQL",
|
|
69
|
+
hint: "Lightweight SQL-first ORM using Supabase Postgres",
|
|
70
|
+
},
|
|
71
|
+
prisma: {
|
|
72
|
+
label: "Prisma ORM + PostgreSQL",
|
|
73
|
+
hint: "Type-safe ORM with migrations & powerful client",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
57
77
|
const ENV_VARS = {
|
|
58
78
|
base: {
|
|
59
|
-
"# App": [
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
"# App": ["NEXT_PUBLIC_APP_URL=http://localhost:3000"],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// 1. Move database to the top level (Fixes the missing Drizzle envs)
|
|
83
|
+
database: {
|
|
84
|
+
drizzle: {
|
|
85
|
+
"# Supabase database url! go to supabase -> connect -> transaction pooler": [
|
|
86
|
+
"DATABASE_URL=",
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
prisma: {
|
|
91
|
+
"# PostgreSQL connection (Supabase or any provider)": [
|
|
92
|
+
"DATABASE_URL=",
|
|
93
|
+
],
|
|
94
|
+
},
|
|
62
95
|
},
|
|
63
96
|
|
|
64
|
-
// auth env's
|
|
65
97
|
auth: {
|
|
66
98
|
supabase: {
|
|
67
99
|
"# Supabase (supabase.com → project → settings → API)": [
|
|
@@ -71,10 +103,7 @@ const ENV_VARS = {
|
|
|
71
103
|
],
|
|
72
104
|
},
|
|
73
105
|
nextauth: {
|
|
74
|
-
"# NextAuth": [
|
|
75
|
-
"AUTH_SECRET=",
|
|
76
|
-
"NEXTAUTH_URL=http://localhost:3000",
|
|
77
|
-
],
|
|
106
|
+
"# NextAuth": ["AUTH_SECRET=", "NEXTAUTH_URL=http://localhost:3000"],
|
|
78
107
|
"# OAuth — Google (console.cloud.google.com)": [
|
|
79
108
|
"AUTH_GOOGLE_ID=",
|
|
80
109
|
"AUTH_GOOGLE_SECRET=",
|
|
@@ -82,12 +111,9 @@ const ENV_VARS = {
|
|
|
82
111
|
},
|
|
83
112
|
},
|
|
84
113
|
|
|
85
|
-
// emails env's
|
|
86
114
|
email: {
|
|
87
115
|
resend: {
|
|
88
|
-
"# Resend (resend.com → API Keys)": [
|
|
89
|
-
"RESEND_API_KEY=",
|
|
90
|
-
],
|
|
116
|
+
"# Resend (resend.com → API Keys)": ["RESEND_API_KEY="],
|
|
91
117
|
},
|
|
92
118
|
mailgun: {
|
|
93
119
|
"# Mailgun (mailgun.com → Sending → Domains)": [
|
|
@@ -95,36 +121,41 @@ const ENV_VARS = {
|
|
|
95
121
|
"MAILGUN_DOMAIN=",
|
|
96
122
|
],
|
|
97
123
|
},
|
|
124
|
+
},
|
|
98
125
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
dodopayments:
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
],
|
|
122
|
-
},
|
|
126
|
+
// 2. Move payments to the top level (Fixes the missing Dodo envs)
|
|
127
|
+
payments: {
|
|
128
|
+
stripe: {
|
|
129
|
+
"# Stripe (dashboard.stripe.com → Developers → API keys)": [
|
|
130
|
+
"STRIPE_SECRET_KEY=",
|
|
131
|
+
"STRIPE_WEBHOOK_SECRET=",
|
|
132
|
+
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=",
|
|
133
|
+
],
|
|
134
|
+
"# Stripe Pricing": [
|
|
135
|
+
"NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC=",
|
|
136
|
+
"NEXT_PUBLIC_STRIPE_PRICE_ID_PRO=",
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
dodopayments: {
|
|
140
|
+
"# Dodo Payments (app.dodopayments.com → Developers → API Keys)": [
|
|
141
|
+
"DODO_PAYMENTS_API_KEY=",
|
|
142
|
+
"DODO_PAYMENTS_WEBHOOK_KEY=",
|
|
143
|
+
],
|
|
144
|
+
"# Dodo Pricing": [
|
|
145
|
+
"NEXT_PUBLIC_DODO_PRICE_ID_BASIC=",
|
|
146
|
+
"NEXT_PUBLIC_DODO_PRICE_ID_PRO=",
|
|
147
|
+
],
|
|
123
148
|
},
|
|
124
|
-
|
|
125
149
|
},
|
|
126
150
|
};
|
|
127
151
|
|
|
152
|
+
// block mapping
|
|
153
|
+
// variable name : folder name
|
|
154
|
+
const DATABASE_BLOCK_MAP = {
|
|
155
|
+
drizzle: "drizzle-supabase",
|
|
156
|
+
prisma: "prisma-supabase",
|
|
157
|
+
};
|
|
158
|
+
|
|
128
159
|
/**
|
|
129
160
|
* RECURSIVE DIRECTORY MERGE
|
|
130
161
|
* * This function walks through the source directory and copies files to the destination.
|
|
@@ -342,7 +373,16 @@ async function main() {
|
|
|
342
373
|
}
|
|
343
374
|
}
|
|
344
375
|
|
|
345
|
-
//
|
|
376
|
+
// pick database provider
|
|
377
|
+
const dbProvider = await p.select({
|
|
378
|
+
message: "Database Provider",
|
|
379
|
+
options: Object.entries(DATABASE_PROVIDERS).map(([value, { label, hint }]) => ({
|
|
380
|
+
value, label, hint,
|
|
381
|
+
})),
|
|
382
|
+
})
|
|
383
|
+
if (p.isCancel(dbProvider)) { p.cancel("Cancelled."); process.exit(0); }
|
|
384
|
+
|
|
385
|
+
// Pick auth provider
|
|
346
386
|
const authProvider = await p.select({
|
|
347
387
|
message: "Auth provider",
|
|
348
388
|
options: Object.entries(AUTH_PROVIDERS).map(([value, { label, hint }]) => ({
|
|
@@ -367,7 +407,12 @@ async function main() {
|
|
|
367
407
|
})),
|
|
368
408
|
});
|
|
369
409
|
|
|
370
|
-
const choices = {
|
|
410
|
+
const choices = {
|
|
411
|
+
database: dbProvider,
|
|
412
|
+
auth: authProvider,
|
|
413
|
+
email: emailProvider,
|
|
414
|
+
payments: paymentProvider
|
|
415
|
+
};
|
|
371
416
|
|
|
372
417
|
// 4. Git + install preferences
|
|
373
418
|
const initGit = await p.confirm({
|
|
@@ -392,15 +437,21 @@ async function main() {
|
|
|
392
437
|
|
|
393
438
|
// 6. Inject Blocks
|
|
394
439
|
// The injectBlock function now handles the internal recursion correctly.
|
|
395
|
-
const features = ["auth", "email", "payments"];
|
|
440
|
+
const features = ["database", "auth", "email", "payments"];
|
|
441
|
+
|
|
396
442
|
for (const feature of features) {
|
|
397
|
-
|
|
398
|
-
if (provider)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
443
|
+
let provider = choices[feature];
|
|
444
|
+
if (!provider) continue;
|
|
445
|
+
|
|
446
|
+
// map database providers to actual folder names
|
|
447
|
+
if (feature === "database") {
|
|
448
|
+
provider = DATABASE_BLOCK_MAP[provider];
|
|
403
449
|
}
|
|
450
|
+
|
|
451
|
+
spin.start(`Injecting ${feature}: ${provider}...`);
|
|
452
|
+
await injectBlock(feature, provider, targetPath);
|
|
453
|
+
await mergePackageJson(targetPath, feature, provider);
|
|
454
|
+
spin.stop(`${feature} injected ✓`);
|
|
404
455
|
}
|
|
405
456
|
|
|
406
457
|
// 8. Write .env.example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipindays/shipindays",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open source CLI to scaffold a Next.js SaaS. Pick your auth, email, and payments wired up and ready to ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,9 +30,14 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.9.1",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
|
-
"
|
|
33
|
+
"figlet": "^1.11.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"gradient-string": "^3.0.0"
|
|
34
36
|
},
|
|
35
37
|
"engines": {
|
|
36
38
|
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/figlet": "^1.7.0"
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"db:generate": "drizzle-kit generate",
|
|
4
|
+
"db:migrate": "drizzle-kit migrate",
|
|
5
|
+
"db:push": "drizzle-kit push",
|
|
6
|
+
"db:studio": "drizzle-kit studio"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"drizzle-orm": "^1.0.0-beta.19",
|
|
10
|
+
"postgres": "^3.4.4"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"dotenv": "^17.2.3",
|
|
14
|
+
"drizzle-kit": "^0.31.8"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"db:generate": "prisma generate",
|
|
4
|
+
"db:push": "prisma db push",
|
|
5
|
+
"db:migrate": "prisma migrate dev",
|
|
6
|
+
"db:studio": "prisma studio"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@prisma/client": "^5.0.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"prisma": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id String @id @default(uuid())
|
|
12
|
+
email String @unique @db.VarChar(255)
|
|
13
|
+
name String? @db.VarChar(255)
|
|
14
|
+
image String?
|
|
15
|
+
|
|
16
|
+
// Auth
|
|
17
|
+
authId String @unique @map("auth_id") @db.VarChar(255)
|
|
18
|
+
|
|
19
|
+
// Tracking
|
|
20
|
+
loginCount Int @default(0) @map("login_count")
|
|
21
|
+
lastLoginAt DateTime? @map("last_login_at")
|
|
22
|
+
|
|
23
|
+
// Timestamps
|
|
24
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
25
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
26
|
+
|
|
27
|
+
// Relations
|
|
28
|
+
subscription Subscription?
|
|
29
|
+
payments Payment[]
|
|
30
|
+
notifications Notification[]
|
|
31
|
+
|
|
32
|
+
@@index([email], map: "email_idx")
|
|
33
|
+
@@index([authId], map: "auth_id_idx")
|
|
34
|
+
@@map("users")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
model Subscription {
|
|
38
|
+
id String @id @db.VarChar(255)
|
|
39
|
+
userId String @unique @map("user_id")
|
|
40
|
+
|
|
41
|
+
planId String @map("plan_id") @db.VarChar(255)
|
|
42
|
+
status String @db.VarChar(50)
|
|
43
|
+
|
|
44
|
+
currentPeriodEnd DateTime @map("current_period_end")
|
|
45
|
+
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
|
46
|
+
|
|
47
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
48
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
49
|
+
|
|
50
|
+
// Relations
|
|
51
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
52
|
+
|
|
53
|
+
@@map("subscriptions")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
model Payment {
|
|
57
|
+
id String @id @db.VarChar(255)
|
|
58
|
+
userId String @map("user_id")
|
|
59
|
+
|
|
60
|
+
amount Int
|
|
61
|
+
currency String @default("USD") @db.VarChar(10)
|
|
62
|
+
status String @db.VarChar(50)
|
|
63
|
+
|
|
64
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
65
|
+
|
|
66
|
+
// Relations
|
|
67
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
68
|
+
|
|
69
|
+
@@index([userId], map: "payment_user_idx")
|
|
70
|
+
@@map("payments")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
model WebhookEvent {
|
|
74
|
+
id String @id @db.VarChar(255)
|
|
75
|
+
type String @db.VarChar(100)
|
|
76
|
+
|
|
77
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
78
|
+
|
|
79
|
+
@@map("webhook_events")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
model Notification {
|
|
83
|
+
id String @id @default(uuid())
|
|
84
|
+
userId String @map("user_id")
|
|
85
|
+
|
|
86
|
+
type String @db.VarChar(100)
|
|
87
|
+
sentAt DateTime @default(now()) @map("sent_at")
|
|
88
|
+
|
|
89
|
+
// Relations
|
|
90
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
91
|
+
|
|
92
|
+
@@index([userId], map: "notification_user_idx")
|
|
93
|
+
@@map("notifications")
|
|
94
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { DodoPayments } from "dodopayments";
|
|
2
|
-
|
|
3
|
-
const dodo = new DodoPayments({
|
|
4
|
-
apiKey: process.env.DODO_PAYMENTS_API_KEY,
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
export async function createCheckout(priceId: string, customerEmail: string) {
|
|
8
|
-
return await dodo.checkouts.create({
|
|
9
|
-
product_id: priceId,
|
|
10
|
-
customer: { email: customerEmail },
|
|
11
|
-
billing_address: { country: "US" }, // Or handle dynamically
|
|
12
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import Stripe from "stripe";
|
|
2
|
-
|
|
3
|
-
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
4
|
-
apiVersion: "2023-10-16", // Use the latest stable version
|
|
5
|
-
typescript: true,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export async function createCheckoutSession({
|
|
9
|
-
customerId,
|
|
10
|
-
priceId,
|
|
11
|
-
mode = "subscription", // "subscription" or "payment" (one-time)
|
|
12
|
-
successUrl,
|
|
13
|
-
cancelUrl,
|
|
14
|
-
}: {
|
|
15
|
-
customerId?: string;
|
|
16
|
-
priceId: string;
|
|
17
|
-
mode?: "subscription" | "payment";
|
|
18
|
-
successUrl: string;
|
|
19
|
-
cancelUrl: string;
|
|
20
|
-
}) {
|
|
21
|
-
return await stripe.checkout.sessions.create({
|
|
22
|
-
customer: customerId,
|
|
23
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
24
|
-
mode: mode,
|
|
25
|
-
success_url: successUrl,
|
|
26
|
-
cancel_url: cancelUrl,
|
|
27
|
-
subscription_data: mode === "subscription" ? {
|
|
28
|
-
metadata: { /* add user info here */ },
|
|
29
|
-
} : undefined,
|
|
30
|
-
});
|
|
31
|
-
}
|