stackpatch 1.1.1 → 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 -2370
  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,224 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import fse from "fs-extra";
4
+ import inquirer from "inquirer";
5
+ import chalk from "chalk";
6
+ import {
7
+ detectAppDirectory,
8
+ detectComponentsDirectory,
9
+ generateComponentImportPath,
10
+ } from "../utils/paths.js";
11
+ import { findTypeScriptFiles } from "../utils/files.js";
12
+
13
+ /**
14
+ * File copying operations
15
+ */
16
+
17
+ /**
18
+ * Copy files from source to destination with smart directory detection
19
+ */
20
+ export async function copyFiles(
21
+ src: string,
22
+ dest: string
23
+ ): Promise<{ success: boolean; addedFiles: string[] }> {
24
+ const addedFiles: string[] = [];
25
+
26
+ if (!fs.existsSync(src)) {
27
+ console.log(chalk.red(`❌ Boilerplate folder not found: ${src}`));
28
+ return { success: false, addedFiles: [] };
29
+ }
30
+
31
+ await fse.ensureDir(dest);
32
+
33
+ // Detect app directory location in target
34
+ const appDir = detectAppDirectory(dest);
35
+ const appDirPath = path.join(dest, appDir);
36
+ const componentsDir = detectComponentsDirectory(dest);
37
+ const componentsDirPath = path.join(dest, componentsDir);
38
+
39
+ // Detect lib directory (src/lib or lib)
40
+ const hasSrcDir = fs.existsSync(path.join(dest, "src"));
41
+ const libDir = hasSrcDir ? "src/lib" : "lib";
42
+ const libDirPath = path.join(dest, libDir);
43
+
44
+ const conflicts: string[] = [];
45
+
46
+ // Check for conflicts before copying
47
+ const entries = fse.readdirSync(src, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ if (entry.name === "app") {
50
+ // For app directory, check conflicts in the detected app directory
51
+ if (fs.existsSync(appDirPath)) {
52
+ const appEntries = fse.readdirSync(path.join(src, "app"), { withFileTypes: true });
53
+ for (const appEntry of appEntries) {
54
+ const destAppPath = path.join(appDirPath, appEntry.name);
55
+ if (fs.existsSync(destAppPath)) {
56
+ conflicts.push(destAppPath);
57
+ }
58
+ }
59
+ }
60
+ } else if (entry.name === "components") {
61
+ // For components directory, check conflicts in the detected components directory
62
+ if (fs.existsSync(componentsDirPath)) {
63
+ const componentEntries = fse.readdirSync(path.join(src, "components"), {
64
+ withFileTypes: true,
65
+ });
66
+ for (const componentEntry of componentEntries) {
67
+ const destComponentPath = path.join(componentsDirPath, componentEntry.name);
68
+ if (fs.existsSync(destComponentPath)) {
69
+ conflicts.push(destComponentPath);
70
+ }
71
+ }
72
+ }
73
+ } else if (entry.name === "lib") {
74
+ // For lib directory, check conflicts in the detected lib directory
75
+ if (fs.existsSync(libDirPath)) {
76
+ const libEntries = fse.readdirSync(path.join(src, "lib"), {
77
+ withFileTypes: true,
78
+ });
79
+ for (const libEntry of libEntries) {
80
+ const destLibPath = path.join(libDirPath, libEntry.name);
81
+ if (fs.existsSync(destLibPath)) {
82
+ conflicts.push(destLibPath);
83
+ }
84
+ }
85
+ }
86
+ } else {
87
+ // For other files/directories (middleware, etc.), check in root
88
+ const destPath = path.join(dest, entry.name);
89
+ if (fs.existsSync(destPath)) {
90
+ conflicts.push(destPath);
91
+ }
92
+ }
93
+ }
94
+
95
+ if (conflicts.length) {
96
+ console.log(chalk.yellow("\n⚠️ The following files already exist:"));
97
+ conflicts.forEach((f) => console.log(` ${f}`));
98
+
99
+ const { overwrite } = await inquirer.prompt([
100
+ {
101
+ type: "list",
102
+ name: "overwrite",
103
+ message: "Do you want to overwrite them?",
104
+ choices: [
105
+ { name: "Yes, overwrite", value: "yes" },
106
+ { name: "No, skip", value: "no" },
107
+ ],
108
+ default: "no",
109
+ },
110
+ ]);
111
+
112
+ if (overwrite !== "yes") {
113
+ console.log(chalk.red("\nAborted! No files were copied."));
114
+ return { success: false, addedFiles: [] };
115
+ }
116
+ }
117
+
118
+ // Track files from SOURCE (boilerplate) before copying
119
+ // This ensures we only track files that are actually from StackPatch
120
+ function trackSourceFiles(srcDir: string, baseDir: string, targetBase: string): void {
121
+ if (!fs.existsSync(srcDir)) return;
122
+
123
+ const files = fs.readdirSync(srcDir, { withFileTypes: true });
124
+ for (const file of files) {
125
+ const srcFilePath = path.join(srcDir, file.name);
126
+ if (file.isDirectory()) {
127
+ trackSourceFiles(srcFilePath, baseDir, targetBase);
128
+ } else {
129
+ const relativePath = path.relative(baseDir, srcFilePath);
130
+ const targetPath = targetBase
131
+ ? path.join(targetBase, relativePath).replace(/\\/g, "/")
132
+ : relativePath.replace(/\\/g, "/");
133
+ addedFiles.push(targetPath);
134
+ }
135
+ }
136
+ }
137
+
138
+ // Copy files with smart app directory handling
139
+ for (const entry of entries) {
140
+ const srcPath = path.join(src, entry.name);
141
+
142
+ if (entry.name === "app") {
143
+ // Track files from SOURCE boilerplate before copying
144
+ trackSourceFiles(srcPath, srcPath, appDir);
145
+
146
+ // Copy app directory contents to the detected app directory location
147
+ await fse.ensureDir(appDirPath);
148
+ await fse.copy(srcPath, appDirPath, { overwrite: true });
149
+ } else if (entry.name === "components") {
150
+ // Track files from SOURCE boilerplate before copying
151
+ trackSourceFiles(srcPath, srcPath, componentsDir);
152
+
153
+ // Copy components directory to the detected components directory location
154
+ await fse.ensureDir(componentsDirPath);
155
+ await fse.copy(srcPath, componentsDirPath, { overwrite: true });
156
+ } else if (entry.name === "lib") {
157
+ // Track files from SOURCE boilerplate before copying
158
+ trackSourceFiles(srcPath, srcPath, libDir);
159
+
160
+ // Copy lib directory to the detected lib directory location
161
+ await fse.ensureDir(libDirPath);
162
+ await fse.copy(srcPath, libDirPath, { overwrite: true });
163
+ } else {
164
+ // For root-level files/directories, track from source
165
+ const srcStat = fs.statSync(srcPath);
166
+ if (srcStat.isDirectory()) {
167
+ trackSourceFiles(srcPath, srcPath, "");
168
+ } else {
169
+ addedFiles.push(entry.name);
170
+ }
171
+
172
+ // Copy other files/directories (middleware, etc.) to root
173
+ const destPath = path.join(dest, entry.name);
174
+ await fse.copy(srcPath, destPath, { overwrite: true });
175
+ }
176
+ }
177
+
178
+ // Update imports in copied files to use correct paths
179
+ updateImportsInFiles(dest);
180
+
181
+ return { success: true, addedFiles };
182
+ }
183
+
184
+ /**
185
+ * Update imports in copied files to use correct paths
186
+ */
187
+ function updateImportsInFiles(target: string): void {
188
+ const appDir = detectAppDirectory(target);
189
+ const appDirPath = path.join(target, appDir);
190
+
191
+ if (!fs.existsSync(appDirPath)) {
192
+ return;
193
+ }
194
+
195
+ const files = findTypeScriptFiles(appDirPath);
196
+
197
+ for (const filePath of files) {
198
+ try {
199
+ let content = fs.readFileSync(filePath, "utf-8");
200
+ let updated = false;
201
+
202
+ // Match imports like: from "@/components/component-name"
203
+ const importRegex = /from\s+["']@\/components\/([^"']+)["']/g;
204
+ const matches = Array.from(content.matchAll(importRegex));
205
+
206
+ for (const match of matches) {
207
+ const componentName = (match as RegExpMatchArray)[1];
208
+ const oldImport = (match as RegExpMatchArray)[0];
209
+ const newImportPath = generateComponentImportPath(target, componentName, filePath);
210
+ const newImport = oldImport.replace(/@\/components\/[^"']+/, newImportPath);
211
+
212
+ content = content.replace(oldImport, newImport);
213
+ updated = true;
214
+ }
215
+
216
+ if (updated) {
217
+ fs.writeFileSync(filePath, content, "utf-8");
218
+ }
219
+ } catch (error) {
220
+ // Silently skip files that can't be processed
221
+ continue;
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,304 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { detectAppDirectory } from "../utils/paths.js";
5
+ import { generateComponentImportPath } from "../utils/paths.js";
6
+ import { backupFile } from "../manifest.js";
7
+
8
+ /**
9
+ * Layout file modification operations
10
+ */
11
+
12
+ export interface LayoutUpdateResult {
13
+ success: boolean;
14
+ modified: boolean;
15
+ filePath: string;
16
+ originalContent?: string;
17
+ }
18
+
19
+ /**
20
+ * Update layout.tsx to include Toaster
21
+ */
22
+ export function updateLayoutForToaster(
23
+ target: string
24
+ ): LayoutUpdateResult {
25
+ const appDir = detectAppDirectory(target);
26
+ const layoutPath = path.join(target, appDir, "layout.tsx");
27
+
28
+ if (!fs.existsSync(layoutPath)) {
29
+ return { success: false, modified: false, filePath: layoutPath };
30
+ }
31
+
32
+ try {
33
+ const originalContent = fs.readFileSync(layoutPath, "utf-8");
34
+ let layoutContent = originalContent;
35
+
36
+ // Check if already has Toaster
37
+ if (layoutContent.includes("Toaster")) {
38
+ console.log(chalk.green("✅ Layout already has Toaster!"));
39
+ return { success: true, modified: false, filePath: layoutPath };
40
+ }
41
+
42
+ // Generate the correct import path
43
+ const importPath = generateComponentImportPath(target, "toaster", layoutPath);
44
+
45
+ // Check if import already exists (check for various patterns)
46
+ const hasImport =
47
+ layoutContent.includes("toaster") &&
48
+ (layoutContent.includes("from") || layoutContent.includes("import"));
49
+
50
+ if (!hasImport) {
51
+ // Find the last import statement
52
+ const lines = layoutContent.split("\n");
53
+ let lastImportIndex = -1;
54
+
55
+ for (let i = 0; i < lines.length; i++) {
56
+ const trimmed = lines[i].trim();
57
+ if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
58
+ lastImportIndex = i;
59
+ } else if (trimmed && !trimmed.startsWith("//") && lastImportIndex >= 0) {
60
+ break;
61
+ }
62
+ }
63
+
64
+ if (lastImportIndex >= 0) {
65
+ lines.splice(lastImportIndex + 1, 0, `import { Toaster } from "${importPath}";`);
66
+ layoutContent = lines.join("\n");
67
+ }
68
+ }
69
+
70
+ // Add Toaster component
71
+ if (layoutContent.includes("{children}")) {
72
+ // Add Toaster after children
73
+ layoutContent = layoutContent.replace(/(\{children\})/, '$1\n <Toaster />');
74
+ } else {
75
+ // Try to find body tag and add Toaster before closing body
76
+ const bodyRegex = /(<body[^>]*>)([\s\S]*?)(<\/body>)/;
77
+ if (bodyRegex.test(layoutContent)) {
78
+ layoutContent = layoutContent.replace(bodyRegex, '$1$2\n <Toaster />\n $3');
79
+ }
80
+ }
81
+
82
+ // Backup before modifying
83
+ backupFile(layoutPath, target);
84
+
85
+ fs.writeFileSync(layoutPath, layoutContent, "utf-8");
86
+ console.log(chalk.green("✅ Updated layout.tsx with Toaster!"));
87
+
88
+ const relativePath = path.relative(target, layoutPath).replace(/\\/g, "/");
89
+ return { success: true, modified: true, filePath: relativePath, originalContent };
90
+ } catch (error: unknown) {
91
+ const errorMessage = error instanceof Error ? error.message : String(error);
92
+ console.log(chalk.yellow(`⚠️ Failed to update layout with Toaster: ${errorMessage}`));
93
+ return { success: false, modified: false, filePath: layoutPath };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Update layout.tsx to include AuthWrapper
99
+ * This wrapper provides client-side route protection
100
+ */
101
+ export function updateLayoutForAuthWrapper(target: string): LayoutUpdateResult {
102
+ const appDir = detectAppDirectory(target);
103
+ const layoutPath = path.join(target, appDir, "layout.tsx");
104
+
105
+ if (!fs.existsSync(layoutPath)) {
106
+ console.log(chalk.yellow("⚠️ layout.tsx not found. Skipping layout update."));
107
+ return { success: false, modified: false, filePath: layoutPath };
108
+ }
109
+
110
+ try {
111
+ const originalContent = fs.readFileSync(layoutPath, "utf-8");
112
+ let layoutContent = originalContent;
113
+
114
+ // Check if already has AuthWrapper
115
+ if (layoutContent.includes("AuthWrapper")) {
116
+ console.log(chalk.green("✅ Layout already has AuthWrapper!"));
117
+ return { success: true, modified: false, filePath: layoutPath };
118
+ }
119
+
120
+ // Generate the correct import path
121
+ const importPath = generateComponentImportPath(target, "auth-wrapper", layoutPath);
122
+
123
+ // Check if import already exists
124
+ const hasImport =
125
+ layoutContent.includes("auth-wrapper") &&
126
+ (layoutContent.includes("from") || layoutContent.includes("import"));
127
+
128
+ if (!hasImport) {
129
+ // Find the last import statement
130
+ const lines = layoutContent.split("\n");
131
+ let lastImportIndex = -1;
132
+
133
+ for (let i = 0; i < lines.length; i++) {
134
+ const trimmed = lines[i].trim();
135
+ if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
136
+ lastImportIndex = i;
137
+ } else if (trimmed && !trimmed.startsWith("//") && lastImportIndex >= 0) {
138
+ break;
139
+ }
140
+ }
141
+
142
+ if (lastImportIndex >= 0) {
143
+ lines.splice(
144
+ lastImportIndex + 1,
145
+ 0,
146
+ `import { AuthWrapper } from "${importPath}";`
147
+ );
148
+ layoutContent = lines.join("\n");
149
+ } else {
150
+ // No imports found, add after the first line
151
+ const firstNewline = layoutContent.indexOf("\n");
152
+ if (firstNewline > 0) {
153
+ layoutContent =
154
+ layoutContent.slice(0, firstNewline + 1) +
155
+ `import { AuthWrapper } from "${importPath}";\n` +
156
+ layoutContent.slice(firstNewline + 1);
157
+ } else {
158
+ layoutContent = `import { AuthWrapper } from "${importPath}";\n` + layoutContent;
159
+ }
160
+ }
161
+ }
162
+
163
+ // Wrap children with AuthWrapper
164
+ // Look for {children} pattern
165
+ const childrenPattern = /(\s*)(\{children\})(\s*)/;
166
+ if (childrenPattern.test(layoutContent)) {
167
+ layoutContent = layoutContent.replace(
168
+ childrenPattern,
169
+ '$1<AuthWrapper>{children}</AuthWrapper>$3'
170
+ );
171
+ } else {
172
+ // Try to find body tag and wrap its content
173
+ const bodyRegex = /(<body[^>]*>)([\s\S]*?)(<\/body>)/;
174
+ const bodyMatch = layoutContent.match(bodyRegex);
175
+ if (bodyMatch) {
176
+ const bodyContent = bodyMatch[2].trim();
177
+ if (bodyContent && !bodyContent.includes("AuthWrapper")) {
178
+ layoutContent = layoutContent.replace(
179
+ bodyRegex,
180
+ `$1\n <AuthWrapper>${bodyContent}</AuthWrapper>\n $3`
181
+ );
182
+ }
183
+ }
184
+ }
185
+
186
+ // Backup before modifying
187
+ backupFile(layoutPath, target);
188
+
189
+ fs.writeFileSync(layoutPath, layoutContent, "utf-8");
190
+ console.log(chalk.green("✅ Updated layout.tsx with AuthWrapper!"));
191
+
192
+ const relativePath = path.relative(target, layoutPath).replace(/\\/g, "/");
193
+ return { success: true, modified: true, filePath: relativePath, originalContent };
194
+ } catch (error: unknown) {
195
+ const errorMessage = error instanceof Error ? error.message : String(error);
196
+ console.log(chalk.red(`❌ Failed to update layout.tsx: ${errorMessage}`));
197
+ return { success: false, modified: false, filePath: layoutPath };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Update layout.tsx to include AuthSessionProvider
203
+ * Note: Better Auth doesn't require a session provider, but we add it for compatibility
204
+ * @deprecated Not used - middleware handles all route protection
205
+ */
206
+ export function updateLayoutForAuth(target: string): LayoutUpdateResult {
207
+ const appDir = detectAppDirectory(target);
208
+ const layoutPath = path.join(target, appDir, "layout.tsx");
209
+
210
+ if (!fs.existsSync(layoutPath)) {
211
+ console.log(chalk.yellow("⚠️ layout.tsx not found. Skipping layout update."));
212
+ return { success: false, modified: false, filePath: layoutPath };
213
+ }
214
+
215
+ try {
216
+ const originalContent = fs.readFileSync(layoutPath, "utf-8");
217
+ let layoutContent = originalContent;
218
+
219
+ // Check if already has AuthSessionProvider
220
+ if (layoutContent.includes("AuthSessionProvider")) {
221
+ console.log(chalk.green("✅ Layout already has AuthSessionProvider!"));
222
+ return { success: true, modified: false, filePath: layoutPath };
223
+ }
224
+
225
+ // Generate the correct import path
226
+ const importPath = generateComponentImportPath(target, "session-provider", layoutPath);
227
+
228
+ // Check if import already exists (check for various patterns)
229
+ const hasImport =
230
+ layoutContent.includes("session-provider") &&
231
+ (layoutContent.includes("from") || layoutContent.includes("import"));
232
+
233
+ if (!hasImport) {
234
+ // Find the last import statement (before the first non-import line)
235
+ const lines = layoutContent.split("\n");
236
+ let lastImportIndex = -1;
237
+
238
+ for (let i = 0; i < lines.length; i++) {
239
+ const trimmed = lines[i].trim();
240
+ if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
241
+ lastImportIndex = i;
242
+ } else if (trimmed && !trimmed.startsWith("//") && lastImportIndex >= 0) {
243
+ break;
244
+ }
245
+ }
246
+
247
+ if (lastImportIndex >= 0) {
248
+ lines.splice(
249
+ lastImportIndex + 1,
250
+ 0,
251
+ `import { AuthSessionProvider } from "${importPath}";`
252
+ );
253
+ layoutContent = lines.join("\n");
254
+ } else {
255
+ // No imports found, add after the first line
256
+ const firstNewline = layoutContent.indexOf("\n");
257
+ if (firstNewline > 0) {
258
+ layoutContent =
259
+ layoutContent.slice(0, firstNewline + 1) +
260
+ `import { AuthSessionProvider } from "${importPath}";\n` +
261
+ layoutContent.slice(firstNewline + 1);
262
+ } else {
263
+ layoutContent = `import { AuthSessionProvider } from "${importPath}";\n` + layoutContent;
264
+ }
265
+ }
266
+ }
267
+
268
+ // Wrap children with AuthSessionProvider
269
+ // Look for {children} pattern in body tag
270
+ const childrenPattern = /(\s*)(\{children\})(\s*)/;
271
+ if (childrenPattern.test(layoutContent)) {
272
+ layoutContent = layoutContent.replace(
273
+ childrenPattern,
274
+ '$1<AuthSessionProvider>{children}</AuthSessionProvider>$3'
275
+ );
276
+ } else {
277
+ // Try to find body tag and wrap its content
278
+ const bodyRegex = /(<body[^>]*>)([\s\S]*?)(<\/body>)/;
279
+ const bodyMatch = layoutContent.match(bodyRegex);
280
+ if (bodyMatch) {
281
+ const bodyContent = bodyMatch[2].trim();
282
+ if (bodyContent && !bodyContent.includes("AuthSessionProvider")) {
283
+ layoutContent = layoutContent.replace(
284
+ bodyRegex,
285
+ `$1\n <AuthSessionProvider>${bodyContent}</AuthSessionProvider>\n $3`
286
+ );
287
+ }
288
+ }
289
+ }
290
+
291
+ // Backup before modifying
292
+ backupFile(layoutPath, target);
293
+
294
+ fs.writeFileSync(layoutPath, layoutContent, "utf-8");
295
+ console.log(chalk.green("✅ Updated layout.tsx with AuthSessionProvider!"));
296
+
297
+ const relativePath = path.relative(target, layoutPath).replace(/\\/g, "/");
298
+ return { success: true, modified: true, filePath: relativePath, originalContent };
299
+ } catch (error: unknown) {
300
+ const errorMessage = error instanceof Error ? error.message : String(error);
301
+ console.log(chalk.red(`❌ Failed to update layout.tsx: ${errorMessage}`));
302
+ return { success: false, modified: false, filePath: layoutPath };
303
+ }
304
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { BOILERPLATE_ROOT } from "../config.js";
4
+ import { detectAppDirectory, detectComponentsDirectory } from "../utils/paths.js";
5
+
6
+ /**
7
+ * Copy protected route files
8
+ */
9
+ export async function copyProtectedRouteFiles(target: string): Promise<void> {
10
+ const protectedRouteSrc = path.join(BOILERPLATE_ROOT, "auth/components/protected-route.tsx");
11
+ const middlewareSrc = path.join(BOILERPLATE_ROOT, "auth/middleware.ts");
12
+
13
+ const componentsDir = detectComponentsDirectory(target);
14
+ const componentsDirPath = path.join(target, componentsDir);
15
+ const protectedRouteDest = path.join(componentsDirPath, "protected-route.tsx");
16
+ const middlewareDest = path.join(target, "middleware.ts");
17
+
18
+ // Ensure components directory exists
19
+ if (!fs.existsSync(componentsDirPath)) {
20
+ fs.mkdirSync(componentsDirPath, { recursive: true });
21
+ }
22
+
23
+ // Copy protected route component
24
+ if (fs.existsSync(protectedRouteSrc)) {
25
+ fs.copyFileSync(protectedRouteSrc, protectedRouteDest);
26
+ }
27
+
28
+ // Copy middleware (only if it doesn't exist)
29
+ if (fs.existsSync(middlewareSrc) && !fs.existsSync(middlewareDest)) {
30
+ fs.copyFileSync(middlewareSrc, middlewareDest);
31
+ }
32
+
33
+ // Auth navbar is not copied by default - it's available in boilerplate if needed
34
+ // Users can manually copy it if they want to use it
35
+
36
+ // Copy example pages (only if they don't exist)
37
+ const appDir = detectAppDirectory(target);
38
+ const dashboardPageSrc = path.join(BOILERPLATE_ROOT, "auth/app/dashboard/page.tsx");
39
+ const landingPageSrc = path.join(BOILERPLATE_ROOT, "auth/app/page.tsx");
40
+ const dashboardPageDest = path.join(target, appDir, "dashboard/page.tsx");
41
+ const landingPageDest = path.join(target, appDir, "page.tsx");
42
+
43
+ // Create dashboard directory if needed
44
+ const dashboardDir = path.join(target, appDir, "dashboard");
45
+ if (!fs.existsSync(dashboardDir)) {
46
+ fs.mkdirSync(dashboardDir, { recursive: true });
47
+ }
48
+
49
+ // Copy dashboard page (only if it doesn't exist)
50
+ if (fs.existsSync(dashboardPageSrc) && !fs.existsSync(dashboardPageDest)) {
51
+ fs.copyFileSync(dashboardPageSrc, dashboardPageDest);
52
+ }
53
+
54
+ // Copy landing page (only if it doesn't exist or is default)
55
+ if (fs.existsSync(landingPageSrc)) {
56
+ // Check if current page is just a default Next.js page
57
+ if (fs.existsSync(landingPageDest)) {
58
+ const currentContent = fs.readFileSync(landingPageDest, "utf-8");
59
+ // Only replace if it's the default Next.js page
60
+ if (currentContent.includes("Get started by editing") || currentContent.length < 500) {
61
+ fs.copyFileSync(landingPageSrc, landingPageDest);
62
+ }
63
+ } else {
64
+ fs.copyFileSync(landingPageSrc, landingPageDest);
65
+ }
66
+ }
67
+ }