scale-stack 0.0.1-alpha.1 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +97 -3
  3. package/dist/index.js +4218 -16
  4. package/package.json +22 -10
  5. package/templates/ai-chat/chat-panel.tsx.ejs +70 -0
  6. package/templates/ai-chat/layout.tsx.ejs +39 -0
  7. package/templates/ai-chat/page.tsx.ejs +31 -0
  8. package/templates/ai-chat/route.ts.ejs +68 -0
  9. package/templates/ai-chat/use-chat.ts.ejs +26 -0
  10. package/templates/analytics/analytics-provider.tsx.ejs +32 -0
  11. package/templates/analytics/analytics.ts.ejs +40 -0
  12. package/templates/auth/auth-client.ts.ejs +7 -0
  13. package/templates/auth/auth.ts.ejs +45 -0
  14. package/templates/auth/route.ts.ejs +4 -0
  15. package/templates/auth/sign-in-page.tsx.ejs +122 -0
  16. package/templates/auth/sign-up-page.tsx.ejs +137 -0
  17. package/templates/auth/unauthorized.tsx.ejs +28 -0
  18. package/templates/core/layout.tsx.ejs +36 -0
  19. package/templates/core/loading.tsx.ejs +7 -0
  20. package/templates/core/next.config.ts.ejs +33 -0
  21. package/templates/error-handling/catch-all-not-found-page.tsx.ejs +5 -0
  22. package/templates/error-handling/error.tsx.ejs +33 -0
  23. package/templates/error-handling/global-error.tsx.ejs +32 -0
  24. package/templates/error-handling/not-found.tsx.ejs +28 -0
  25. package/templates/eslint-prettier/.prettierignore.ejs +29 -0
  26. package/templates/eslint-prettier/eslint.config.mjs.ejs +31 -0
  27. package/templates/form-handling/dashboard-page.tsx.ejs +33 -0
  28. package/templates/form-handling/example-form-action.ts.ejs +50 -0
  29. package/templates/form-handling/example-form-schema.ts.ejs +87 -0
  30. package/templates/form-handling/example-form.tsx.ejs +428 -0
  31. package/templates/i18n/ar.json.ejs +77 -0
  32. package/templates/i18n/en.json.ejs +77 -0
  33. package/templates/i18n/locale-layout.tsx.ejs +45 -0
  34. package/templates/i18n/navigation.ts.ejs +5 -0
  35. package/templates/i18n/next-intl.d.ts.ejs +9 -0
  36. package/templates/i18n/request.ts.ejs +15 -0
  37. package/templates/i18n/routing.ts.ejs +7 -0
  38. package/templates/orm/prisma.config.ts.ejs +12 -0
  39. package/templates/orm/prisma.ts.ejs +17 -0
  40. package/templates/orm/schema.prisma.ejs +8 -0
  41. package/templates/pre-commit/prek.toml.ejs +35 -0
  42. package/templates/proxy/proxy.ts.ejs +81 -0
  43. package/templates/server-actions/safe-action.ts.ejs +51 -0
  44. package/templates/ui/client-side-wrappers.tsx.ejs +19 -0
  45. package/templates/ui/page.tsx.ejs +111 -0
