stackpatch 1.1.2 → 1.1.4

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 (36) hide show
  1. package/README.md +123 -114
  2. package/bin/stackpatch.ts +2 -2441
  3. package/boilerplate/auth/app/auth/login/page.tsx +50 -24
  4. package/boilerplate/auth/app/auth/signup/page.tsx +69 -56
  5. package/boilerplate/auth/app/stackpatch/page.tsx +269 -0
  6. package/boilerplate/auth/components/auth-wrapper.tsx +61 -0
  7. package/package.json +4 -2
  8. package/src/auth/generator.ts +569 -0
  9. package/src/auth/index.ts +372 -0
  10. package/src/auth/setup.ts +293 -0
  11. package/src/commands/add.ts +112 -0
  12. package/src/commands/create.ts +128 -0
  13. package/src/commands/revert.ts +389 -0
  14. package/src/config.ts +52 -0
  15. package/src/fileOps/copy.ts +224 -0
  16. package/src/fileOps/layout.ts +304 -0
  17. package/src/fileOps/protected.ts +67 -0
  18. package/src/index.ts +215 -0
  19. package/src/manifest.ts +87 -0
  20. package/src/ui/logo.ts +24 -0
  21. package/src/ui/progress.ts +82 -0
  22. package/src/utils/dependencies.ts +114 -0
  23. package/src/utils/deps-check.ts +45 -0
  24. package/src/utils/files.ts +58 -0
  25. package/src/utils/paths.ts +217 -0
  26. package/src/utils/scanner.ts +109 -0
  27. package/boilerplate/auth/app/api/auth/[...nextauth]/route.ts +0 -124
  28. package/boilerplate/auth/app/api/auth/signup/route.ts +0 -45
  29. package/boilerplate/auth/app/dashboard/page.tsx +0 -82
  30. package/boilerplate/auth/app/login/page.tsx +0 -136
  31. package/boilerplate/auth/app/page.tsx +0 -48
  32. package/boilerplate/auth/components/auth-button.tsx +0 -43
  33. package/boilerplate/auth/components/auth-navbar.tsx +0 -118
  34. package/boilerplate/auth/components/protected-route.tsx +0 -74
  35. package/boilerplate/auth/components/session-provider.tsx +0 -11
  36. package/boilerplate/auth/middleware.ts +0 -51
