create-cronus-stack 0.6.0 → 0.6.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.
- package/dist/scaffold.js +867 -23
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/scaffold.js
CHANGED
|
@@ -41,6 +41,79 @@ function cronusUiPaths(config) {
|
|
|
41
41
|
function usesCronusUi(config) {
|
|
42
42
|
return single(config, "ui") === "ui-cronus";
|
|
43
43
|
}
|
|
44
|
+
const SQL_DATABASES = new Set(["db-sqlite", "db-postgres", "db-mysql"]);
|
|
45
|
+
const HOSTED_DB_SETUPS = new Set([
|
|
46
|
+
"dbsetup-turso",
|
|
47
|
+
"dbsetup-neon",
|
|
48
|
+
"dbsetup-supabase",
|
|
49
|
+
"dbsetup-planetscale",
|
|
50
|
+
"dbsetup-d1",
|
|
51
|
+
"dbsetup-atlas",
|
|
52
|
+
]);
|
|
53
|
+
function sqlDialect(config) {
|
|
54
|
+
switch (single(config, "database")) {
|
|
55
|
+
case "db-sqlite":
|
|
56
|
+
return "sqlite";
|
|
57
|
+
case "db-postgres":
|
|
58
|
+
return "postgresql";
|
|
59
|
+
case "db-mysql":
|
|
60
|
+
return "mysql";
|
|
61
|
+
default:
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function emitsDrizzle(config) {
|
|
66
|
+
return (single(config, "web") === "web-next" &&
|
|
67
|
+
single(config, "orm") === "orm-drizzle" &&
|
|
68
|
+
SQL_DATABASES.has(single(config, "database") ?? ""));
|
|
69
|
+
}
|
|
70
|
+
function emitsBetterAuth(config) {
|
|
71
|
+
return emitsDrizzle(config) && single(config, "auth") === "auth-better-auth";
|
|
72
|
+
}
|
|
73
|
+
function usesImportAlias(config) {
|
|
74
|
+
return single(config, "importAlias") === "import-alias";
|
|
75
|
+
}
|
|
76
|
+
function sourceRoot(config) {
|
|
77
|
+
return single(config, "structure") === "structure-root" ? "" : "src/";
|
|
78
|
+
}
|
|
79
|
+
function dbDir(config) {
|
|
80
|
+
return `${sourceRoot(config)}db`;
|
|
81
|
+
}
|
|
82
|
+
function libDir(config) {
|
|
83
|
+
return `${sourceRoot(config)}lib`;
|
|
84
|
+
}
|
|
85
|
+
function defaultDatabaseUrl(config) {
|
|
86
|
+
switch (single(config, "database")) {
|
|
87
|
+
case "db-sqlite":
|
|
88
|
+
return "file:./data/app.db";
|
|
89
|
+
case "db-postgres":
|
|
90
|
+
return "postgres://postgres:postgres@localhost:5432/app";
|
|
91
|
+
case "db-mysql":
|
|
92
|
+
return "mysql://root:password@localhost:3306/app";
|
|
93
|
+
default:
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function betterAuthProvider(config) {
|
|
98
|
+
switch (single(config, "database")) {
|
|
99
|
+
case "db-postgres":
|
|
100
|
+
return "pg";
|
|
101
|
+
case "db-mysql":
|
|
102
|
+
return "mysql";
|
|
103
|
+
default:
|
|
104
|
+
return "sqlite";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function dbModuleImport(config, subpath) {
|
|
108
|
+
const target = subpath ? `db/${subpath}` : "db";
|
|
109
|
+
return usesImportAlias(config) ? `@/${target}` : `../${target}`;
|
|
110
|
+
}
|
|
111
|
+
function authModuleImport(config) {
|
|
112
|
+
return usesImportAlias(config) ? "@/lib/auth" : "../../../../lib/auth";
|
|
113
|
+
}
|
|
114
|
+
function scriptCommand(pm, script) {
|
|
115
|
+
return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
116
|
+
}
|
|
44
117
|
function packageJson(projectName, config) {
|
|
45
118
|
const isNext = single(config, "web") === "web-next";
|
|
46
119
|
const isCronusUi = usesCronusUi(config);
|
|
@@ -70,6 +143,25 @@ function packageJson(projectName, config) {
|
|
|
70
143
|
add(devDeps, "@commitlint/cli", "^20.2.0");
|
|
71
144
|
add(devDeps, "@commitlint/config-conventional", "^20.2.0");
|
|
72
145
|
}
|
|
146
|
+
if (emitsDrizzle(config)) {
|
|
147
|
+
add(deps, "drizzle-orm", "^0.45.2");
|
|
148
|
+
add(devDeps, "drizzle-kit", "^0.31.10");
|
|
149
|
+
switch (single(config, "database")) {
|
|
150
|
+
case "db-sqlite":
|
|
151
|
+
add(deps, "better-sqlite3", "^12.0.0");
|
|
152
|
+
add(devDeps, "@types/better-sqlite3", "^9.6.0");
|
|
153
|
+
break;
|
|
154
|
+
case "db-postgres":
|
|
155
|
+
add(deps, "postgres", "^3.4.9");
|
|
156
|
+
break;
|
|
157
|
+
case "db-mysql":
|
|
158
|
+
add(deps, "mysql2", "^3.24.2");
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (emitsBetterAuth(config)) {
|
|
163
|
+
add(deps, "better-auth", "^1.7.2");
|
|
164
|
+
}
|
|
73
165
|
const scripts = isNext
|
|
74
166
|
? {
|
|
75
167
|
dev: "next dev",
|
|
@@ -86,7 +178,12 @@ function packageJson(projectName, config) {
|
|
|
86
178
|
scripts.lint = "biome check .";
|
|
87
179
|
scripts.format = "biome format --write .";
|
|
88
180
|
}
|
|
89
|
-
if (
|
|
181
|
+
if (emitsDrizzle(config)) {
|
|
182
|
+
scripts["db:push"] = "drizzle-kit push";
|
|
183
|
+
scripts["db:generate"] = "drizzle-kit generate";
|
|
184
|
+
scripts["db:studio"] = "drizzle-kit studio";
|
|
185
|
+
}
|
|
186
|
+
else if (single(config, "database") !== "db-none") {
|
|
90
187
|
scripts["db:push"] = 'echo "Configure the selected database/ORM before syncing schema."';
|
|
91
188
|
}
|
|
92
189
|
return `${JSON.stringify({
|
|
@@ -214,19 +311,95 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
214
311
|
}
|
|
215
312
|
`;
|
|
216
313
|
}
|
|
217
|
-
function
|
|
314
|
+
function fromAppImport(config, target) {
|
|
315
|
+
return usesImportAlias(config) ? `@/${target}` : `../${target}`;
|
|
316
|
+
}
|
|
317
|
+
function pageStatusCopy(config) {
|
|
318
|
+
if (emitsBetterAuth(config) && emitsDrizzle(config)) {
|
|
319
|
+
return ` {session ? (
|
|
320
|
+
<p className="max-w-prose text-fg-secondary">
|
|
321
|
+
Signed in as {session.user?.email}. {itemCount} items in the active workspace.
|
|
322
|
+
</p>
|
|
323
|
+
) : (
|
|
324
|
+
<p className="max-w-prose text-fg-secondary">
|
|
325
|
+
Sign in required on this stack. Middleware redirects unsigned-in visitors to /login.
|
|
326
|
+
</p>
|
|
327
|
+
)}`;
|
|
328
|
+
}
|
|
329
|
+
if (emitsDrizzle(config)) {
|
|
330
|
+
return ` <p className="max-w-prose text-fg-secondary">
|
|
331
|
+
{itemCount} items in the database. Read KICKOFF.md before changing frameworks,
|
|
332
|
+
databases, auth, payments, or design-system rules.
|
|
333
|
+
</p>`;
|
|
334
|
+
}
|
|
335
|
+
return ` <p className="max-w-prose text-fg-secondary">
|
|
336
|
+
This app was scaffolded from the Cronus Stack Builder. Read KICKOFF.md before changing
|
|
337
|
+
frameworks, databases, auth, payments, or design-system rules.
|
|
338
|
+
</p>`;
|
|
339
|
+
}
|
|
340
|
+
function pageTsx(config) {
|
|
341
|
+
const withDrizzle = emitsDrizzle(config);
|
|
342
|
+
const withAuth = emitsBetterAuth(config);
|
|
343
|
+
const extraImports = [
|
|
344
|
+
withDrizzle && withAuth
|
|
345
|
+
? 'import { count, eq } from "drizzle-orm";'
|
|
346
|
+
: withDrizzle
|
|
347
|
+
? 'import { count } from "drizzle-orm";'
|
|
348
|
+
: undefined,
|
|
349
|
+
withAuth ? 'import { headers } from "next/headers";' : undefined,
|
|
350
|
+
withDrizzle ? `import { db } from "${fromAppImport(config, "db")}";` : undefined,
|
|
351
|
+
withDrizzle && withAuth
|
|
352
|
+
? `import { items, member } from "${fromAppImport(config, "db/schema")}";`
|
|
353
|
+
: withDrizzle
|
|
354
|
+
? `import { items } from "${fromAppImport(config, "db/schema")}";`
|
|
355
|
+
: undefined,
|
|
356
|
+
withAuth ? `import { auth } from "${fromAppImport(config, "lib/auth")}";` : undefined,
|
|
357
|
+
]
|
|
358
|
+
.filter((line) => line !== undefined)
|
|
359
|
+
.join("\n");
|
|
360
|
+
const asyncKw = withDrizzle || withAuth ? "async " : "";
|
|
361
|
+
let loader = "";
|
|
362
|
+
if (withDrizzle && withAuth) {
|
|
363
|
+
loader = ` const session = await auth.api.getSession({ headers: await headers() });
|
|
364
|
+
let orgId = session?.session?.activeOrganizationId ?? null;
|
|
365
|
+
if (!orgId && session?.user?.id) {
|
|
366
|
+
const [row] = await db
|
|
367
|
+
.select({ organizationId: member.organizationId })
|
|
368
|
+
.from(member)
|
|
369
|
+
.where(eq(member.userId, session.user.id))
|
|
370
|
+
.limit(1);
|
|
371
|
+
orgId = row?.organizationId ?? null;
|
|
372
|
+
}
|
|
373
|
+
const [itemRow] = orgId
|
|
374
|
+
? await db.select({ value: count() }).from(items).where(eq(items.workspaceId, orgId))
|
|
375
|
+
: [{ value: 0 }];
|
|
376
|
+
const itemCount = new Intl.NumberFormat("en-US").format(Number(itemRow?.value ?? 0));
|
|
377
|
+
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
else if (withDrizzle) {
|
|
381
|
+
loader = ` const [itemRow] = await db.select({ value: count() }).from(items);
|
|
382
|
+
const itemCount = new Intl.NumberFormat("en-US").format(Number(itemRow?.value ?? 0));
|
|
383
|
+
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
else if (withAuth) {
|
|
387
|
+
loader = ` const session = await auth.api.getSession({ headers: await headers() });
|
|
388
|
+
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
218
391
|
return `import { Badge } from "@cronus-ui/ui/badge";
|
|
219
392
|
import { Button } from "@cronus-ui/ui/button";
|
|
220
393
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@cronus-ui/ui/card";
|
|
221
|
-
|
|
394
|
+
${extraImports ? `${extraImports}\n` : ""}
|
|
222
395
|
const metrics = [
|
|
223
396
|
{ label: "Revenue", value: "R$ 48.2k" },
|
|
224
397
|
{ label: "Active users", value: "2,318" },
|
|
225
398
|
{ label: "NPS", value: "72" },
|
|
226
399
|
];
|
|
227
400
|
|
|
228
|
-
export default function Page() {
|
|
229
|
-
return (
|
|
401
|
+
export default ${asyncKw}function Page() {
|
|
402
|
+
${loader} return (
|
|
230
403
|
<main className="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-8 px-6 py-12">
|
|
231
404
|
<header className="flex flex-col gap-4">
|
|
232
405
|
<Badge variant="primary" className="w-fit">
|
|
@@ -234,10 +407,7 @@ export default function Page() {
|
|
|
234
407
|
</Badge>
|
|
235
408
|
<div className="flex flex-col gap-3">
|
|
236
409
|
<h1 className="text-4xl font-semibold tracking-tight text-fg">Your stack is ready</h1>
|
|
237
|
-
|
|
238
|
-
This app was scaffolded from the Cronus Stack Builder. Read KICKOFF.md before changing
|
|
239
|
-
frameworks, databases, auth, payments, or design-system rules.
|
|
240
|
-
</p>
|
|
410
|
+
${pageStatusCopy(config)}
|
|
241
411
|
</div>
|
|
242
412
|
<div className="flex flex-wrap gap-3">
|
|
243
413
|
<Button variant="primary">Start building</Button>
|
|
@@ -303,8 +473,12 @@ function basicIndex(projectName) {
|
|
|
303
473
|
}
|
|
304
474
|
function readme(projectName, config, unsupported) {
|
|
305
475
|
const pm = packageManagerFromConfig(config);
|
|
306
|
-
const dev = pm === "npm" ? "npm run dev" : `${pm} dev`;
|
|
307
476
|
const install = pm === "yarn" ? "yarn" : `${pm} install`;
|
|
477
|
+
const runLines = [install];
|
|
478
|
+
if (emitsDrizzle(config) && single(config, "database") === "db-sqlite") {
|
|
479
|
+
runLines.push(scriptCommand(pm, "db:push"));
|
|
480
|
+
}
|
|
481
|
+
runLines.push(scriptCommand(pm, "dev"));
|
|
308
482
|
return `# ${projectName}
|
|
309
483
|
|
|
310
484
|
Generated by \`create-cronus-stack\`.
|
|
@@ -315,8 +489,7 @@ conventions, AI capabilities, guardrails, and Definition of Done.
|
|
|
315
489
|
## Run
|
|
316
490
|
|
|
317
491
|
\`\`\`sh
|
|
318
|
-
${
|
|
319
|
-
${dev}
|
|
492
|
+
${runLines.join("\n")}
|
|
320
493
|
\`\`\`
|
|
321
494
|
|
|
322
495
|
## Generated artifacts
|
|
@@ -330,16 +503,42 @@ ${unsupported.length ? `## Manual follow-up\n\n${unsupported.map((item) => `- ${
|
|
|
330
503
|
}
|
|
331
504
|
function envExample(config) {
|
|
332
505
|
const lines = [];
|
|
333
|
-
if (single(config, "database") !== "db-none")
|
|
334
|
-
|
|
335
|
-
|
|
506
|
+
if (single(config, "database") !== "db-none") {
|
|
507
|
+
const url = emitsDrizzle(config) ? (defaultDatabaseUrl(config) ?? "") : "";
|
|
508
|
+
lines.push(`DATABASE_URL=${url}`);
|
|
509
|
+
}
|
|
510
|
+
if (emitsBetterAuth(config)) {
|
|
511
|
+
lines.push("BETTER_AUTH_SECRET=change-me-to-a-32-character-secret");
|
|
512
|
+
lines.push("BETTER_AUTH_URL=http://localhost:3000");
|
|
513
|
+
}
|
|
514
|
+
else if (single(config, "auth") !== "auth-none") {
|
|
336
515
|
lines.push("AUTH_SECRET=");
|
|
516
|
+
}
|
|
337
517
|
if (single(config, "payments") !== "pay-none") {
|
|
338
518
|
lines.push("PAYMENTS_SECRET_KEY=");
|
|
339
519
|
lines.push("PAYMENTS_WEBHOOK_SECRET=");
|
|
340
520
|
}
|
|
341
521
|
return lines.length ? `${lines.join("\n")}\n` : undefined;
|
|
342
522
|
}
|
|
523
|
+
function gitignore(config) {
|
|
524
|
+
const lines = ["node_modules", ".next", "dist", ".env*", "!.env.example", ".DS_Store"];
|
|
525
|
+
if (emitsDrizzle(config)) {
|
|
526
|
+
lines.push("*.db", "data/", "drizzle/");
|
|
527
|
+
}
|
|
528
|
+
return `${lines.join("\n")}\n`;
|
|
529
|
+
}
|
|
530
|
+
function nextConfigMjs(config) {
|
|
531
|
+
const body = emitsDrizzle(config) && single(config, "database") === "db-sqlite"
|
|
532
|
+
? `{
|
|
533
|
+
serverExternalPackages: ["better-sqlite3"],
|
|
534
|
+
}`
|
|
535
|
+
: "{}";
|
|
536
|
+
return `/** @type {import('next').NextConfig} */
|
|
537
|
+
const nextConfig = ${body};
|
|
538
|
+
|
|
539
|
+
export default nextConfig;
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
343
542
|
function assistantIds(config) {
|
|
344
543
|
const picked = new Set();
|
|
345
544
|
for (const id of multi(config, "assistants")) {
|
|
@@ -354,6 +553,636 @@ function assistantIds(config) {
|
|
|
354
553
|
}
|
|
355
554
|
return [...picked];
|
|
356
555
|
}
|
|
556
|
+
const CATALOG_SKILL_TO_KIT = {
|
|
557
|
+
"skill-ui-add": "ui-add",
|
|
558
|
+
"skill-theme": "theme",
|
|
559
|
+
"skill-compose": "compose",
|
|
560
|
+
"skill-upgrade": "upgrade",
|
|
561
|
+
"skill-code-review": "code-review",
|
|
562
|
+
"skill-ship-pr": "ship-pr",
|
|
563
|
+
"skill-evidence-check": "evidence-check",
|
|
564
|
+
};
|
|
565
|
+
/** Catalog skill ids → AI Kit skills. Empty selection keeps the kit default. */
|
|
566
|
+
function kitSkillsFromConfig(config) {
|
|
567
|
+
const selected = multi(config, "skills");
|
|
568
|
+
if (selected.length === 0)
|
|
569
|
+
return undefined;
|
|
570
|
+
const mapped = [];
|
|
571
|
+
for (const id of selected) {
|
|
572
|
+
const skill = CATALOG_SKILL_TO_KIT[id];
|
|
573
|
+
if (skill)
|
|
574
|
+
mapped.push(skill);
|
|
575
|
+
}
|
|
576
|
+
return mapped;
|
|
577
|
+
}
|
|
578
|
+
function drizzleConfig(config) {
|
|
579
|
+
const dialect = sqlDialect(config) ?? "sqlite";
|
|
580
|
+
const fallback = defaultDatabaseUrl(config) ?? "file:./data/app.db";
|
|
581
|
+
if (dialect === "sqlite") {
|
|
582
|
+
return `import { mkdirSync } from "node:fs";
|
|
583
|
+
import { dirname } from "node:path";
|
|
584
|
+
import { defineConfig } from "drizzle-kit";
|
|
585
|
+
|
|
586
|
+
const url = process.env.DATABASE_URL ?? "${fallback}";
|
|
587
|
+
const fileFromUrl = url.startsWith("file:") ? url.slice("file:".length) : url;
|
|
588
|
+
mkdirSync(dirname(fileFromUrl) || ".", { recursive: true });
|
|
589
|
+
|
|
590
|
+
export default defineConfig({
|
|
591
|
+
dialect: "sqlite",
|
|
592
|
+
schema: "./${dbDir(config)}/schema.ts",
|
|
593
|
+
out: "./drizzle",
|
|
594
|
+
dbCredentials: { url },
|
|
595
|
+
});
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
return `import { defineConfig } from "drizzle-kit";
|
|
599
|
+
|
|
600
|
+
export default defineConfig({
|
|
601
|
+
dialect: "${dialect}",
|
|
602
|
+
schema: "./${dbDir(config)}/schema.ts",
|
|
603
|
+
out: "./drizzle",
|
|
604
|
+
dbCredentials: { url: process.env.DATABASE_URL ?? "${fallback}" },
|
|
605
|
+
});
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
function sqliteAuthTables() {
|
|
609
|
+
return `
|
|
610
|
+
export const user = sqliteTable("user", {
|
|
611
|
+
id: text("id").primaryKey(),
|
|
612
|
+
name: text("name").notNull(),
|
|
613
|
+
email: text("email").notNull().unique(),
|
|
614
|
+
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
|
|
615
|
+
image: text("image"),
|
|
616
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
617
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
export const session = sqliteTable("session", {
|
|
621
|
+
id: text("id").primaryKey(),
|
|
622
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
623
|
+
token: text("token").notNull().unique(),
|
|
624
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
625
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
626
|
+
ipAddress: text("ip_address"),
|
|
627
|
+
userAgent: text("user_agent"),
|
|
628
|
+
activeOrganizationId: text("active_organization_id"),
|
|
629
|
+
userId: text("user_id")
|
|
630
|
+
.notNull()
|
|
631
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
export const account = sqliteTable("account", {
|
|
635
|
+
id: text("id").primaryKey(),
|
|
636
|
+
issuer: text("issuer").notNull(),
|
|
637
|
+
accountId: text("account_id").notNull(),
|
|
638
|
+
providerId: text("provider_id").notNull(),
|
|
639
|
+
userId: text("user_id")
|
|
640
|
+
.notNull()
|
|
641
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
642
|
+
accessToken: text("access_token"),
|
|
643
|
+
refreshToken: text("refresh_token"),
|
|
644
|
+
idToken: text("id_token"),
|
|
645
|
+
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
|
|
646
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
|
|
647
|
+
scope: text("scope"),
|
|
648
|
+
password: text("password"),
|
|
649
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
650
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
export const verification = sqliteTable("verification", {
|
|
654
|
+
id: text("id").primaryKey(),
|
|
655
|
+
identifier: text("identifier").notNull(),
|
|
656
|
+
value: text("value").notNull(),
|
|
657
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
658
|
+
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
659
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
export const organization = sqliteTable("organization", {
|
|
663
|
+
id: text("id").primaryKey(),
|
|
664
|
+
name: text("name").notNull(),
|
|
665
|
+
slug: text("slug").notNull().unique(),
|
|
666
|
+
logo: text("logo"),
|
|
667
|
+
metadata: text("metadata"),
|
|
668
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
export const member = sqliteTable("member", {
|
|
672
|
+
id: text("id").primaryKey(),
|
|
673
|
+
organizationId: text("organization_id")
|
|
674
|
+
.notNull()
|
|
675
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
676
|
+
userId: text("user_id")
|
|
677
|
+
.notNull()
|
|
678
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
679
|
+
role: text("role").notNull(),
|
|
680
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
export const invitation = sqliteTable("invitation", {
|
|
684
|
+
id: text("id").primaryKey(),
|
|
685
|
+
organizationId: text("organization_id")
|
|
686
|
+
.notNull()
|
|
687
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
688
|
+
email: text("email").notNull(),
|
|
689
|
+
role: text("role"),
|
|
690
|
+
status: text("status").notNull(),
|
|
691
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
692
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
693
|
+
inviterId: text("inviter_id")
|
|
694
|
+
.notNull()
|
|
695
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
696
|
+
});
|
|
697
|
+
`;
|
|
698
|
+
}
|
|
699
|
+
function pgAuthTables() {
|
|
700
|
+
return `
|
|
701
|
+
export const user = pgTable("user", {
|
|
702
|
+
id: text("id").primaryKey(),
|
|
703
|
+
name: text("name").notNull(),
|
|
704
|
+
email: text("email").notNull().unique(),
|
|
705
|
+
emailVerified: boolean("email_verified").notNull(),
|
|
706
|
+
image: text("image"),
|
|
707
|
+
createdAt: timestamp("created_at").notNull(),
|
|
708
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
export const session = pgTable("session", {
|
|
712
|
+
id: text("id").primaryKey(),
|
|
713
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
714
|
+
token: text("token").notNull().unique(),
|
|
715
|
+
createdAt: timestamp("created_at").notNull(),
|
|
716
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
717
|
+
ipAddress: text("ip_address"),
|
|
718
|
+
userAgent: text("user_agent"),
|
|
719
|
+
activeOrganizationId: text("active_organization_id"),
|
|
720
|
+
userId: text("user_id")
|
|
721
|
+
.notNull()
|
|
722
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
export const account = pgTable("account", {
|
|
726
|
+
id: text("id").primaryKey(),
|
|
727
|
+
issuer: text("issuer").notNull(),
|
|
728
|
+
accountId: text("account_id").notNull(),
|
|
729
|
+
providerId: text("provider_id").notNull(),
|
|
730
|
+
userId: text("user_id")
|
|
731
|
+
.notNull()
|
|
732
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
733
|
+
accessToken: text("access_token"),
|
|
734
|
+
refreshToken: text("refresh_token"),
|
|
735
|
+
idToken: text("id_token"),
|
|
736
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
737
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
738
|
+
scope: text("scope"),
|
|
739
|
+
password: text("password"),
|
|
740
|
+
createdAt: timestamp("created_at").notNull(),
|
|
741
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
export const verification = pgTable("verification", {
|
|
745
|
+
id: text("id").primaryKey(),
|
|
746
|
+
identifier: text("identifier").notNull(),
|
|
747
|
+
value: text("value").notNull(),
|
|
748
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
749
|
+
createdAt: timestamp("created_at"),
|
|
750
|
+
updatedAt: timestamp("updated_at"),
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
export const organization = pgTable("organization", {
|
|
754
|
+
id: text("id").primaryKey(),
|
|
755
|
+
name: text("name").notNull(),
|
|
756
|
+
slug: text("slug").notNull().unique(),
|
|
757
|
+
logo: text("logo"),
|
|
758
|
+
metadata: text("metadata"),
|
|
759
|
+
createdAt: timestamp("created_at").notNull(),
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
export const member = pgTable("member", {
|
|
763
|
+
id: text("id").primaryKey(),
|
|
764
|
+
organizationId: text("organization_id")
|
|
765
|
+
.notNull()
|
|
766
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
767
|
+
userId: text("user_id")
|
|
768
|
+
.notNull()
|
|
769
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
770
|
+
role: text("role").notNull(),
|
|
771
|
+
createdAt: timestamp("created_at").notNull(),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
export const invitation = pgTable("invitation", {
|
|
775
|
+
id: text("id").primaryKey(),
|
|
776
|
+
organizationId: text("organization_id")
|
|
777
|
+
.notNull()
|
|
778
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
779
|
+
email: text("email").notNull(),
|
|
780
|
+
role: text("role"),
|
|
781
|
+
status: text("status").notNull(),
|
|
782
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
783
|
+
createdAt: timestamp("created_at").notNull(),
|
|
784
|
+
inviterId: text("inviter_id")
|
|
785
|
+
.notNull()
|
|
786
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
787
|
+
});
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
function mysqlAuthTables() {
|
|
791
|
+
return `
|
|
792
|
+
export const user = mysqlTable("user", {
|
|
793
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
794
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
795
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
796
|
+
emailVerified: boolean("email_verified").notNull(),
|
|
797
|
+
image: text("image"),
|
|
798
|
+
createdAt: timestamp("created_at").notNull(),
|
|
799
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
export const session = mysqlTable("session", {
|
|
803
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
804
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
805
|
+
token: varchar("token", { length: 255 }).notNull().unique(),
|
|
806
|
+
createdAt: timestamp("created_at").notNull(),
|
|
807
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
808
|
+
ipAddress: text("ip_address"),
|
|
809
|
+
userAgent: text("user_agent"),
|
|
810
|
+
activeOrganizationId: varchar("active_organization_id", { length: 36 }),
|
|
811
|
+
userId: varchar("user_id", { length: 36 })
|
|
812
|
+
.notNull()
|
|
813
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
export const account = mysqlTable("account", {
|
|
817
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
818
|
+
issuer: varchar("issuer", { length: 255 }).notNull(),
|
|
819
|
+
accountId: varchar("account_id", { length: 255 }).notNull(),
|
|
820
|
+
providerId: varchar("provider_id", { length: 255 }).notNull(),
|
|
821
|
+
userId: varchar("user_id", { length: 36 })
|
|
822
|
+
.notNull()
|
|
823
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
824
|
+
accessToken: text("access_token"),
|
|
825
|
+
refreshToken: text("refresh_token"),
|
|
826
|
+
idToken: text("id_token"),
|
|
827
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
828
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
829
|
+
scope: text("scope"),
|
|
830
|
+
password: text("password"),
|
|
831
|
+
createdAt: timestamp("created_at").notNull(),
|
|
832
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
export const verification = mysqlTable("verification", {
|
|
836
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
837
|
+
identifier: varchar("identifier", { length: 255 }).notNull(),
|
|
838
|
+
value: text("value").notNull(),
|
|
839
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
840
|
+
createdAt: timestamp("created_at"),
|
|
841
|
+
updatedAt: timestamp("updated_at"),
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
export const organization = mysqlTable("organization", {
|
|
845
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
846
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
847
|
+
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
|
848
|
+
logo: text("logo"),
|
|
849
|
+
metadata: text("metadata"),
|
|
850
|
+
createdAt: timestamp("created_at").notNull(),
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
export const member = mysqlTable("member", {
|
|
854
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
855
|
+
organizationId: varchar("organization_id", { length: 36 })
|
|
856
|
+
.notNull()
|
|
857
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
858
|
+
userId: varchar("user_id", { length: 36 })
|
|
859
|
+
.notNull()
|
|
860
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
861
|
+
role: varchar("role", { length: 255 }).notNull(),
|
|
862
|
+
createdAt: timestamp("created_at").notNull(),
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
export const invitation = mysqlTable("invitation", {
|
|
866
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
867
|
+
organizationId: varchar("organization_id", { length: 36 })
|
|
868
|
+
.notNull()
|
|
869
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
870
|
+
email: varchar("email", { length: 255 }).notNull(),
|
|
871
|
+
role: varchar("role", { length: 255 }),
|
|
872
|
+
status: varchar("status", { length: 255 }).notNull(),
|
|
873
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
874
|
+
createdAt: timestamp("created_at").notNull(),
|
|
875
|
+
inviterId: varchar("inviter_id", { length: 36 })
|
|
876
|
+
.notNull()
|
|
877
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
878
|
+
});
|
|
879
|
+
`;
|
|
880
|
+
}
|
|
881
|
+
function drizzleSchema(config) {
|
|
882
|
+
const withAuth = emitsBetterAuth(config);
|
|
883
|
+
switch (sqlDialect(config)) {
|
|
884
|
+
case "postgresql": {
|
|
885
|
+
const imports = withAuth
|
|
886
|
+
? 'import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";'
|
|
887
|
+
: 'import { integer, pgTable, text } from "drizzle-orm/pg-core";';
|
|
888
|
+
const workspaceCol = withAuth
|
|
889
|
+
? `\n workspaceId: text("workspace_id").references(() => organization.id, { onDelete: "cascade" }),`
|
|
890
|
+
: "";
|
|
891
|
+
return `${imports}
|
|
892
|
+
${withAuth ? pgAuthTables() : ""}
|
|
893
|
+
export const items = pgTable("items", {
|
|
894
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
895
|
+
title: text("title").notNull(),${workspaceCol}
|
|
896
|
+
});
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
case "mysql": {
|
|
900
|
+
const imports = withAuth
|
|
901
|
+
? 'import { boolean, int, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";'
|
|
902
|
+
: 'import { int, mysqlTable, varchar } from "drizzle-orm/mysql-core";';
|
|
903
|
+
const workspaceCol = withAuth
|
|
904
|
+
? `\n workspaceId: varchar("workspace_id", { length: 36 }).references(() => organization.id, { onDelete: "cascade" }),`
|
|
905
|
+
: "";
|
|
906
|
+
return `${imports}
|
|
907
|
+
${withAuth ? mysqlAuthTables() : ""}
|
|
908
|
+
export const items = mysqlTable("items", {
|
|
909
|
+
id: int("id").autoincrement().primaryKey(),
|
|
910
|
+
title: varchar("title", { length: 255 }).notNull(),${workspaceCol}
|
|
911
|
+
});
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
914
|
+
default: {
|
|
915
|
+
const workspaceCol = withAuth
|
|
916
|
+
? `\n workspaceId: text("workspace_id").references(() => organization.id, { onDelete: "cascade" }),`
|
|
917
|
+
: "";
|
|
918
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
919
|
+
${withAuth ? sqliteAuthTables() : ""}
|
|
920
|
+
export const items = sqliteTable("items", {
|
|
921
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
922
|
+
title: text("title").notNull(),${workspaceCol}
|
|
923
|
+
});
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function drizzleClient(config) {
|
|
929
|
+
const fallback = defaultDatabaseUrl(config) ?? "file:./data/app.db";
|
|
930
|
+
switch (sqlDialect(config)) {
|
|
931
|
+
case "postgresql":
|
|
932
|
+
return `import { drizzle } from "drizzle-orm/postgres-js";
|
|
933
|
+
import postgres from "postgres";
|
|
934
|
+
import * as schema from "./schema";
|
|
935
|
+
|
|
936
|
+
const url = process.env.DATABASE_URL ?? "${fallback}";
|
|
937
|
+
const client = postgres(url);
|
|
938
|
+
|
|
939
|
+
export const db = drizzle(client, { schema });
|
|
940
|
+
`;
|
|
941
|
+
case "mysql":
|
|
942
|
+
return `import { drizzle } from "drizzle-orm/mysql2";
|
|
943
|
+
import mysql from "mysql2/promise";
|
|
944
|
+
import * as schema from "./schema";
|
|
945
|
+
|
|
946
|
+
const url = process.env.DATABASE_URL ?? "${fallback}";
|
|
947
|
+
const pool = mysql.createPool(url);
|
|
948
|
+
|
|
949
|
+
export const db = drizzle(pool, { schema, mode: "default" });
|
|
950
|
+
`;
|
|
951
|
+
default:
|
|
952
|
+
return `import { mkdirSync } from "node:fs";
|
|
953
|
+
import { dirname } from "node:path";
|
|
954
|
+
import Database from "better-sqlite3";
|
|
955
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
956
|
+
import * as schema from "./schema";
|
|
957
|
+
|
|
958
|
+
const url = process.env.DATABASE_URL ?? "${fallback}";
|
|
959
|
+
const fileFromUrl = url.startsWith("file:") ? url.slice("file:".length) : url;
|
|
960
|
+
mkdirSync(dirname(fileFromUrl) || ".", { recursive: true });
|
|
961
|
+
const sqlite = new Database(fileFromUrl);
|
|
962
|
+
|
|
963
|
+
export const db = drizzle(sqlite, { schema });
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function betterAuthServer(config) {
|
|
968
|
+
const provider = betterAuthProvider(config);
|
|
969
|
+
const dbImport = dbModuleImport(config);
|
|
970
|
+
const schemaImport = dbModuleImport(config, "schema");
|
|
971
|
+
return `import { betterAuth } from "better-auth";
|
|
972
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
973
|
+
import { nextCookies } from "better-auth/next-js";
|
|
974
|
+
import { organization } from "better-auth/plugins";
|
|
975
|
+
import { and, eq } from "drizzle-orm";
|
|
976
|
+
import { db } from "${dbImport}";
|
|
977
|
+
import * as schema from "${schemaImport}";
|
|
978
|
+
import {
|
|
979
|
+
invitation as invitationTable,
|
|
980
|
+
member,
|
|
981
|
+
organization as organizationTable,
|
|
982
|
+
user as userTable,
|
|
983
|
+
} from "${schemaImport}";
|
|
984
|
+
|
|
985
|
+
function newId(): string {
|
|
986
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export const auth = betterAuth({
|
|
990
|
+
database: drizzleAdapter(db, { provider: "${provider}", schema }),
|
|
991
|
+
emailAndPassword: {
|
|
992
|
+
enabled: true,
|
|
993
|
+
sendResetPassword: async ({ url }) => {
|
|
994
|
+
console.info("Password reset URL:", url);
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
databaseHooks: {
|
|
998
|
+
session: {
|
|
999
|
+
create: {
|
|
1000
|
+
before: async (session) => {
|
|
1001
|
+
const [existing] = await db
|
|
1002
|
+
.select({ organizationId: member.organizationId })
|
|
1003
|
+
.from(member)
|
|
1004
|
+
.where(eq(member.userId, session.userId))
|
|
1005
|
+
.limit(1);
|
|
1006
|
+
if (existing?.organizationId) {
|
|
1007
|
+
return { data: { ...session, activeOrganizationId: existing.organizationId } };
|
|
1008
|
+
}
|
|
1009
|
+
const [owner] = await db
|
|
1010
|
+
.select({ name: userTable.name, email: userTable.email })
|
|
1011
|
+
.from(userTable)
|
|
1012
|
+
.where(eq(userTable.id, session.userId))
|
|
1013
|
+
.limit(1);
|
|
1014
|
+
if (owner?.email) {
|
|
1015
|
+
const [pending] = await db
|
|
1016
|
+
.select({ id: invitationTable.id })
|
|
1017
|
+
.from(invitationTable)
|
|
1018
|
+
.where(
|
|
1019
|
+
and(eq(invitationTable.email, owner.email), eq(invitationTable.status, "pending")),
|
|
1020
|
+
)
|
|
1021
|
+
.limit(1);
|
|
1022
|
+
if (pending) return;
|
|
1023
|
+
}
|
|
1024
|
+
const orgId = newId();
|
|
1025
|
+
const now = new Date();
|
|
1026
|
+
await db.insert(organizationTable).values({
|
|
1027
|
+
id: orgId,
|
|
1028
|
+
name: owner?.name.trim() || "Workspace",
|
|
1029
|
+
slug: \`ws-\${session.userId.slice(0, 16)}\`,
|
|
1030
|
+
createdAt: now,
|
|
1031
|
+
});
|
|
1032
|
+
await db.insert(member).values({
|
|
1033
|
+
id: newId(),
|
|
1034
|
+
organizationId: orgId,
|
|
1035
|
+
userId: session.userId,
|
|
1036
|
+
role: "owner",
|
|
1037
|
+
createdAt: now,
|
|
1038
|
+
});
|
|
1039
|
+
return { data: { ...session, activeOrganizationId: orgId } };
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
plugins: [
|
|
1045
|
+
organization({
|
|
1046
|
+
sendInvitationEmail: async (data) => {
|
|
1047
|
+
const base = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
|
|
1048
|
+
console.info(\`Invite \${data.email}: \${base}/accept-invitation?id=\${data.id}\`);
|
|
1049
|
+
},
|
|
1050
|
+
}),
|
|
1051
|
+
nextCookies(),
|
|
1052
|
+
],
|
|
1053
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
1054
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
1055
|
+
});
|
|
1056
|
+
`;
|
|
1057
|
+
}
|
|
1058
|
+
function betterAuthMiddleware() {
|
|
1059
|
+
return `import { NextRequest, NextResponse } from "next/server";
|
|
1060
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
1061
|
+
|
|
1062
|
+
const AUTH_PAGES = ["/login", "/signup", "/forgot-password"];
|
|
1063
|
+
|
|
1064
|
+
function invitationOf(request: NextRequest): string | null {
|
|
1065
|
+
const { pathname, searchParams } = request.nextUrl;
|
|
1066
|
+
return (
|
|
1067
|
+
searchParams.get("invitation") ??
|
|
1068
|
+
(pathname === "/accept-invitation" ? searchParams.get("id") : null)
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function middleware(request: NextRequest) {
|
|
1073
|
+
const sessionCookie = getSessionCookie(request);
|
|
1074
|
+
const path = request.nextUrl.pathname;
|
|
1075
|
+
const isAuthPage = AUTH_PAGES.includes(path);
|
|
1076
|
+
const invitation = invitationOf(request);
|
|
1077
|
+
if (!sessionCookie && path === "/accept-invitation") {
|
|
1078
|
+
const url = new URL("/signup", request.url);
|
|
1079
|
+
if (invitation) url.searchParams.set("invitation", invitation);
|
|
1080
|
+
return NextResponse.redirect(url);
|
|
1081
|
+
}
|
|
1082
|
+
if (!sessionCookie && !isAuthPage) {
|
|
1083
|
+
const url = new URL("/login", request.url);
|
|
1084
|
+
if (invitation) url.searchParams.set("invitation", invitation);
|
|
1085
|
+
return NextResponse.redirect(url);
|
|
1086
|
+
}
|
|
1087
|
+
if (sessionCookie && isAuthPage) {
|
|
1088
|
+
if (invitation) {
|
|
1089
|
+
return NextResponse.redirect(
|
|
1090
|
+
new URL(\`/accept-invitation?id=\${encodeURIComponent(invitation)}\`, request.url),
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
1094
|
+
}
|
|
1095
|
+
return NextResponse.next();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export const config = {
|
|
1099
|
+
matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico|.*\\\\..*).*)"],
|
|
1100
|
+
};
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
function acceptInvitationPage(config) {
|
|
1104
|
+
const authClientImport = fromAppImport(config, "lib/auth-client");
|
|
1105
|
+
return `"use client";
|
|
1106
|
+
|
|
1107
|
+
import { Suspense, useEffect, useState } from "react";
|
|
1108
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
1109
|
+
import { authClient } from "${authClientImport}";
|
|
1110
|
+
|
|
1111
|
+
function AcceptInvitation() {
|
|
1112
|
+
const router = useRouter();
|
|
1113
|
+
const params = useSearchParams();
|
|
1114
|
+
const id = params.get("id") ?? params.get("invitation");
|
|
1115
|
+
const { data: session, isPending } = authClient.useSession();
|
|
1116
|
+
const [error, setError] = useState<string | null>(null);
|
|
1117
|
+
|
|
1118
|
+
useEffect(() => {
|
|
1119
|
+
if (isPending) return;
|
|
1120
|
+
if (!id) {
|
|
1121
|
+
setError("Invitation is missing.");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!session) {
|
|
1125
|
+
router.replace(\`/signup?invitation=\${encodeURIComponent(id)}\`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
let cancelled = false;
|
|
1129
|
+
void (async () => {
|
|
1130
|
+
const { data, error: acceptError } = await authClient.organization.acceptInvitation({
|
|
1131
|
+
invitationId: id,
|
|
1132
|
+
});
|
|
1133
|
+
if (cancelled) return;
|
|
1134
|
+
if (acceptError) {
|
|
1135
|
+
setError(acceptError.message || "Could not accept invitation.");
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const orgId = data?.invitation?.organizationId ?? data?.member?.organizationId;
|
|
1139
|
+
if (orgId) {
|
|
1140
|
+
await authClient.organization.setActive({ organizationId: orgId });
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
sessionStorage.removeItem("cronus-invitation");
|
|
1144
|
+
} catch {
|
|
1145
|
+
// ignore
|
|
1146
|
+
}
|
|
1147
|
+
window.location.assign("/");
|
|
1148
|
+
})();
|
|
1149
|
+
return () => {
|
|
1150
|
+
cancelled = true;
|
|
1151
|
+
};
|
|
1152
|
+
}, [id, isPending, router, session]);
|
|
1153
|
+
|
|
1154
|
+
return (
|
|
1155
|
+
<main>
|
|
1156
|
+
{error ? <p role="alert">{error}</p> : <p>Accepting invitation…</p>}
|
|
1157
|
+
</main>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export default function AcceptInvitationPage() {
|
|
1162
|
+
return (
|
|
1163
|
+
<Suspense fallback={<main><p>Accepting invitation…</p></main>}>
|
|
1164
|
+
<AcceptInvitation />
|
|
1165
|
+
</Suspense>
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
function betterAuthClient() {
|
|
1171
|
+
return `import { organizationClient } from "better-auth/client/plugins";
|
|
1172
|
+
import { createAuthClient } from "better-auth/react";
|
|
1173
|
+
|
|
1174
|
+
export const authClient = createAuthClient({
|
|
1175
|
+
plugins: [organizationClient()],
|
|
1176
|
+
});
|
|
1177
|
+
`;
|
|
1178
|
+
}
|
|
1179
|
+
function betterAuthRoute(config) {
|
|
1180
|
+
return `import { auth } from "${authModuleImport(config)}";
|
|
1181
|
+
import { toNextJsHandler } from "better-auth/next-js";
|
|
1182
|
+
|
|
1183
|
+
export const { GET, POST } = toNextJsHandler(auth);
|
|
1184
|
+
`;
|
|
1185
|
+
}
|
|
357
1186
|
function unsupportedNotes(config) {
|
|
358
1187
|
const notes = [];
|
|
359
1188
|
if (single(config, "web") !== "web-next") {
|
|
@@ -367,10 +1196,15 @@ function unsupportedNotes(config) {
|
|
|
367
1196
|
single(config, "backend") !== "backend-fullstack-next") {
|
|
368
1197
|
notes.push("Add the selected dedicated backend service described in KICKOFF.md.");
|
|
369
1198
|
}
|
|
370
|
-
if (
|
|
1199
|
+
if (emitsDrizzle(config)) {
|
|
1200
|
+
if (HOSTED_DB_SETUPS.has(single(config, "dbSetup") ?? "")) {
|
|
1201
|
+
notes.push("Configure the selected hosted database provider; generated Drizzle files use a local DATABASE_URL.");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
else if (single(config, "database") !== "db-none") {
|
|
371
1205
|
notes.push("Wire the selected database/ORM/provider before running db:push.");
|
|
372
1206
|
}
|
|
373
|
-
if (single(config, "auth") !== "auth-none") {
|
|
1207
|
+
if (!emitsBetterAuth(config) && single(config, "auth") !== "auth-none") {
|
|
374
1208
|
notes.push("Implement the selected auth provider and protect mutating routes.");
|
|
375
1209
|
}
|
|
376
1210
|
if (single(config, "payments") !== "pay-none") {
|
|
@@ -388,9 +1222,6 @@ function unsupportedNotes(config) {
|
|
|
388
1222
|
if (mcpIds.includes("mcp-cronus-ui") && !usesCronusUi(config)) {
|
|
389
1223
|
notes.push("The cronus-ui MCP server is not generated for stacks that do not use Cronus UI.");
|
|
390
1224
|
}
|
|
391
|
-
if (multi(config, "skills").length > 0) {
|
|
392
|
-
notes.push("Install the selected agent skill packs in your agent environment; KICKOFF.md records the choices but the scaffold does not install external skills.");
|
|
393
|
-
}
|
|
394
1225
|
const unsupportedAddons = multi(config, "addons").filter((id) => id !== "addon-biome");
|
|
395
1226
|
if (unsupportedAddons.length > 0) {
|
|
396
1227
|
notes.push("Wire the selected addons manually unless noted otherwise; this generator currently scaffolds Biome config and records the rest in KICKOFF.md.");
|
|
@@ -418,7 +1249,7 @@ export function scaffoldStack(options) {
|
|
|
418
1249
|
emit("README.md", readme(projectName, config, unsupported));
|
|
419
1250
|
emit("KICKOFF.md", generateKickoff(config, projectName, catalog));
|
|
420
1251
|
emit("stack.json", `${generateStackJson(config, projectName)}\n`);
|
|
421
|
-
emit(".gitignore",
|
|
1252
|
+
emit(".gitignore", gitignore(config));
|
|
422
1253
|
emit("tsconfig.json", tsconfig(config));
|
|
423
1254
|
emit(".env.example", envExample(config));
|
|
424
1255
|
if (single(config, "commitStyle") === "commit-conventional") {
|
|
@@ -429,13 +1260,13 @@ export function scaffoldStack(options) {
|
|
|
429
1260
|
}
|
|
430
1261
|
if (single(config, "web") === "web-next") {
|
|
431
1262
|
const isCronusUi = usesCronusUi(config);
|
|
432
|
-
emit("next.config.mjs",
|
|
1263
|
+
emit("next.config.mjs", nextConfigMjs(config));
|
|
433
1264
|
const app = appDir(config);
|
|
434
1265
|
if (isCronusUi) {
|
|
435
1266
|
emit("postcss.config.mjs", 'export default { plugins: { "@tailwindcss/postcss": {} } };\n');
|
|
436
1267
|
emit(`${app}/globals.css`, globalsCss(config));
|
|
437
1268
|
emit(`${app}/layout.tsx`, layoutTsx(projectName));
|
|
438
|
-
emit(`${app}/page.tsx`, pageTsx());
|
|
1269
|
+
emit(`${app}/page.tsx`, pageTsx(config));
|
|
439
1270
|
emit("cronus-ui.json", `${JSON.stringify({
|
|
440
1271
|
aliases: { ui: "@/components/ui", lib: "@/lib", blocks: "@/components/blocks" },
|
|
441
1272
|
paths: cronusUiPaths(config),
|
|
@@ -448,6 +1279,18 @@ export function scaffoldStack(options) {
|
|
|
448
1279
|
emit(`${app}/layout.tsx`, neutralLayoutTsx(projectName));
|
|
449
1280
|
emit(`${app}/page.tsx`, neutralPageTsx(config));
|
|
450
1281
|
}
|
|
1282
|
+
if (emitsDrizzle(config)) {
|
|
1283
|
+
emit("drizzle.config.ts", drizzleConfig(config));
|
|
1284
|
+
emit(`${dbDir(config)}/schema.ts`, drizzleSchema(config));
|
|
1285
|
+
emit(`${dbDir(config)}/index.ts`, drizzleClient(config));
|
|
1286
|
+
}
|
|
1287
|
+
if (emitsBetterAuth(config)) {
|
|
1288
|
+
emit(`${libDir(config)}/auth.ts`, betterAuthServer(config));
|
|
1289
|
+
emit(`${libDir(config)}/auth-client.ts`, betterAuthClient());
|
|
1290
|
+
emit(`${app}/api/auth/[...all]/route.ts`, betterAuthRoute(config));
|
|
1291
|
+
emit(single(config, "structure") === "structure-root" ? "middleware.ts" : "src/middleware.ts", betterAuthMiddleware());
|
|
1292
|
+
emit(`${app}/accept-invitation/page.tsx`, acceptInvitationPage(config));
|
|
1293
|
+
}
|
|
451
1294
|
}
|
|
452
1295
|
else {
|
|
453
1296
|
emit("src/index.ts", basicIndex(projectName));
|
|
@@ -461,6 +1304,7 @@ export function scaffoldStack(options) {
|
|
|
461
1304
|
name: projectName,
|
|
462
1305
|
assistants,
|
|
463
1306
|
preset: "standard",
|
|
1307
|
+
skills: kitSkillsFromConfig(config),
|
|
464
1308
|
includeCronusUi: isCronusUi,
|
|
465
1309
|
cronusUiMcp,
|
|
466
1310
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CREATE_STACK_VERSION = "0.6.
|
|
1
|
+
export declare const CREATE_STACK_VERSION = "0.6.2";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const CREATE_STACK_VERSION = "0.6.
|
|
1
|
+
export const CREATE_STACK_VERSION = "0.6.2";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cronus-stack",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Scaffold a Cronus stack from the Cronus Stack Builder contract: app files, stack.json, KICKOFF.md, and optional AI Kit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"prepublishOnly": "tsc -p tsconfig.json"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@cronus-ui/ai-kit": "0.6.
|
|
48
|
-
"@cronus-ui/stack": "0.6.
|
|
47
|
+
"@cronus-ui/ai-kit": "0.6.2",
|
|
48
|
+
"@cronus-ui/stack": "0.6.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.10.0",
|