@@ -0,0 +1,12 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ export default defineConfig({
5
+ schema: "prisma/schema.prisma",
6
+ migrations: {
7
+ path: "prisma/migrations",
8
+ },
9
+ datasource: {
10
+ url: env("DATABASE_URL"),
11
+ },
12
+ });
@@ -0,0 +1,17 @@
1
+ import { env } from "@/env";
2
+ import { PrismaPg } from "@prisma/adapter-pg";
3
+ import { PrismaClient } from "../../prisma/generated/client";
4
+
5
+ const globalForPrisma = globalThis as unknown as {
6
+ prisma?: PrismaClient;
7
+ };
8
+
9
+ export const prisma =
10
+ globalForPrisma.prisma ??
11
+ new PrismaClient({
12
+ adapter: new PrismaPg({ connectionString: env.DATABASE_URL }),
13
+ });
14
+
15
+ if (process.env.NODE_ENV !== "production") {
16
+ globalForPrisma.prisma = prisma;
17
+ }
@@ -0,0 +1,8 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "./generated"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ }
@@ -0,0 +1,35 @@
1
+ # --- Built-in fast hooks (offline, zero setup) ---
2
+ [[repos]]
3
+ repo = "builtin"
4
+ hooks = [
5
+ { id = "trailing-whitespace" },
6
+ { id = "end-of-file-fixer" },
7
+ { id = "check-yaml" },
8
+ { id = "check-json" },
9
+ ]
10
+
11
+ # --- Local project hooks (use package.json scripts) ---
12
+ [[repos]]
13
+ repo = "local"
14
+
15
+ [[repos.hooks]]
16
+ id = "eslint"
17
+ name = "eslint"
18
+ language = "system"
19
+ entry = "pnpm lint"
20
+ files = { glob = "*.{ts,tsx,js,jsx,mjs}" }
21
+ pass_filenames = false
22
+
23
+ [[repos.hooks]]
24
+ id = "prettier"
25
+ name = "prettier"
26
+ language = "system"
27
+ entry = "pnpm format"
28
+ files = { glob = "*.{ts,tsx,js,jsx,json,css,md}" }
29
+
30
+ [[repos.hooks]]
31
+ id = "typecheck"
32
+ name = "typecheck"
33
+ language = "system"
34
+ entry = "pnpm typecheck"
35
+ pass_filenames = false
@@ -0,0 +1,81 @@
1
+ import type { NextRequest } from "next/server";
2
+ <% if (hasAuth || !hasI18n) { %>
3
+ import { NextResponse } from "next/server";
4
+ <% } %>
5
+ <% if (hasI18n) { %>import createMiddleware from "next-intl/middleware";
6
+ <% } %><% if (hasAuth) { %>import { getSessionCookie } from "better-auth/cookies";
7
+ <% } %><% if (hasI18n) { %>import { routing } from "./i18n/routing";
8
+ <% if (hasAuth) { %>import { getPathname } from "./i18n/navigation";
9
+ <% } %><% } %>
10
+ <% if (hasI18n) { %>const intlProxy = createMiddleware(routing);
11
+
12
+ <% } %><% if (hasAuth) { %>const PROTECTED_ROUTE_PREFIXES = ["/dashboard"] as const;
13
+
14
+ function normalizePathname(pathname: string): string {
15
+ <% if (hasI18n) { %> const [, maybeLocale, ...rest] = pathname.split("/");
16
+ const normalizedPathname = routing.locales.some(
17
+ (locale) => locale === maybeLocale,
18
+ )
19
+ ? `/${rest.join("/")}`
20
+ : pathname;
21
+
22
+ <% } else { %> const normalizedPathname = pathname;
23
+ <% } %> return normalizedPathname === "/"
24
+ ? "/"
25
+ : normalizedPathname.replace(/\/$/, "");
26
+ }
27
+
28
+ function isProtectedRoute(pathname: string): boolean {
29
+ const normalizedPathname = normalizePathname(pathname);
30
+
31
+ return PROTECTED_ROUTE_PREFIXES.some(
32
+ (prefix) =>
33
+ normalizedPathname === prefix ||
34
+ normalizedPathname.startsWith(`${prefix}/`),
35
+ );
36
+ }
37
+
38
+ function buildSignInUrl(request: NextRequest): URL {
39
+ const url = request.nextUrl.clone();
40
+ <% if (hasI18n) { %> const [, maybeLocale] = request.nextUrl.pathname.split("/");
41
+ const locale =
42
+ routing.locales.find((item) => item === maybeLocale) ??
43
+ routing.defaultLocale;
44
+
45
+ url.pathname = getPathname({ locale, href: "/sign-in" });
46
+ <% } else { %> url.pathname = "/sign-in";
47
+ <% } %> url.searchParams.set(
48
+ "callbackUrl",
49
+ `${request.nextUrl.pathname}${request.nextUrl.search}`,
50
+ );
51
+ return url;
52
+ }
53
+
54
+ function authProxy(request: NextRequest): NextResponse | null {
55
+ if (!isProtectedRoute(request.nextUrl.pathname)) {
56
+ return null;
57
+ }
58
+
59
+ // This proxy check is only for fast redirects; validate the session again
60
+ // before reading protected data in pages, route handlers, and actions.
61
+ if (getSessionCookie(request)) {
62
+ return null;
63
+ }
64
+
65
+ return NextResponse.redirect(buildSignInUrl(request));
66
+ }
67
+
68
+ <% } %>export default function proxy(<% if (hasAuth || hasI18n) { %>request<% } else { %>_request<% } %>: NextRequest) {
69
+ <% if (hasAuth) { %> const authResponse = authProxy(request);
70
+
71
+ if (authResponse) {
72
+ return authResponse;
73
+ }
74
+
75
+ <% } %><% if (hasI18n) { %> return intlProxy(request);
76
+ <% } else { %> return NextResponse.next();
77
+ <% } %>}
78
+
79
+ export const config = {
80
+ matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
81
+ };
@@ -0,0 +1,51 @@
1
+ import { createSafeActionClient } from "next-safe-action";
2
+ <% if (hasAuth) { %>import { betterAuth } from "@next-safe-action/adapter-better-auth";
3
+ import { auth } from "@/lib/auth";
4
+ <% } %>import { z } from "zod";
5
+
6
+ const actionMetadataSchema = z.object({
7
+ actionName: z.string().min(1),
8
+ });
9
+
10
+ type ActionMetadata = z.infer<typeof actionMetadataSchema>;
11
+ type ActionOutcome = "success" | "error";
12
+
13
+ function logAction(
14
+ metadata: ActionMetadata | undefined,
15
+ durationMs: number,
16
+ outcome: ActionOutcome,
17
+ ): void {
18
+ console.info("[safe-action]", {
19
+ actionName: metadata?.actionName ?? "unknown",
20
+ durationMs,
21
+ outcome,
22
+ });
23
+ }
24
+ export const actionClient = createSafeActionClient({
25
+ defaultValidationErrorsShape: "flattened",
26
+ defineMetadataSchema() {
27
+ return actionMetadataSchema;
28
+ },
29
+ handleServerError(error, { metadata }) {
30
+ console.error("[safe-action] server error", {
31
+ actionName: metadata?.actionName ?? "unknown",
32
+ error,
33
+ });
34
+
35
+ return "Something went wrong. Please try again.";
36
+ },
37
+ }).use(async ({ next, metadata }) => {
38
+ const startedAt = performance.now();
39
+
40
+ try {
41
+ const result = await next();
42
+ logAction(metadata, Math.round(performance.now() - startedAt), "success");
43
+ return result;
44
+ } catch (error) {
45
+ logAction(metadata, Math.round(performance.now() - startedAt), "error");
46
+ throw error;
47
+ }
48
+ });
49
+ <% if (hasAuth) { %>
50
+ export const authActionClient = actionClient.use(betterAuth(auth));
51
+ <% } %>
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { NuqsAdapter } from "nuqs/adapters/next/app";
5
+ import { Toaster } from "@/components/ui/sonner";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ <% if (hasAnalyticsProvider) { %>import { AnalyticsProvider } from "./analytics-provider";
8
+ <% } %>
9
+
10
+ export function ClientSideWrappers({ children }: { children: ReactNode }) {
11
+ return (
12
+ <NuqsAdapter>
13
+ <TooltipProvider>
14
+ <% if (hasAnalyticsProvider) { %><AnalyticsProvider>{children}</AnalyticsProvider><% } else { %>{children}<% } %>
15
+ <Toaster />
16
+ </TooltipProvider>
17
+ </NuqsAdapter>
18
+ );
19
+ }
@@ -0,0 +1,111 @@
1
+ <% if (i18n) { %>import { getTranslations } from "next-intl/server";
2
+ import { Link } from "@/i18n/navigation";
3
+ <% } else { %>import Link from "next/link";
4
+ <% } %>import { Button } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+
13
+ export default <% if (i18n) { %>async <% } %>function Home() {
14
+ <% if (i18n) { %> const t = await getTranslations("Home");
15
+
16
+ <% } %>
17
+ return (
18
+ <main className="relative flex flex-1 flex-col overflow-hidden px-6 py-16">
19
+ <div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,var(--muted),transparent_34rem),radial-gradient(circle_at_bottom_right,var(--accent),transparent_28rem)]" />
20
+ <section className="mx-auto flex w-full max-w-6xl flex-col gap-10">
21
+ <div className="max-w-3xl space-y-6">
22
+ <p className="text-sm font-medium tracking-[0.2em] text-muted-foreground uppercase">
23
+ <% if (i18n) { %>{t("eyebrow")}<% } else { %>Scale Stack<% } %>
24
+ </p>
25
+ <div className="space-y-4">
26
+ <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-6xl">
27
+ <%= projectName %>
28
+ </h1>
29
+ <p className="max-w-2xl text-lg text-muted-foreground text-balance">
30
+ <% if (i18n) { %>{t("description")}<% } else { %>Your app is wired with shadcn/ui, Tailwind CSS 4, typed server
31
+ actions, form handling, and the Scale Stack guardrails agents
32
+ need to keep moving safely.<% } %>
33
+ </p>
34
+ </div>
35
+ </div>
36
+
37
+ <div className="space-y-3">
38
+ <div>
39
+ <h2 className="text-2xl font-semibold tracking-tight">
40
+ <% if (i18n) { %>{t("explorePages")}<% } else { %>Explore generated pages<% } %>
41
+ </h2>
42
+ <p className="text-muted-foreground">
43
+ <% if (i18n) { %>{t("explorePagesDescription")}<% } else { %>Each card links to a real route in this scaffold.<% } %>
44
+ </p>
45
+ </div>
46
+ <div className="grid gap-4 md:grid-cols-2<% if (hasAuth && hasAiChat) { %> xl:grid-cols-4<% } else { %> xl:grid-cols-3<% } %>">
47
+ <Card className="transition-colors hover:border-primary/50">
48
+ <CardHeader>
49
+ <CardTitle><% if (i18n) { %>{t("dashboard")}<% } else { %>Dashboard<% } %></CardTitle>
50
+ <CardDescription>
51
+ <% if (i18n) { %>{t("dashboardCardDescription")}<% } else { %>Try the generated React Hook Form example backed by Zod,
52
+ shadcn/ui fields, and next-safe-action.<% } %>
53
+ </CardDescription>
54
+ </CardHeader>
55
+ <CardContent>
56
+ <Button asChild variant="secondary">
57
+ <Link href="/dashboard"><% if (i18n) { %>{t("exploreForms")}<% } else { %>Explore forms<% } %></Link>
58
+ </Button>
59
+ </CardContent>
60
+ </Card>
61
+ <% if (hasAiChat) { %>
62
+ <Card className="transition-colors hover:border-primary/50">
63
+ <CardHeader>
64
+ <CardTitle><% if (i18n) { %>{t("aiChat")}<% } else { %>AI chat<% } %></CardTitle>
65
+ <CardDescription>
66
+ <% if (i18n) { %>{t("aiChatCardDescription")}<% } else { %>Open the provider-free AI Elements mock and see the AI SDK
67
+ transport contract ready for your backend.<% } %>
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent>
71
+ <Button asChild variant="secondary">
72
+ <Link href="/chat"><% if (i18n) { %>{t("openChat")}<% } else { %>Open chat<% } %></Link>
73
+ </Button>
74
+ </CardContent>
75
+ </Card>
76
+ <% } %><% if (hasAuth) { %>
77
+ <Card className="transition-colors hover:border-primary/50">
78
+ <CardHeader>
79
+ <CardTitle><% if (i18n) { %>{t("signIn")}<% } else { %>Sign in<% } %></CardTitle>
80
+ <CardDescription>
81
+ <% if (i18n) { %>{t("<% if (isStatefulAuth) { %>signInStatefulCardDescription<% } else { %>signInCardDescription<% } %>")}<% } else { %>Exercise Better Auth with Microsoft Entra OAuth<% if (isStatefulAuth) { %>
82
+ and Prisma-backed email/password sessions<% } %>.<% } %>
83
+ </CardDescription>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <Button asChild variant="secondary">
87
+ <Link href="/sign-in"><% if (i18n) { %>{t("signIn")}<% } else { %>Sign in<% } %></Link>
88
+ </Button>
89
+ </CardContent>
90
+ </Card>
91
+ <Card className="transition-colors hover:border-primary/50">
92
+ <CardHeader>
93
+ <CardTitle><% if (i18n) { %>{t("signUp")}<% } else { %>Sign up<% } %></CardTitle>
94
+ <CardDescription>
95
+ <% if (i18n) { %>{t("signUpCardDescription")}<% } else { %>Create an account through the same Better Auth setup and keep
96
+ the onboarding route ready for customization.<% } %>
97
+ </CardDescription>
98
+ </CardHeader>
99
+ <CardContent>
100
+ <Button asChild variant="secondary">
101
+ <Link href="/sign-up"><% if (i18n) { %>{t("signUp")}<% } else { %>Sign up<% } %></Link>
102
+ </Button>
103
+ </CardContent>
104
+ </Card>
105
+ <% } %>
106
+ </div>
107
+ </div>
108
+ </section>
109
+ </main>
110
+ );
111
+ }