@@ -0,0 +1,217 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Utility functions for detecting project structure and paths
6
+ */
7
+
8
+ /**
9
+ * Detect the app directory location (app/ or src/app/)
10
+ */
11
+ export function detectAppDirectory(target: string): string {
12
+ // Check for src/app first (more common in modern Next.js projects)
13
+ if (fs.existsSync(path.join(target, "src", "app"))) {
14
+ return "src/app";
15
+ }
16
+ // Check for app directory
17
+ if (fs.existsSync(path.join(target, "app"))) {
18
+ return "app";
19
+ }
20
+ // Check for src/pages (legacy)
21
+ if (fs.existsSync(path.join(target, "src", "pages"))) {
22
+ return "src/pages";
23
+ }
24
+ // Check for pages (legacy)
25
+ if (fs.existsSync(path.join(target, "pages"))) {
26
+ return "pages";
27
+ }
28
+ // Default to app if nothing found (will fail gracefully later)
29
+ return "app";
30
+ }
31
+
32
+ /**
33
+ * Detect the components directory location (components/ or src/components/)
34
+ */
35
+ export function detectComponentsDirectory(target: string): string {
36
+ const appDir = detectAppDirectory(target);
37
+
38
+ // If app is in src/app, components should be in src/components
39
+ if (appDir.startsWith("src/")) {
40
+ // Check if src/components exists
41
+ if (fs.existsSync(path.join(target, "src", "components"))) {
42
+ return "src/components";
43
+ }
44
+ // Even if it doesn't exist yet, return src/components to match app structure
45
+ return "src/components";
46
+ }
47
+
48
+ // If app is in root, components should be in root
49
+ if (fs.existsSync(path.join(target, "components"))) {
50
+ return "components";
51
+ }
52
+
53
+ // Default to components
54
+ return "components";
55
+ }
56
+
57
+ /**
58
+ * Detect path aliases from tsconfig.json
59
+ */
60
+ export function detectPathAliases(target: string): { alias: string; path: string } | null {
61
+ const tsconfigPath = path.join(target, "tsconfig.json");
62
+
63
+ if (!fs.existsSync(tsconfigPath)) {
64
+ return null;
65
+ }
66
+
67
+ try {
68
+ const tsconfigContent = fs.readFileSync(tsconfigPath, "utf-8");
69
+ const tsconfig = JSON.parse(tsconfigContent);
70
+
71
+ const paths = tsconfig.compilerOptions?.paths;
72
+ if (!paths || typeof paths !== "object") {
73
+ return null;
74
+ }
75
+
76
+ // Look for common aliases like @/*, ~/*, etc.
77
+ for (const [alias, pathsArray] of Object.entries(paths)) {
78
+ if (Array.isArray(pathsArray) && pathsArray.length > 0) {
79
+ // Remove the /* from alias (e.g., "@/*" -> "@")
80
+ const cleanAlias = alias.replace(/\/\*$/, "");
81
+ // Get the first path and remove /* from it
82
+ const cleanPath = pathsArray[0].replace(/\/\*$/, "");
83
+ return { alias: cleanAlias, path: cleanPath };
84
+ }
85
+ }
86
+ } catch (error) {
87
+ // If parsing fails, return null
88
+ }
89
+
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Generate import path for components
95
+ */
96
+ export function generateComponentImportPath(
97
+ target: string,
98
+ componentName: string,
99
+ fromFile: string
100
+ ): string {
101
+ const pathAlias = detectPathAliases(target);
102
+ const componentsDir = detectComponentsDirectory(target);
103
+
104
+ // If we have a path alias, use it
105
+ if (pathAlias) {
106
+ // Check if the alias path matches components directory
107
+ const aliasPath = pathAlias.path.replace(/^\.\//, ""); // Remove leading ./
108
+
109
+ // If alias points to root and components is in root, use alias
110
+ if (aliasPath === "" && componentsDir === "components") {
111
+ return `${pathAlias.alias}/components/${componentName}`;
112
+ }
113
+
114
+ // If alias points to src and components is in src/components, use alias
115
+ if (aliasPath === "src" && componentsDir === "src/components") {
116
+ return `${pathAlias.alias}/components/${componentName}`;
117
+ }
118
+
119
+ // Try to match the alias path structure
120
+ if (componentsDir.startsWith(aliasPath)) {
121
+ const relativeFromAlias = componentsDir.replace(new RegExp(`^${aliasPath}/?`), "");
122
+ return `${pathAlias.alias}/${relativeFromAlias}/${componentName}`;
123
+ }
124
+
125
+ // If alias path is "./" (root), components should be accessible via alias
126
+ if (aliasPath === "" || aliasPath === ".") {
127
+ return `${pathAlias.alias}/components/${componentName}`;
128
+ }
129
+ }
130
+
131
+ // Fallback: calculate relative path
132
+ // fromFile is the full path to the file we're importing into
133
+ const fromDir = path.dirname(fromFile);
134
+ const toComponents = path.join(target, componentsDir);
135
+
136
+ // Calculate relative path from the file's directory to components directory
137
+ const relativePath = path.relative(fromDir, toComponents).replace(/\\/g, "/");
138
+ const normalizedPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
139
+
140
+ return `${normalizedPath}/${componentName}`;
141
+ }
142
+
143
+ /**
144
+ * Get all parent directories of a file path
145
+ */
146
+ export function getParentDirectories(filePath: string, rootPath: string): string[] {
147
+ const dirs: string[] = [];
148
+ let current = path.dirname(filePath);
149
+ const root = path.resolve(rootPath);
150
+
151
+ while (current !== root && current !== path.dirname(current)) {
152
+ dirs.push(current);
153
+ current = path.dirname(current);
154
+ }
155
+
156
+ return dirs;
157
+ }
158
+
159
+ /**
160
+ * Auto-detect target directory for Next.js app
161
+ */
162
+ export function detectTargetDirectory(startDir: string = process.cwd()): string {
163
+ let target = startDir;
164
+
165
+ // Check if we're in a Next.js app (has app/, src/app/, pages/, or src/pages/ directory)
166
+ const hasAppDir =
167
+ fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
168
+ const hasPagesDir =
169
+ fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
170
+
171
+ if (!hasAppDir && !hasPagesDir) {
172
+ // Try parent directory
173
+ const parent = path.resolve(target, "..");
174
+ if (
175
+ fs.existsSync(path.join(parent, "app")) ||
176
+ fs.existsSync(path.join(parent, "src", "app")) ||
177
+ fs.existsSync(path.join(parent, "pages")) ||
178
+ fs.existsSync(path.join(parent, "src", "pages"))
179
+ ) {
180
+ target = parent;
181
+ } else {
182
+ // Try common monorepo locations: apps/, packages/, or root
183
+ const possiblePaths = [
184
+ path.join(target, "apps"),
185
+ path.join(parent, "apps"),
186
+ path.join(target, "packages"),
187
+ path.join(parent, "packages"),
188
+ ];
189
+
190
+ let foundApp = false;
191
+ for (const possiblePath of possiblePaths) {
192
+ if (fs.existsSync(possiblePath)) {
193
+ // Look for Next.js apps in this directory
194
+ const entries = fs.readdirSync(possiblePath, { withFileTypes: true });
195
+ for (const entry of entries) {
196
+ if (entry.isDirectory()) {
197
+ const appPath = path.join(possiblePath, entry.name);
198
+ if (
199
+ fs.existsSync(path.join(appPath, "app")) ||
200
+ fs.existsSync(path.join(appPath, "src", "app")) ||
201
+ fs.existsSync(path.join(appPath, "pages")) ||
202
+ fs.existsSync(path.join(appPath, "src", "pages"))
203
+ ) {
204
+ target = appPath;
205
+ foundApp = true;
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ if (foundApp) break;
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ return target;
217
+ }
@@ -0,0 +1,109 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Project scanner to detect project configuration
6
+ */
7
+
8
+ export interface ProjectScan {
9
+ framework: "nextjs" | "unknown";
10
+ router: "app" | "pages" | "unknown";
11
+ typescript: boolean;
12
+ packageManager: "pnpm" | "npm" | "yarn" | "bun" | "unknown";
13
+ runtime: "node" | "bun" | "unknown";
14
+ hasSrcDir: boolean;
15
+ existingAuth: "better-auth" | "next-auth" | "none";
16
+ }
17
+
18
+ /**
19
+ * Scan project to detect configuration
20
+ */
21
+ export function scanProject(target: string): ProjectScan {
22
+ const scan: ProjectScan = {
23
+ framework: "unknown",
24
+ router: "unknown",
25
+ typescript: false,
26
+ packageManager: "unknown",
27
+ runtime: "node",
28
+ hasSrcDir: false,
29
+ existingAuth: "none",
30
+ };
31
+
32
+ // Check for Next.js
33
+ const packageJsonPath = path.join(target, "package.json");
34
+ if (fs.existsSync(packageJsonPath)) {
35
+ try {
36
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
37
+ const deps = {
38
+ ...packageJson.dependencies,
39
+ ...packageJson.devDependencies,
40
+ };
41
+
42
+ if (deps.next || deps["next"]) {
43
+ scan.framework = "nextjs";
44
+ }
45
+
46
+ // Detect package manager from lock files
47
+ if (fs.existsSync(path.join(target, "pnpm-lock.yaml"))) {
48
+ scan.packageManager = "pnpm";
49
+ } else if (fs.existsSync(path.join(target, "yarn.lock"))) {
50
+ scan.packageManager = "yarn";
51
+ } else if (fs.existsSync(path.join(target, "package-lock.json"))) {
52
+ scan.packageManager = "npm";
53
+ } else if (fs.existsSync(path.join(target, "bun.lockb"))) {
54
+ scan.packageManager = "bun";
55
+ scan.runtime = "bun";
56
+ }
57
+
58
+ // Check for existing auth
59
+ if (deps["better-auth"]) {
60
+ scan.existingAuth = "better-auth";
61
+ } else if (deps["next-auth"]) {
62
+ scan.existingAuth = "next-auth";
63
+ }
64
+ } catch {
65
+ // Ignore errors
66
+ }
67
+ }
68
+
69
+ // Check for TypeScript
70
+ if (
71
+ fs.existsSync(path.join(target, "tsconfig.json")) ||
72
+ fs.existsSync(path.join(target, "src", "tsconfig.json"))
73
+ ) {
74
+ scan.typescript = true;
75
+ }
76
+
77
+ // Check for src directory
78
+ if (fs.existsSync(path.join(target, "src"))) {
79
+ scan.hasSrcDir = true;
80
+ }
81
+
82
+ // Detect router type
83
+ if (fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"))) {
84
+ scan.router = "app";
85
+ } else if (
86
+ fs.existsSync(path.join(target, "pages")) ||
87
+ fs.existsSync(path.join(target, "src", "pages"))
88
+ ) {
89
+ scan.router = "pages";
90
+ }
91
+
92
+ return scan;
93
+ }
94
+
95
+ /**
96
+ * Format scan results for display
97
+ */
98
+ export function formatScanResults(scan: ProjectScan): string[] {
99
+ const results: string[] = [];
100
+
101
+ results.push(`✔ Framework: ${scan.framework === "nextjs" ? "Next.js" : "Unknown"} ${scan.router !== "unknown" ? `(${scan.router === "app" ? "App Router" : "Pages Router"})` : ""}`);
102
+ results.push(`✔ TypeScript: ${scan.typescript ? "Yes" : "No"}`);
103
+ results.push(`✔ Package Manager: ${scan.packageManager !== "unknown" ? scan.packageManager : "Unknown"}`);
104
+ results.push(`✔ Runtime: ${scan.runtime === "bun" ? "Bun" : "Node"}`);
105
+ results.push(`✔ src directory: ${scan.hasSrcDir ? "Yes" : "No"}`);
106
+ results.push(`✔ Existing auth: ${scan.existingAuth === "none" ? "None" : scan.existingAuth === "better-auth" ? "Better Auth" : "NextAuth"}`);
107
+
108
+ return results;
109
+ }
@@ -1,124 +0,0 @@
1
- import NextAuth from "next-auth";
2
- import type { NextAuthOptions } from "next-auth";
3
- import GoogleProvider from "next-auth/providers/google";
4
- import GitHubProvider from "next-auth/providers/github";
5
- import CredentialsProvider from "next-auth/providers/credentials";
6
-
7
- export const authOptions: NextAuthOptions = {
8
- providers: [
9
- CredentialsProvider({
10
- name: "Credentials",
11
- credentials: {
12
- email: { label: "Email", type: "email" },
13
- password: { label: "Password", type: "password" },
14
- },
15
- async authorize(credentials) {
16
- // ⚠️ DEMO MODE: This is a placeholder implementation
17
- //
18
- // TO IMPLEMENT REAL AUTHENTICATION:
19
- // 1. Set up a database (PostgreSQL, MongoDB, Prisma, etc.)
20
- // 2. Install bcrypt: npm install bcryptjs @types/bcryptjs
21
- // 3. Replace this function with database lookup:
22
- //
23
- // Example implementation:
24
- // ```ts
25
- // import bcrypt from "bcryptjs";
26
- // import { db } from "@/lib/db"; // Your database connection
27
- //
28
- // async authorize(credentials) {
29
- // if (!credentials?.email || !credentials?.password) {
30
- // return null;
31
- // }
32
- //
33
- // // Find user in database
34
- // const user = await db.user.findUnique({
35
- // where: { email: credentials.email },
36
- // });
37
- //
38
- // if (!user) {
39
- // return null;
40
- // }
41
- //
42
- // // Verify password
43
- // const isValid = await bcrypt.compare(
44
- // credentials.password,
45
- // user.password
46
- // );
47
- //
48
- // if (!isValid) {
49
- // return null;
50
- // }
51
- //
52
- // return {
53
- // id: user.id,
54
- // email: user.email,
55
- // name: user.name,
56
- // };
57
- // }
58
- // ```
59
- //
60
- // Current demo credentials (REMOVE IN PRODUCTION):
61
- // Email: demo@example.com
62
- // Password: demo123
63
-
64
- if (!credentials?.email || !credentials?.password) {
65
- return null;
66
- }
67
-
68
- // Demo check - REMOVE THIS IN PRODUCTION
69
- if (
70
- credentials.email === "demo@example.com" &&
71
- credentials.password === "demo123"
72
- ) {
73
- return {
74
- id: "1",
75
- email: credentials.email,
76
- name: "Demo User",
77
- };
78
- }
79
-
80
- return null;
81
- },
82
- }),
83
- GoogleProvider({
84
- clientId: process.env.GOOGLE_CLIENT_ID!,
85
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
86
- }),
87
- GitHubProvider({
88
- clientId: process.env.GITHUB_CLIENT_ID!,
89
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
90
- }),
91
- ],
92
- pages: {
93
- signIn: "/auth/login",
94
- error: "/auth/error",
95
- },
96
- session: {
97
- strategy: "jwt",
98
- },
99
- callbacks: {
100
- async jwt({ token, user, account }) {
101
- if (user) {
102
- token.id = user.id;
103
- token.email = user.email;
104
- token.name = user.name;
105
- }
106
- if (account) {
107
- token.accessToken = account.access_token;
108
- token.provider = account.provider;
109
- }
110
- return token;
111
- },
112
- async session({ session, token }) {
113
- if (session.user) {
114
- session.user.id = token.id as string;
115
- session.accessToken = token.accessToken as string;
116
- }
117
- return session;
118
- },
119
- },
120
- };
121
-
122
- const handler = NextAuth(authOptions);
123
-
124
- export { handler as GET, handler as POST };
@@ -1,45 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- export async function POST(request: Request) {
4
- try {
5
- const { email, password, name } = await request.json();
6
-
7
- // TODO: Replace with your actual signup logic
8
- // This is a placeholder - you should:
9
- // 1. Validate input
10
- // 2. Check if user already exists
11
- // 3. Hash password (use bcrypt or similar)
12
- // 4. Save user to database
13
- // 5. Return success or error
14
-
15
- // Example validation
16
- if (!email || !password || !name) {
17
- return NextResponse.json(
18
- { error: "Missing required fields" },
19
- { status: 400 }
20
- );
21
- }
22
-
23
- if (password.length < 6) {
24
- return NextResponse.json(
25
- { error: "Password must be at least 6 characters" },
26
- { status: 400 }
27
- );
28
- }
29
-
30
- // Placeholder: In production, save to database here
31
- // const hashedPassword = await bcrypt.hash(password, 10);
32
- // const user = await db.user.create({ email, password: hashedPassword, name });
33
-
34
- return NextResponse.json(
35
- { message: "Account created successfully" },
36
- { status: 201 }
37
- );
38
- } catch (error) {
39
- console.error("Signup error:", error);
40
- return NextResponse.json(
41
- { error: "Failed to create account" },
42
- { status: 500 }
43
- );
44
- }
45
- }
@@ -1,82 +0,0 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { useSession } from "next-auth/react";
5
- import { ProtectedRoute } from "@/components/protected-route";
6
- import { AuthNavbar } from "@/components/auth-navbar";
7
-
8
- /**
9
- * Dashboard Page Example
10
- *
11
- * This is an example protected dashboard page.
12
- *
13
- * To use this:
14
- * 1. Copy this file to your app/dashboard/page.tsx
15
- * 2. The page is automatically protected using ProtectedRoute
16
- * 3. The AuthNavbar shows session status and sign out button
17
- *
18
- * You can customize this page to show your dashboard content.
19
- * If you have an existing navbar, replace AuthNavbar with your own.
20
- */
21
- export default function DashboardPage() {
22
- const { data: session } = useSession();
23
-
24
- return (
25
- <ProtectedRoute>
26
- <div className="min-h-screen bg-zinc-50 dark:bg-black">
27
- <AuthNavbar />
28
- <main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
29
- <div className="rounded-lg bg-white p-8 shadow dark:bg-zinc-900">
30
- <h1 className="text-3xl font-bold text-zinc-900 dark:text-zinc-50">
31
- Dashboard
32
- </h1>
33
- <p className="mt-2 text-zinc-600 dark:text-zinc-400">
34
- Welcome to your protected dashboard!
35
- </p>
36
-
37
- {session && (
38
- <div className="mt-6 rounded-md bg-zinc-100 p-4 dark:bg-zinc-800">
39
- <h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
40
- Session Information
41
- </h2>
42
- <div className="mt-2 space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
43
- <p>
44
- <span className="font-medium">Name:</span>{" "}
45
- {session.user?.name || "Not provided"}
46
- </p>
47
- <p>
48
- <span className="font-medium">Email:</span>{" "}
49
- {session.user?.email || "Not provided"}
50
- </p>
51
- {session.user?.image && (
52
- <p>
53
- <span className="font-medium">Image:</span>{" "}
54
- <img
55
- src={session.user.image}
56
- alt={session.user.name || "User"}
57
- className="mt-2 h-16 w-16 rounded-full"
58
- />
59
- </p>
60
- )}
61
- </div>
62
- </div>
63
- )}
64
-
65
- <div className="mt-8">
66
- <h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-50">
67
- Getting Started
68
- </h2>
69
- <p className="mt-2 text-zinc-600 dark:text-zinc-400">
70
- This is a protected page. Only authenticated users can see this
71
- content.
72
- </p>
73
- <p className="mt-2 text-zinc-600 dark:text-zinc-400">
74
- Customize this page to add your dashboard features.
75
- </p>
76
- </div>
77
- </div>
78
- </main>
79
- </div>
80
- </ProtectedRoute>
81
- );
82
- }