stackpatch 1.2.0 → 1.2.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.
package/bin/stackpatch.ts DELETED
@@ -1,2443 +0,0 @@
1
- // This file is executed via bin/stackpatch wrapper
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import inquirer from "inquirer";
6
- import chalk from "chalk";
7
- import fse from "fs-extra";
8
- import { spawnSync } from "child_process";
9
- import Jimp from "jimp";
10
-
11
- // ---------------- CONFIG ----------------
12
- // Get directory path - Works with both Bun and Node.js
13
- // Bun has import.meta.dir, Node.js doesn't - use fallback
14
- const CLI_DIR = (import.meta as any).dir || path.dirname(new URL(import.meta.url).pathname);
15
- // Resolve boilerplate path - use local boilerplate inside CLI package
16
- const BOILERPLATE_ROOT = path.resolve(CLI_DIR, "../boilerplate");
17
- const PATCHES: Record<
18
- string,
19
- { path: string; dependencies: string[] }
20
- > = {
21
- auth: {
22
- path: "auth",
23
- dependencies: ["next-auth", "react-hot-toast"],
24
- },
25
- "auth-ui": {
26
- path: "auth",
27
- dependencies: ["next-auth", "react-hot-toast"],
28
- },
29
- // Example for future patches:
30
- // stripe: { path: "stripe", dependencies: ["stripe"] },
31
- // redux: { path: "redux", dependencies: ["@reduxjs/toolkit", "react-redux"] },
32
- };
33
-
34
- // Manifest structure for tracking changes
35
- interface StackPatchManifest {
36
- version: string;
37
- patchName: string;
38
- target: string;
39
- timestamp: string;
40
- files: {
41
- added: string[];
42
- modified: {
43
- path: string;
44
- originalContent: string;
45
- }[];
46
- backedUp: string[];
47
- envFiles?: string[]; // Track .env.local and .env.example if created
48
- };
49
- dependencies: string[];
50
- oauthProviders: string[];
51
- }
52
-
53
- const MANIFEST_VERSION = "1.0.0";
54
-
55
-
56
-
57
- // ---------------- Manifest & Tracking ----------------
58
- // Get manifest path for a target directory
59
- function getManifestPath(target: string): string {
60
- return path.join(target, ".stackpatch", "manifest.json");
61
- }
62
-
63
- // Read manifest if it exists
64
- function readManifest(target: string): StackPatchManifest | null {
65
- const manifestPath = getManifestPath(target);
66
- if (!fs.existsSync(manifestPath)) {
67
- return null;
68
- }
69
- try {
70
- const content = fs.readFileSync(manifestPath, "utf-8");
71
- return JSON.parse(content) as StackPatchManifest;
72
- } catch {
73
- return null;
74
- }
75
- }
76
-
77
- // Write manifest
78
- function writeManifest(target: string, manifest: StackPatchManifest): void {
79
- const manifestDir = path.join(target, ".stackpatch");
80
- const manifestPath = getManifestPath(target);
81
-
82
- if (!fs.existsSync(manifestDir)) {
83
- fs.mkdirSync(manifestDir, { recursive: true });
84
- }
85
-
86
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
87
- }
88
-
89
- // Backup a file before modifying it
90
- function backupFile(filePath: string, target: string): string | null {
91
- if (!fs.existsSync(filePath)) {
92
- return null;
93
- }
94
-
95
- const backupDir = path.join(target, ".stackpatch", "backups");
96
- if (!fs.existsSync(backupDir)) {
97
- fs.mkdirSync(backupDir, { recursive: true });
98
- }
99
-
100
- const relativePath = path.relative(target, filePath);
101
- const backupPath = path.join(backupDir, relativePath.replace(/\//g, "_").replace(/\\/g, "_"));
102
-
103
- // Create directory structure in backup
104
- const backupFileDir = path.dirname(backupPath);
105
- if (!fs.existsSync(backupFileDir)) {
106
- fs.mkdirSync(backupFileDir, { recursive: true });
107
- }
108
-
109
- fs.copyFileSync(filePath, backupPath);
110
- return backupPath;
111
- }
112
-
113
- // Restore a file from backup
114
- function restoreFile(backupPath: string, originalPath: string): boolean {
115
- if (!fs.existsSync(backupPath)) {
116
- return false;
117
- }
118
-
119
- const originalDir = path.dirname(originalPath);
120
- if (!fs.existsSync(originalDir)) {
121
- fs.mkdirSync(originalDir, { recursive: true });
122
- }
123
-
124
- fs.copyFileSync(backupPath, originalPath);
125
- return true;
126
- }
127
-
128
- // ---------------- Progress & UI Helpers ----------------
129
- // Show StackPatch logo (based on the actual SVG logo design)
130
- function showLogo() {
131
- console.log("\n");
132
-
133
- // StackPatch logo ASCII art
134
- const logo = [
135
- chalk.magentaBright(" _________ __ __ __________ __ .__"),
136
- chalk.magentaBright(" / _____// |______ ____ | | __ \\\\______ \\_____ _/ |_ ____ | |__"),
137
- chalk.magentaBright(" \\_____ \\\\ __\\__ \\ _/ ___\\| |/ / | ___/\\__ \\\\ __\\/ ___\\| | \\"),
138
- chalk.magentaBright(" / \\| | / __ \\\\ \\___| < | | / __ \\| | \\ \\___| Y \\"),
139
- chalk.magentaBright("/_______ /|__| (____ /\\___ >__|_ \\ |____| (____ /__| \\___ >___| /"),
140
- chalk.magentaBright(" \\/ \\/ \\/ \\/ \\/ \\/ \\/"),
141
- "",
142
- chalk.white(" Composable frontend features for modern React & Next.js"),
143
- chalk.gray(" Add authentication, UI components, and more with zero configuration"),
144
- "",
145
- ];
146
-
147
- logo.forEach(line => console.log(line));
148
- }
149
-
150
- // Progress tracker with checkmarks
151
- class ProgressTracker {
152
- private steps: Array<{ name: string; status: "pending" | "processing" | "completed" | "failed"; interval?: NodeJS.Timeout }> = [];
153
-
154
- addStep(name: string) {
155
- this.steps.push({ name, status: "pending" });
156
- }
157
-
158
- startStep(index: number) {
159
- if (index >= 0 && index < this.steps.length) {
160
- this.steps[index].status = "processing";
161
- const frames = ["ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā "];
162
- let frameIndex = 0;
163
- const step = this.steps[index];
164
-
165
- const interval = setInterval(() => {
166
- process.stdout.write(`\r${chalk.yellow(frames[frameIndex])} ${step.name}`);
167
- frameIndex = (frameIndex + 1) % frames.length;
168
- }, 100);
169
-
170
- this.steps[index].interval = interval;
171
- }
172
- }
173
-
174
- completeStep(index: number) {
175
- if (index >= 0 && index < this.steps.length) {
176
- if (this.steps[index].interval) {
177
- clearInterval(this.steps[index].interval);
178
- this.steps[index].interval = undefined;
179
- }
180
- process.stdout.write(`\r${chalk.green("āœ“")} ${this.steps[index].name}\n`);
181
- this.steps[index].status = "completed";
182
- }
183
- }
184
-
185
- failStep(index: number) {
186
- if (index >= 0 && index < this.steps.length) {
187
- if (this.steps[index].interval) {
188
- clearInterval(this.steps[index].interval);
189
- this.steps[index].interval = undefined;
190
- }
191
- process.stdout.write(`\r${chalk.red("āœ—")} ${this.steps[index].name}\n`);
192
- this.steps[index].status = "failed";
193
- }
194
- }
195
-
196
- getSteps() {
197
- return this.steps;
198
- }
199
- }
200
-
201
- // Helper function with spinner and checkmark
202
- async function withSpinner<T>(text: string, fn: () => Promise<T> | T): Promise<T> {
203
- const frames = ["ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā "];
204
- let frameIndex = 0;
205
-
206
- const interval = setInterval(() => {
207
- process.stdout.write(`\r${chalk.yellow(frames[frameIndex])} ${text}`);
208
- frameIndex = (frameIndex + 1) % frames.length;
209
- }, 100);
210
-
211
- try {
212
- const result = await fn();
213
- clearInterval(interval);
214
- process.stdout.write(`\r${chalk.green("āœ“")} ${text}\n`);
215
- return result;
216
- } catch (error) {
217
- clearInterval(interval);
218
- process.stdout.write(`\r${chalk.red("āœ—")} ${text}\n`);
219
- throw error;
220
- }
221
- }
222
-
223
- // Ask if user wants to go back
224
- async function askGoBack(): Promise<boolean> {
225
- const { goBack } = await inquirer.prompt([
226
- {
227
- type: "confirm",
228
- name: "goBack",
229
- message: chalk.yellow("Do you want to go back and change your selection?"),
230
- default: false,
231
- },
232
- ]);
233
- return goBack;
234
- }
235
- async function copyFiles(src: string, dest: string): Promise<{ success: boolean; addedFiles: string[] }> {
236
- const addedFiles: string[] = [];
237
-
238
- if (!fs.existsSync(src)) {
239
- console.log(chalk.red(`āŒ Boilerplate folder not found: ${src}`));
240
- return { success: false, addedFiles: [] };
241
- }
242
-
243
- await fse.ensureDir(dest);
244
-
245
- // Detect app directory location in target
246
- const appDir = detectAppDirectory(dest);
247
- const appDirPath = path.join(dest, appDir);
248
- const componentsDir = detectComponentsDirectory(dest);
249
- const componentsDirPath = path.join(dest, componentsDir);
250
-
251
- const conflicts: string[] = [];
252
-
253
- // Check for conflicts before copying
254
- const entries = fse.readdirSync(src, { withFileTypes: true });
255
- for (const entry of entries) {
256
- if (entry.name === "app") {
257
- // For app directory, check conflicts in the detected app directory
258
- if (fs.existsSync(appDirPath)) {
259
- const appEntries = fse.readdirSync(path.join(src, "app"), { withFileTypes: true });
260
- for (const appEntry of appEntries) {
261
- const destAppPath = path.join(appDirPath, appEntry.name);
262
- if (fs.existsSync(destAppPath)) {
263
- conflicts.push(destAppPath);
264
- }
265
- }
266
- }
267
- } else if (entry.name === "components") {
268
- // For components directory, check conflicts in the detected components directory
269
- if (fs.existsSync(componentsDirPath)) {
270
- const componentEntries = fse.readdirSync(path.join(src, "components"), { withFileTypes: true });
271
- for (const componentEntry of componentEntries) {
272
- const destComponentPath = path.join(componentsDirPath, componentEntry.name);
273
- if (fs.existsSync(destComponentPath)) {
274
- conflicts.push(destComponentPath);
275
- }
276
- }
277
- }
278
- } else {
279
- // For other files/directories (middleware, etc.), check in root
280
- const destPath = path.join(dest, entry.name);
281
- if (fs.existsSync(destPath)) {
282
- conflicts.push(destPath);
283
- }
284
- }
285
- }
286
-
287
- if (conflicts.length) {
288
- console.log(chalk.yellow("\nāš ļø The following files already exist:"));
289
- conflicts.forEach((f) => console.log(` ${f}`));
290
-
291
- const { overwrite } = await inquirer.prompt([
292
- {
293
- type: "confirm",
294
- name: "overwrite",
295
- message: "Do you want to overwrite them?",
296
- default: false,
297
- },
298
- ]);
299
-
300
- if (!overwrite) {
301
- console.log(chalk.red("\nAborted! No files were copied."));
302
- return { success: false, addedFiles: [] };
303
- }
304
- }
305
-
306
- // Track files from SOURCE (boilerplate) before copying
307
- // This ensures we only track files that are actually from StackPatch
308
- function trackSourceFiles(srcDir: string, baseDir: string, targetBase: string): void {
309
- if (!fs.existsSync(srcDir)) return;
310
-
311
- const files = fs.readdirSync(srcDir, { withFileTypes: true });
312
- for (const file of files) {
313
- const srcFilePath = path.join(srcDir, file.name);
314
- if (file.isDirectory()) {
315
- trackSourceFiles(srcFilePath, baseDir, targetBase);
316
- } else {
317
- const relativePath = path.relative(baseDir, srcFilePath);
318
- const targetPath = targetBase
319
- ? path.join(targetBase, relativePath).replace(/\\/g, "/")
320
- : relativePath.replace(/\\/g, "/");
321
- addedFiles.push(targetPath);
322
- }
323
- }
324
- }
325
-
326
- // Copy files with smart app directory handling
327
- for (const entry of entries) {
328
- const srcPath = path.join(src, entry.name);
329
-
330
- if (entry.name === "app") {
331
- // Track files from SOURCE boilerplate before copying
332
- trackSourceFiles(srcPath, srcPath, appDir);
333
-
334
- // Copy app directory contents to the detected app directory location
335
- await fse.ensureDir(appDirPath);
336
- await fse.copy(srcPath, appDirPath, { overwrite: true });
337
- } else if (entry.name === "components") {
338
- // Track files from SOURCE boilerplate before copying
339
- trackSourceFiles(srcPath, srcPath, componentsDir);
340
-
341
- // Copy components directory to the detected components directory location
342
- await fse.ensureDir(componentsDirPath);
343
- await fse.copy(srcPath, componentsDirPath, { overwrite: true });
344
- } else {
345
- // For root-level files/directories, track from source
346
- const srcStat = fs.statSync(srcPath);
347
- if (srcStat.isDirectory()) {
348
- trackSourceFiles(srcPath, srcPath, "");
349
- } else {
350
- addedFiles.push(entry.name);
351
- }
352
-
353
- // Copy other files/directories (middleware, etc.) to root
354
- const destPath = path.join(dest, entry.name);
355
- await fse.copy(srcPath, destPath, { overwrite: true });
356
- }
357
- }
358
-
359
- // Update imports in copied files to use correct paths
360
- updateImportsInFiles(dest);
361
-
362
- return { success: true, addedFiles };
363
- }
364
-
365
- // Detect the app directory location (app/ or src/app/)
366
- function detectAppDirectory(target: string): string {
367
- // Check for src/app first (more common in modern Next.js projects)
368
- if (fs.existsSync(path.join(target, "src", "app"))) {
369
- return "src/app";
370
- }
371
- // Check for app directory
372
- if (fs.existsSync(path.join(target, "app"))) {
373
- return "app";
374
- }
375
- // Check for src/pages (legacy)
376
- if (fs.existsSync(path.join(target, "src", "pages"))) {
377
- return "src/pages";
378
- }
379
- // Check for pages (legacy)
380
- if (fs.existsSync(path.join(target, "pages"))) {
381
- return "pages";
382
- }
383
- // Default to app if nothing found (will fail gracefully later)
384
- return "app";
385
- }
386
-
387
- // Detect the components directory location (components/ or src/components/)
388
- function detectComponentsDirectory(target: string): string {
389
- const appDir = detectAppDirectory(target);
390
-
391
- // If app is in src/app, components should be in src/components
392
- if (appDir.startsWith("src/")) {
393
- // Check if src/components exists
394
- if (fs.existsSync(path.join(target, "src", "components"))) {
395
- return "src/components";
396
- }
397
- // Even if it doesn't exist yet, return src/components to match app structure
398
- return "src/components";
399
- }
400
-
401
- // If app is in root, components should be in root
402
- if (fs.existsSync(path.join(target, "components"))) {
403
- return "components";
404
- }
405
-
406
- // Default to components
407
- return "components";
408
- }
409
-
410
- // Detect path aliases from tsconfig.json
411
- function detectPathAliases(target: string): { alias: string; path: string } | null {
412
- const tsconfigPath = path.join(target, "tsconfig.json");
413
-
414
- if (!fs.existsSync(tsconfigPath)) {
415
- return null;
416
- }
417
-
418
- try {
419
- const tsconfigContent = fs.readFileSync(tsconfigPath, "utf-8");
420
- const tsconfig = JSON.parse(tsconfigContent);
421
-
422
- const paths = tsconfig.compilerOptions?.paths;
423
- if (!paths || typeof paths !== "object") {
424
- return null;
425
- }
426
-
427
- // Look for common aliases like @/*, ~/*, etc.
428
- for (const [alias, pathsArray] of Object.entries(paths)) {
429
- if (Array.isArray(pathsArray) && pathsArray.length > 0) {
430
- // Remove the /* from alias (e.g., "@/*" -> "@")
431
- const cleanAlias = alias.replace(/\/\*$/, "");
432
- // Get the first path and remove /* from it
433
- const cleanPath = pathsArray[0].replace(/\/\*$/, "");
434
- return { alias: cleanAlias, path: cleanPath };
435
- }
436
- }
437
- } catch (error) {
438
- // If parsing fails, return null
439
- return null;
440
- }
441
-
442
- return null;
443
- }
444
-
445
- // Generate import path for components
446
- function generateComponentImportPath(target: string, componentName: string, fromFile: string): string {
447
- const pathAlias = detectPathAliases(target);
448
- const componentsDir = detectComponentsDirectory(target);
449
-
450
- // If we have a path alias, use it
451
- if (pathAlias) {
452
- // Check if the alias path matches components directory
453
- const aliasPath = pathAlias.path.replace(/^\.\//, ""); // Remove leading ./
454
-
455
- // If alias points to root and components is in root, use alias
456
- if (aliasPath === "" && componentsDir === "components") {
457
- return `${pathAlias.alias}/components/${componentName}`;
458
- }
459
-
460
- // If alias points to src and components is in src/components, use alias
461
- if (aliasPath === "src" && componentsDir === "src/components") {
462
- return `${pathAlias.alias}/components/${componentName}`;
463
- }
464
-
465
- // Try to match the alias path structure
466
- if (componentsDir.startsWith(aliasPath)) {
467
- const relativeFromAlias = componentsDir.replace(new RegExp(`^${aliasPath}/?`), "");
468
- return `${pathAlias.alias}/${relativeFromAlias}/${componentName}`;
469
- }
470
-
471
- // If alias path is "./" (root), components should be accessible via alias
472
- if (aliasPath === "" || aliasPath === ".") {
473
- return `${pathAlias.alias}/components/${componentName}`;
474
- }
475
- }
476
-
477
- // Fallback: calculate relative path
478
- // fromFile is the full path to the file we're importing into
479
- const fromDir = path.dirname(fromFile);
480
- const toComponents = path.join(target, componentsDir);
481
-
482
- // Calculate relative path from the file's directory to components directory
483
- const relativePath = path.relative(fromDir, toComponents).replace(/\\/g, "/");
484
- const normalizedPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
485
-
486
- return `${normalizedPath}/${componentName}`;
487
- }
488
-
489
- // Update imports in copied files to use correct paths
490
- function updateImportsInFiles(target: string) {
491
- const appDir = detectAppDirectory(target);
492
- const appDirPath = path.join(target, appDir);
493
-
494
- if (!fs.existsSync(appDirPath)) {
495
- return;
496
- }
497
-
498
- // Recursively find all .tsx and .ts files in the app directory
499
- function findFiles(dir: string, fileList: string[] = []): string[] {
500
- const files = fs.readdirSync(dir);
501
-
502
- for (const file of files) {
503
- const filePath = path.join(dir, file);
504
- const stat = fs.statSync(filePath);
505
-
506
- if (stat.isDirectory()) {
507
- findFiles(filePath, fileList);
508
- } else if (file.endsWith('.tsx') || file.endsWith('.ts')) {
509
- fileList.push(filePath);
510
- }
511
- }
512
-
513
- return fileList;
514
- }
515
-
516
- const files = findFiles(appDirPath);
517
-
518
- for (const filePath of files) {
519
- try {
520
- let content = fs.readFileSync(filePath, 'utf-8');
521
- let updated = false;
522
-
523
- // Match imports like: from "@/components/component-name"
524
- const importRegex = /from\s+["']@\/components\/([^"']+)["']/g;
525
- const matches = Array.from(content.matchAll(importRegex));
526
-
527
- for (const match of matches) {
528
- const componentName = match[1];
529
- const oldImport = match[0];
530
- const newImportPath = generateComponentImportPath(target, componentName, filePath);
531
- const newImport = oldImport.replace(/@\/components\/[^"']+/, newImportPath);
532
-
533
- content = content.replace(oldImport, newImport);
534
- updated = true;
535
- }
536
-
537
- if (updated) {
538
- fs.writeFileSync(filePath, content, 'utf-8');
539
- }
540
- } catch (error) {
541
- // Silently skip files that can't be processed
542
- continue;
543
- }
544
- }
545
- }
546
-
547
- // Check if dependency exists in package.json
548
- function hasDependency(target: string, depName: string): boolean {
549
- const packageJsonPath = path.join(target, "package.json");
550
- if (!fs.existsSync(packageJsonPath)) return false;
551
-
552
- try {
553
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
554
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
555
- return !!deps[depName];
556
- } catch {
557
- return false;
558
- }
559
- }
560
-
561
- // Install dependencies (only missing ones)
562
- function installDependencies(target: string, deps: string[]) {
563
- if (deps.length === 0) return;
564
-
565
- const missingDeps = deps.filter(dep => !hasDependency(target, dep));
566
-
567
- if (missingDeps.length === 0) {
568
- return; // Already installed, spinner will show completion
569
- }
570
-
571
- const result = spawnSync("pnpm", ["add", ...missingDeps], {
572
- cwd: target,
573
- stdio: "pipe",
574
- });
575
-
576
- if (result.status !== 0) {
577
- throw new Error(`Failed to install dependencies: ${missingDeps.join(", ")}`);
578
- }
579
- }
580
-
581
- // Remove dependencies from package.json
582
- function removeDependencies(target: string, deps: string[]): boolean {
583
- if (deps.length === 0) return true;
584
-
585
- const packageJsonPath = path.join(target, "package.json");
586
- if (!fs.existsSync(packageJsonPath)) {
587
- return false;
588
- }
589
-
590
- try {
591
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
592
- let modified = false;
593
-
594
- // Remove from dependencies
595
- if (packageJson.dependencies) {
596
- for (const dep of deps) {
597
- if (packageJson.dependencies[dep]) {
598
- delete packageJson.dependencies[dep];
599
- modified = true;
600
- }
601
- }
602
- }
603
-
604
- // Remove from devDependencies
605
- if (packageJson.devDependencies) {
606
- for (const dep of deps) {
607
- if (packageJson.devDependencies[dep]) {
608
- delete packageJson.devDependencies[dep];
609
- modified = true;
610
- }
611
- }
612
- }
613
-
614
- if (modified) {
615
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
616
- }
617
-
618
- return modified;
619
- } catch {
620
- return false;
621
- }
622
- }
623
-
624
- // Remove empty directories recursively
625
- function removeEmptyDirectories(dirPath: string, rootPath: string): void {
626
- if (!fs.existsSync(dirPath)) return;
627
-
628
- // Don't remove the root directory or .stackpatch
629
- if (dirPath === rootPath || dirPath.includes(".stackpatch")) return;
630
-
631
- try {
632
- const entries = fs.readdirSync(dirPath);
633
-
634
- // Recursively remove empty subdirectories
635
- for (const entry of entries) {
636
- const fullPath = path.join(dirPath, entry);
637
- if (fs.statSync(fullPath).isDirectory()) {
638
- removeEmptyDirectories(fullPath, rootPath);
639
- }
640
- }
641
-
642
- // Check if directory is now empty (after removing subdirectories)
643
- const remainingEntries = fs.readdirSync(dirPath);
644
- if (remainingEntries.length === 0) {
645
- fs.rmdirSync(dirPath);
646
- }
647
- } catch {
648
- // Ignore errors when removing directories
649
- }
650
- }
651
-
652
- // Get all parent directories of a file path
653
- function getParentDirectories(filePath: string, rootPath: string): string[] {
654
- const dirs: string[] = [];
655
- let current = path.dirname(filePath);
656
- const root = path.resolve(rootPath);
657
-
658
- while (current !== root && current !== path.dirname(current)) {
659
- dirs.push(current);
660
- current = path.dirname(current);
661
- }
662
-
663
- return dirs;
664
- }
665
-
666
- // Update layout.tsx to include Toaster
667
- function updateLayoutForToaster(target: string): { success: boolean; modified: boolean; filePath: string; originalContent?: string } {
668
- const appDir = detectAppDirectory(target);
669
- const layoutPath = path.join(target, appDir, "layout.tsx");
670
-
671
- if (!fs.existsSync(layoutPath)) {
672
- return { success: false, modified: false, filePath: layoutPath };
673
- }
674
-
675
- try {
676
- const originalContent = fs.readFileSync(layoutPath, "utf-8");
677
- let layoutContent = originalContent;
678
-
679
- // Check if already has Toaster
680
- if (layoutContent.includes("Toaster")) {
681
- console.log(chalk.green("āœ… Layout already has Toaster!"));
682
- return { success: true, modified: false, filePath: layoutPath };
683
- }
684
-
685
- // Generate the correct import path
686
- const importPath = generateComponentImportPath(target, "toaster", layoutPath);
687
-
688
- // Check if import already exists (check for various patterns)
689
- const hasImport = layoutContent.includes("toaster") &&
690
- (layoutContent.includes("from") || layoutContent.includes("import"));
691
-
692
- if (!hasImport) {
693
- // Find the last import statement
694
- const lines = layoutContent.split("\n");
695
- let lastImportIndex = -1;
696
-
697
- for (let i = 0; i < lines.length; i++) {
698
- const trimmed = lines[i].trim();
699
- if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
700
- lastImportIndex = i;
701
- } else if (trimmed && !trimmed.startsWith("//") && lastImportIndex >= 0) {
702
- break;
703
- }
704
- }
705
-
706
- if (lastImportIndex >= 0) {
707
- lines.splice(lastImportIndex + 1, 0, `import { Toaster } from "${importPath}";`);
708
- layoutContent = lines.join("\n");
709
- }
710
- }
711
-
712
- // Add Toaster component before closing AuthSessionProvider
713
- if (layoutContent.includes("</AuthSessionProvider>")) {
714
- layoutContent = layoutContent.replace(
715
- /(<\/AuthSessionProvider>)/,
716
- '<Toaster />\n $1'
717
- );
718
- } else if (layoutContent.includes("{children}")) {
719
- // If AuthSessionProvider wraps children, add Toaster after children
720
- layoutContent = layoutContent.replace(
721
- /(\{children\})/,
722
- '$1\n <Toaster />'
723
- );
724
- }
725
-
726
- // Backup before modifying
727
- backupFile(layoutPath, target);
728
-
729
- fs.writeFileSync(layoutPath, layoutContent, "utf-8");
730
- console.log(chalk.green("āœ… Updated layout.tsx with Toaster!"));
731
-
732
- const relativePath = path.relative(target, layoutPath).replace(/\\/g, "/");
733
- return { success: true, modified: true, filePath: relativePath, originalContent };
734
- } catch (error: unknown) {
735
- const errorMessage = error instanceof Error ? error.message : String(error);
736
- console.log(chalk.yellow(`āš ļø Failed to update layout with Toaster: ${errorMessage}`));
737
- return { success: false, modified: false, filePath: layoutPath };
738
- }
739
- }
740
-
741
- // Update layout.tsx to include AuthSessionProvider
742
- function updateLayoutForAuth(target: string): { success: boolean; modified: boolean; filePath: string; originalContent?: string } {
743
- const appDir = detectAppDirectory(target);
744
- const layoutPath = path.join(target, appDir, "layout.tsx");
745
-
746
- if (!fs.existsSync(layoutPath)) {
747
- console.log(chalk.yellow("āš ļø layout.tsx not found. Skipping layout update."));
748
- return { success: false, modified: false, filePath: layoutPath };
749
- }
750
-
751
- try {
752
- const originalContent = fs.readFileSync(layoutPath, "utf-8");
753
- let layoutContent = originalContent;
754
-
755
- // Check if already has AuthSessionProvider
756
- if (layoutContent.includes("AuthSessionProvider")) {
757
- console.log(chalk.green("āœ… Layout already has AuthSessionProvider!"));
758
- return { success: true, modified: false, filePath: layoutPath };
759
- }
760
-
761
- // Generate the correct import path
762
- const importPath = generateComponentImportPath(target, "session-provider", layoutPath);
763
-
764
- // Check if import already exists (check for various patterns)
765
- const hasImport = layoutContent.includes("session-provider") &&
766
- (layoutContent.includes("from") || layoutContent.includes("import"));
767
-
768
- if (!hasImport) {
769
- // Find the last import statement (before the first non-import line)
770
- const lines = layoutContent.split("\n");
771
- let lastImportIndex = -1;
772
-
773
- for (let i = 0; i < lines.length; i++) {
774
- const trimmed = lines[i].trim();
775
- if (trimmed.startsWith("import ") && trimmed.endsWith(";")) {
776
- lastImportIndex = i;
777
- } else if (trimmed && !trimmed.startsWith("//") && lastImportIndex >= 0) {
778
- break;
779
- }
780
- }
781
-
782
- if (lastImportIndex >= 0) {
783
- lines.splice(lastImportIndex + 1, 0, `import { AuthSessionProvider } from "${importPath}";`);
784
- layoutContent = lines.join("\n");
785
- } else {
786
- // No imports found, add after the first line
787
- const firstNewline = layoutContent.indexOf("\n");
788
- if (firstNewline > 0) {
789
- layoutContent =
790
- layoutContent.slice(0, firstNewline + 1) +
791
- `import { AuthSessionProvider } from "${importPath}";\n` +
792
- layoutContent.slice(firstNewline + 1);
793
- } else {
794
- layoutContent = `import { AuthSessionProvider } from "${importPath}";\n` + layoutContent;
795
- }
796
- }
797
- }
798
-
799
- // Wrap children with AuthSessionProvider
800
- // Look for {children} pattern in body tag
801
- const childrenPattern = /(\s*)(\{children\})(\s*)/;
802
- if (childrenPattern.test(layoutContent)) {
803
- layoutContent = layoutContent.replace(
804
- childrenPattern,
805
- '$1<AuthSessionProvider>{children}</AuthSessionProvider>$3'
806
- );
807
- } else {
808
- // Try to find body tag and wrap its content
809
- const bodyRegex = /(<body[^>]*>)([\s\S]*?)(<\/body>)/;
810
- const bodyMatch = layoutContent.match(bodyRegex);
811
- if (bodyMatch) {
812
- const bodyContent = bodyMatch[2].trim();
813
- if (bodyContent && !bodyContent.includes("AuthSessionProvider")) {
814
- layoutContent = layoutContent.replace(
815
- bodyRegex,
816
- `$1\n <AuthSessionProvider>${bodyContent}</AuthSessionProvider>\n $3`
817
- );
818
- }
819
- }
820
- }
821
-
822
- // Backup before modifying
823
- backupFile(layoutPath, target);
824
-
825
- fs.writeFileSync(layoutPath, layoutContent, "utf-8");
826
- console.log(chalk.green("āœ… Updated layout.tsx with AuthSessionProvider!"));
827
-
828
- const relativePath = path.relative(target, layoutPath).replace(/\\/g, "/");
829
- return { success: true, modified: true, filePath: relativePath, originalContent };
830
- } catch (error: unknown) {
831
- const errorMessage = error instanceof Error ? error.message : String(error);
832
- console.log(chalk.red(`āŒ Failed to update layout.tsx: ${errorMessage}`));
833
- return { success: false, modified: false, filePath: layoutPath };
834
- }
835
- }
836
-
837
- // Ask user which OAuth providers to configure
838
- async function askOAuthProviders(): Promise<string[]> {
839
- const { providers } = await inquirer.prompt([
840
- {
841
- type: "checkbox",
842
- name: "providers",
843
- message: "Which OAuth providers would you like to configure?",
844
- choices: [
845
- { name: "Google", value: "google", checked: true },
846
- { name: "GitHub", value: "github", checked: true },
847
- { name: "Email/Password (Credentials)", value: "credentials", checked: true },
848
- ],
849
- validate: (input: string[]) => {
850
- if (input.length === 0) {
851
- return "Please select at least one provider";
852
- }
853
- return true;
854
- },
855
- },
856
- ]);
857
- return providers;
858
- }
859
-
860
- // Setup authentication with selected OAuth providers
861
- async function setupAuth(target: string, selectedProviders: string[]): Promise<boolean> {
862
- const tracker = new ProgressTracker();
863
- tracker.addStep("Setting up authentication");
864
- tracker.addStep("Generating environment files");
865
- tracker.addStep("Configuring NextAuth with selected providers");
866
- tracker.addStep("Updating UI components");
867
-
868
- try {
869
- const appDir = detectAppDirectory(target);
870
- const nextAuthRoutePath = path.join(target, appDir, "api/auth/[...nextauth]/route.ts");
871
-
872
- if (!fs.existsSync(nextAuthRoutePath)) {
873
- console.log(chalk.yellow("āš ļø NextAuth route not found, skipping auth setup"));
874
- return false;
875
- }
876
-
877
- tracker.startStep(0);
878
-
879
- // Step 1: Generate .env.example file
880
- tracker.startStep(1);
881
- await generateEnvExample(target, selectedProviders);
882
- tracker.completeStep(1);
883
-
884
- // Step 2: Update NextAuth route with selected providers
885
- tracker.startStep(2);
886
- await updateNextAuthWithProviders(nextAuthRoutePath, selectedProviders);
887
- tracker.completeStep(2);
888
-
889
- // Step 3: Update UI components
890
- tracker.startStep(3);
891
- await updateAuthButtonWithProviders(target, selectedProviders);
892
- tracker.completeStep(3);
893
-
894
- tracker.completeStep(0);
895
-
896
- // Show OAuth setup instructions
897
- await showOAuthSetupInstructions(target, selectedProviders);
898
-
899
- return true;
900
- } catch (error) {
901
- tracker.failStep(0);
902
- return false;
903
- }
904
- }
905
-
906
- // Show OAuth setup instructions
907
- async function showOAuthSetupInstructions(target: string, selectedProviders: string[] = ["google", "github", "credentials"]) {
908
- const envLocalPath = path.join(target, ".env.local");
909
- let hasGoogleCreds = false;
910
- let hasGitHubCreds = false;
911
-
912
- if (fs.existsSync(envLocalPath)) {
913
- const envContent = fs.readFileSync(envLocalPath, "utf-8");
914
- hasGoogleCreds = envContent.includes("GOOGLE_CLIENT_ID") &&
915
- envContent.includes("GOOGLE_CLIENT_SECRET") &&
916
- !envContent.includes("your_google_client_id_here");
917
- hasGitHubCreds = envContent.includes("GITHUB_CLIENT_ID") &&
918
- envContent.includes("GITHUB_CLIENT_SECRET") &&
919
- !envContent.includes("your_github_client_id_here");
920
- }
921
-
922
- console.log(chalk.blue.bold("\nšŸ“‹ OAuth Setup Instructions\n"));
923
-
924
- const needsGoogle = selectedProviders.includes("google") && !hasGoogleCreds;
925
- const needsGitHub = selectedProviders.includes("github") && !hasGitHubCreds;
926
-
927
- if (needsGoogle || needsGitHub) {
928
- console.log(chalk.yellow("āš ļø OAuth credentials not configured yet.\n"));
929
-
930
- if (needsGoogle) {
931
- console.log(chalk.cyan.bold("šŸ”µ Google OAuth Setup:"));
932
- console.log(chalk.white(" 1. Go to: ") + chalk.underline("https://console.cloud.google.com/"));
933
- console.log(chalk.white(" 2. Create a new project or select existing one"));
934
- console.log(chalk.white(" 3. Navigate to: APIs & Services > Credentials"));
935
- console.log(chalk.white(" 4. Click: Create Credentials > OAuth client ID"));
936
- console.log(chalk.white(" 5. Choose: Web application"));
937
- console.log(chalk.white(" 6. Add Authorized redirect URI:"));
938
- console.log(chalk.magentaBright(" → ") + chalk.bold("http://localhost:3000/api/auth/callback/google"));
939
- console.log(chalk.white(" 7. Copy Client ID and Client Secret"));
940
- console.log(chalk.white(" 8. Add them to your .env.local file:\n"));
941
- console.log(chalk.gray(" GOOGLE_CLIENT_ID=your_client_id_here"));
942
- console.log(chalk.gray(" GOOGLE_CLIENT_SECRET=your_client_secret_here\n"));
943
- }
944
-
945
- if (needsGitHub) {
946
- console.log(chalk.cyan.bold("šŸ™ GitHub OAuth Setup:"));
947
- console.log(chalk.white(" 1. Go to: ") + chalk.underline("https://github.com/settings/developers"));
948
- console.log(chalk.white(" 2. Click: New OAuth App"));
949
- console.log(chalk.white(" 3. Fill in the form:"));
950
- console.log(chalk.white(" - Application name: Your app name"));
951
- console.log(chalk.white(" - Homepage URL: http://localhost:3000"));
952
- console.log(chalk.white(" - Authorization callback URL:"));
953
- console.log(chalk.magentaBright(" → ") + chalk.bold("http://localhost:3000/api/auth/callback/github"));
954
- console.log(chalk.white(" 4. Click: Register application"));
955
- console.log(chalk.white(" 5. Copy Client ID"));
956
- console.log(chalk.white(" 6. Generate a new Client Secret"));
957
- console.log(chalk.white(" 7. Add them to your .env.local file:\n"));
958
- console.log(chalk.gray(" GITHUB_CLIENT_ID=your_client_id_here"));
959
- console.log(chalk.gray(" GITHUB_CLIENT_SECRET=your_client_secret_here\n"));
960
- }
961
-
962
- if (selectedProviders.includes("google") || selectedProviders.includes("github")) {
963
- console.log(chalk.blue.bold("šŸ“ Required Redirect URIs:"));
964
- if (selectedProviders.includes("google")) {
965
- console.log(chalk.white(" For Google: ") + chalk.bold("http://localhost:3000/api/auth/callback/google"));
966
- }
967
- if (selectedProviders.includes("github")) {
968
- console.log(chalk.white(" For GitHub: ") + chalk.bold("http://localhost:3000/api/auth/callback/github"));
969
- }
970
- console.log(chalk.gray("\n For production, also add your production domain URLs\n"));
971
- }
972
-
973
- console.log(chalk.green("āœ… Once configured, restart your dev server and test OAuth login!"));
974
- } else {
975
- console.log(chalk.green("āœ… OAuth credentials are configured!"));
976
- console.log(chalk.white(" Make sure your redirect URIs are set in:"));
977
- console.log(chalk.cyan(" - Google Cloud Console"));
978
- console.log(chalk.cyan(" - GitHub OAuth App settings\n"));
979
- }
980
-
981
- console.log(chalk.blue.bold("\nšŸ“š Documentation:"));
982
- console.log(chalk.white(" - Complete guide: ") + chalk.cyan("README.md"));
983
- console.log(chalk.white(" - Custom Auth: See comments in ") + chalk.cyan("app/api/auth/[...nextauth]/route.ts"));
984
- console.log(chalk.white(" - Login/Signup: See comments in ") + chalk.cyan("app/auth/login/page.tsx") + " and " + chalk.cyan("app/auth/signup/page.tsx\n"));
985
- }
986
-
987
- // Generate .env.example file
988
- async function generateEnvExample(target: string, providers: string[] = ["google", "github", "credentials"]) {
989
- const envExamplePath = path.join(target, ".env.example");
990
- const envLocalPath = path.join(target, ".env.local");
991
-
992
- // Generate a random secret for NEXTAUTH_SECRET
993
- const generateSecret = () => {
994
- if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
995
- return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(32)))
996
- .map(b => b.toString(16).padStart(2, '0'))
997
- .join('');
998
- }
999
- // Fallback: generate random bytes
1000
- const bytes = new Uint8Array(32);
1001
- for (let i = 0; i < 32; i++) {
1002
- bytes[i] = Math.floor(Math.random() * 256);
1003
- }
1004
- return Array.from(bytes)
1005
- .map(b => b.toString(16).padStart(2, '0'))
1006
- .join('');
1007
- };
1008
-
1009
- let envContent = `# NextAuth Configuration
1010
- NEXTAUTH_URL=http://localhost:3000
1011
- NEXTAUTH_SECRET=${generateSecret()}
1012
-
1013
- `;
1014
-
1015
- if (providers.includes("google")) {
1016
- envContent += `# Google OAuth
1017
- GOOGLE_CLIENT_ID=your_google_client_id_here
1018
- GOOGLE_CLIENT_SECRET=your_google_client_secret_here
1019
-
1020
- `;
1021
- }
1022
-
1023
- if (providers.includes("github")) {
1024
- envContent += `# GitHub OAuth
1025
- GITHUB_CLIENT_ID=your_github_client_id_here
1026
- GITHUB_CLIENT_SECRET=your_github_client_secret_here
1027
-
1028
- `;
1029
- }
1030
-
1031
- // Write .env.example
1032
- fs.writeFileSync(envExamplePath, envContent, "utf-8");
1033
- console.log(chalk.green("āœ… Created .env.example file"));
1034
-
1035
- // Create .env.local if it doesn't exist
1036
- if (!fs.existsSync(envLocalPath)) {
1037
- fs.writeFileSync(envLocalPath, envContent.replace(/your_.*_here/g, ""), "utf-8");
1038
- console.log(chalk.green("āœ… Created .env.local file (update with your credentials)"));
1039
- }
1040
- }
1041
-
1042
- // Update NextAuth route with providers
1043
- async function updateNextAuthWithProviders(routePath: string, selectedProviders: string[] = ["google", "github", "credentials"]) {
1044
- // Build imports based on selected providers
1045
- const imports = ["import NextAuth from \"next-auth\";", "import type { NextAuthOptions } from \"next-auth\";"];
1046
-
1047
- if (selectedProviders.includes("google")) {
1048
- imports.push("import GoogleProvider from \"next-auth/providers/google\";");
1049
- }
1050
- if (selectedProviders.includes("github")) {
1051
- imports.push("import GitHubProvider from \"next-auth/providers/github\";");
1052
- }
1053
- if (selectedProviders.includes("credentials")) {
1054
- imports.push("import CredentialsProvider from \"next-auth/providers/credentials\";");
1055
- }
1056
-
1057
- // Build providers array
1058
- const providersArray: string[] = [];
1059
-
1060
- if (selectedProviders.includes("credentials")) {
1061
- providersArray.push(` CredentialsProvider({
1062
- name: "Credentials",
1063
- credentials: {
1064
- email: { label: "Email", type: "email" },
1065
- password: { label: "Password", type: "password" },
1066
- },
1067
- async authorize(credentials) {
1068
- // TODO: Replace with your actual authentication logic
1069
- // This is a placeholder that accepts any email/password
1070
- // In production, you should:
1071
- // 1. Validate credentials against your database
1072
- // 2. Hash and compare passwords
1073
- // 3. Return user object or null
1074
-
1075
- if (!credentials?.email || !credentials?.password) {
1076
- return null;
1077
- }
1078
-
1079
- // Example: Check against hardcoded credentials (REMOVE IN PRODUCTION)
1080
- // Replace this with your database lookup
1081
- if (
1082
- credentials.email === "demo@example.com" &&
1083
- credentials.password === "demo123"
1084
- ) {
1085
- return {
1086
- id: "1",
1087
- email: credentials.email,
1088
- name: "Demo User",
1089
- };
1090
- }
1091
-
1092
- // If credentials don't match, return null
1093
- return null;
1094
- },
1095
- })`);
1096
- }
1097
-
1098
- if (selectedProviders.includes("google")) {
1099
- providersArray.push(` GoogleProvider({
1100
- clientId: process.env.GOOGLE_CLIENT_ID!,
1101
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
1102
- })`);
1103
- }
1104
-
1105
- if (selectedProviders.includes("github")) {
1106
- providersArray.push(` GitHubProvider({
1107
- clientId: process.env.GITHUB_CLIENT_ID!,
1108
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
1109
- })`);
1110
- }
1111
-
1112
- const providerAuthContent = `${imports.join("\n")}
1113
-
1114
- export const authOptions: NextAuthOptions = {
1115
- providers: [
1116
- ${providersArray.join(",\n")}
1117
- ],
1118
- pages: {
1119
- signIn: "/auth/login",
1120
- error: "/auth/error",
1121
- },
1122
- session: {
1123
- strategy: "jwt",
1124
- },
1125
- callbacks: {
1126
- async jwt({ token, user, account }) {
1127
- if (user) {
1128
- token.id = user.id;
1129
- token.email = user.email;
1130
- token.name = user.name;
1131
- }
1132
- if (account) {
1133
- token.accessToken = account.access_token;
1134
- token.provider = account.provider;
1135
- }
1136
- return token;
1137
- },
1138
- async session({ session, token }) {
1139
- if (session.user) {
1140
- session.user.id = token.id as string;
1141
- session.accessToken = token.accessToken as string;
1142
- session.provider = token.provider as string;
1143
- }
1144
- return session;
1145
- },
1146
- },
1147
- };
1148
-
1149
- const handler = NextAuth(authOptions);
1150
-
1151
- export { handler as GET, handler as POST };
1152
- `;
1153
-
1154
- fs.writeFileSync(routePath, providerAuthContent, "utf-8");
1155
- }
1156
-
1157
- // Update login and signup pages with OAuth buttons
1158
- async function updateAuthPagesWithProviders(target: string) {
1159
- const appDir = detectAppDirectory(target);
1160
- const loginPagePath = path.join(target, appDir, "auth/login/page.tsx");
1161
- const signupPagePath = path.join(target, appDir, "auth/signup/page.tsx");
1162
-
1163
- // Update login page
1164
- if (fs.existsSync(loginPagePath)) {
1165
- const loginPageContent = `"use client";
1166
-
1167
- import { signIn } from "next-auth/react";
1168
- import { useRouter } from "next/navigation";
1169
- import toast from "react-hot-toast";
1170
-
1171
- export default function LoginPage() {
1172
- const router = useRouter();
1173
-
1174
- const handleOAuthSignIn = async (provider: "google" | "github") => {
1175
- try {
1176
- await signIn(provider, { callbackUrl: "/" });
1177
- } catch (error) {
1178
- toast.error(\`Failed to sign in with \${provider}\`);
1179
- }
1180
- };
1181
-
1182
- return (
1183
- <div className="flex min-h-screen items-center justify-center bg-zinc-50 px-4 py-12 dark:bg-black sm:px-6 lg:px-8">
1184
- <div className="w-full max-w-md space-y-8">
1185
- <div>
1186
- <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50">
1187
- Sign in to your account
1188
- </h2>
1189
- <p className="mt-2 text-center text-sm text-zinc-600 dark:text-zinc-400">
1190
- Or{" "}
1191
- <a
1192
- href="/auth/signup"
1193
- className="font-medium text-zinc-950 hover:text-zinc-700 dark:text-zinc-50 dark:hover:text-zinc-300"
1194
- >
1195
- create a new account
1196
- </a>
1197
- </p>
1198
- </div>
1199
- <div className="mt-8 space-y-4">
1200
- <button
1201
- onClick={() => handleOAuthSignIn("google")}
1202
- className="group relative flex w-full justify-center items-center gap-3 rounded-md bg-white px-3 py-3 text-sm font-semibold text-zinc-900 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:ring-zinc-700 dark:hover:bg-zinc-700"
1203
- >
1204
- <svg className="h-5 w-5" viewBox="0 0 24 24">
1205
- <path
1206
- fill="currentColor"
1207
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
1208
- />
1209
- <path
1210
- fill="currentColor"
1211
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
1212
- />
1213
- <path
1214
- fill="currentColor"
1215
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
1216
- />
1217
- <path
1218
- fill="currentColor"
1219
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
1220
- />
1221
- </svg>
1222
- Continue with Google
1223
- </button>
1224
- <button
1225
- onClick={() => handleOAuthSignIn("github")}
1226
- className="group relative flex w-full justify-center items-center gap-3 rounded-md bg-zinc-900 px-3 py-3 text-sm font-semibold text-white hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1227
- >
1228
- <svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
1229
- <path
1230
- fillRule="evenodd"
1231
- d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482C19.138 20.197 22 16.425 22 12.017 22 6.484 17.522 2 12 2z"
1232
- clipRule="evenodd"
1233
- />
1234
- </svg>
1235
- Continue with GitHub
1236
- </button>
1237
- </div>
1238
- </div>
1239
- </div>
1240
- );
1241
- }
1242
- `;
1243
- fs.writeFileSync(loginPagePath, loginPageContent, "utf-8");
1244
- }
1245
-
1246
- // Update signup page
1247
- if (fs.existsSync(signupPagePath)) {
1248
- const signupPageContent = `"use client";
1249
-
1250
- import { signIn } from "next-auth/react";
1251
- import { useRouter } from "next/navigation";
1252
- import toast from "react-hot-toast";
1253
-
1254
- export default function SignupPage() {
1255
- const router = useRouter();
1256
-
1257
- const handleOAuthSignIn = async (provider: "google" | "github") => {
1258
- try {
1259
- await signIn(provider, { callbackUrl: "/" });
1260
- } catch (error) {
1261
- toast.error(\`Failed to sign in with \${provider}\`);
1262
- }
1263
- };
1264
-
1265
- return (
1266
- <div className="flex min-h-screen items-center justify-center bg-zinc-50 px-4 py-12 dark:bg-black sm:px-6 lg:px-8">
1267
- <div className="w-full max-w-md space-y-8">
1268
- <div>
1269
- <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50">
1270
- Create your account
1271
- </h2>
1272
- <p className="mt-2 text-center text-sm text-zinc-600 dark:text-zinc-400">
1273
- Or{" "}
1274
- <a
1275
- href="/auth/login"
1276
- className="font-medium text-zinc-950 hover:text-zinc-700 dark:text-zinc-50 dark:hover:text-zinc-300"
1277
- >
1278
- sign in to your existing account
1279
- </a>
1280
- </p>
1281
- </div>
1282
- <div className="mt-8 space-y-4">
1283
- <button
1284
- onClick={() => handleOAuthSignIn("google")}
1285
- className="group relative flex w-full justify-center items-center gap-3 rounded-md bg-white px-3 py-3 text-sm font-semibold text-zinc-900 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:ring-zinc-700 dark:hover:bg-zinc-700"
1286
- >
1287
- <svg className="h-5 w-5" viewBox="0 0 24 24">
1288
- <path
1289
- fill="currentColor"
1290
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
1291
- />
1292
- <path
1293
- fill="currentColor"
1294
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
1295
- />
1296
- <path
1297
- fill="currentColor"
1298
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
1299
- />
1300
- <path
1301
- fill="currentColor"
1302
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
1303
- />
1304
- </svg>
1305
- Continue with Google
1306
- </button>
1307
- <button
1308
- onClick={() => handleOAuthSignIn("github")}
1309
- className="group relative flex w-full justify-center items-center gap-3 rounded-md bg-zinc-900 px-3 py-3 text-sm font-semibold text-white hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1310
- >
1311
- <svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
1312
- <path
1313
- fillRule="evenodd"
1314
- d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482C19.138 20.197 22 16.425 22 12.017 22 6.484 17.522 2 12 2z"
1315
- clipRule="evenodd"
1316
- />
1317
- </svg>
1318
- Continue with GitHub
1319
- </button>
1320
- </div>
1321
- </div>
1322
- </div>
1323
- );
1324
- }
1325
- `;
1326
- fs.writeFileSync(signupPagePath, signupPageContent, "utf-8");
1327
- }
1328
- }
1329
-
1330
- // Copy protected route files
1331
- async function copyProtectedRouteFiles(target: string) {
1332
- const protectedRouteSrc = path.join(BOILERPLATE_ROOT, "auth/components/protected-route.tsx");
1333
- const middlewareSrc = path.join(BOILERPLATE_ROOT, "auth/middleware.ts");
1334
-
1335
- const componentsDir = detectComponentsDirectory(target);
1336
- const componentsDirPath = path.join(target, componentsDir);
1337
- const protectedRouteDest = path.join(componentsDirPath, "protected-route.tsx");
1338
- const middlewareDest = path.join(target, "middleware.ts");
1339
-
1340
- // Ensure components directory exists
1341
- if (!fs.existsSync(componentsDirPath)) {
1342
- fs.mkdirSync(componentsDirPath, { recursive: true });
1343
- }
1344
-
1345
- // Copy protected route component
1346
- if (fs.existsSync(protectedRouteSrc)) {
1347
- fs.copyFileSync(protectedRouteSrc, protectedRouteDest);
1348
- }
1349
-
1350
- // Copy middleware (only if it doesn't exist)
1351
- if (fs.existsSync(middlewareSrc) && !fs.existsSync(middlewareDest)) {
1352
- fs.copyFileSync(middlewareSrc, middlewareDest);
1353
- }
1354
-
1355
- // Copy auth navbar component (demo/example - won't overwrite existing navbar)
1356
- const authNavbarSrc = path.join(BOILERPLATE_ROOT, "auth/components/auth-navbar.tsx");
1357
- const authNavbarDest = path.join(componentsDirPath, "auth-navbar.tsx");
1358
- if (fs.existsSync(authNavbarSrc)) {
1359
- // Only copy if it doesn't exist (won't overwrite)
1360
- if (!fs.existsSync(authNavbarDest)) {
1361
- fs.copyFileSync(authNavbarSrc, authNavbarDest);
1362
- }
1363
- }
1364
-
1365
- // Copy example pages (only if they don't exist)
1366
- const appDir = detectAppDirectory(target);
1367
- const dashboardPageSrc = path.join(BOILERPLATE_ROOT, "auth/app/dashboard/page.tsx");
1368
- const landingPageSrc = path.join(BOILERPLATE_ROOT, "auth/app/page.tsx");
1369
- const dashboardPageDest = path.join(target, appDir, "dashboard/page.tsx");
1370
- const landingPageDest = path.join(target, appDir, "page.tsx");
1371
-
1372
- // Create dashboard directory if needed
1373
- const dashboardDir = path.join(target, appDir, "dashboard");
1374
- if (!fs.existsSync(dashboardDir)) {
1375
- fs.mkdirSync(dashboardDir, { recursive: true });
1376
- }
1377
-
1378
- // Copy dashboard page (only if it doesn't exist)
1379
- if (fs.existsSync(dashboardPageSrc) && !fs.existsSync(dashboardPageDest)) {
1380
- fs.copyFileSync(dashboardPageSrc, dashboardPageDest);
1381
- }
1382
-
1383
- // Copy landing page (only if it doesn't exist or is default)
1384
- if (fs.existsSync(landingPageSrc)) {
1385
- // Check if current page is just a default Next.js page
1386
- if (fs.existsSync(landingPageDest)) {
1387
- const currentContent = fs.readFileSync(landingPageDest, "utf-8");
1388
- // Only replace if it's the default Next.js page
1389
- if (currentContent.includes("Get started by editing") || currentContent.length < 500) {
1390
- fs.copyFileSync(landingPageSrc, landingPageDest);
1391
- }
1392
- } else {
1393
- fs.copyFileSync(landingPageSrc, landingPageDest);
1394
- }
1395
- }
1396
- }
1397
-
1398
- // Update auth button with OAuth providers
1399
- async function updateAuthButtonWithProviders(target: string, selectedProviders: string[] = ["google", "github", "credentials"]) {
1400
- const componentsDir = detectComponentsDirectory(target);
1401
- const authButtonPath = path.join(target, componentsDir, "auth-button.tsx");
1402
-
1403
- if (fs.existsSync(authButtonPath)) {
1404
- // Build OAuth buttons based on selected providers
1405
- const oauthButtons: string[] = [];
1406
-
1407
- if (selectedProviders.includes("google")) {
1408
- oauthButtons.push(` <button
1409
- onClick={() => signIn("google")}
1410
- className="flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-zinc-900 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 dark:bg-zinc-800 dark:text-zinc-50 dark:ring-zinc-700 dark:hover:bg-zinc-700"
1411
- >
1412
- <svg className="h-4 w-4" viewBox="0 0 24 24">
1413
- <path
1414
- fill="currentColor"
1415
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
1416
- />
1417
- <path
1418
- fill="currentColor"
1419
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
1420
- />
1421
- <path
1422
- fill="currentColor"
1423
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
1424
- />
1425
- <path
1426
- fill="currentColor"
1427
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
1428
- />
1429
- </svg>
1430
- Google
1431
- </button>`);
1432
- }
1433
-
1434
- if (selectedProviders.includes("github")) {
1435
- oauthButtons.push(` <button
1436
- onClick={() => signIn("github")}
1437
- className="flex items-center gap-2 rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1438
- >
1439
- <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
1440
- <path
1441
- fillRule="evenodd"
1442
- d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482C19.138 20.197 22 16.425 22 12.017 22 6.484 17.522 2 12 2z"
1443
- clipRule="evenodd"
1444
- />
1445
- </svg>
1446
- GitHub
1447
- </button>`);
1448
- }
1449
-
1450
- const authButtonContent = `"use client";
1451
-
1452
- import { signIn, signOut, useSession } from "next-auth/react";
1453
-
1454
- export function AuthButton() {
1455
- const { data: session, status } = useSession();
1456
-
1457
- if (status === "loading") {
1458
- return (
1459
- <button
1460
- disabled
1461
- className="rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
1462
- >
1463
- Loading...
1464
- </button>
1465
- );
1466
- }
1467
-
1468
- if (session) {
1469
- return (
1470
- <div className="flex items-center gap-4">
1471
- <span className="text-sm text-zinc-600 dark:text-zinc-400">
1472
- {session.user?.email}
1473
- </span>
1474
- <button
1475
- onClick={() => signOut()}
1476
- className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
1477
- >
1478
- Sign out
1479
- </button>
1480
- </div>
1481
- );
1482
- }
1483
-
1484
- return (
1485
- <div className="flex items-center gap-2">
1486
- ${oauthButtons.join("\n")}
1487
- </div>
1488
- );
1489
- }
1490
- `;
1491
- fs.writeFileSync(authButtonPath, authButtonContent, "utf-8");
1492
- }
1493
- }
1494
-
1495
- // Generate rainbow gradient color for a character
1496
- function getRainbowColor(char: string, index: number, total: number): string {
1497
- const colors = [
1498
- chalk.magentaBright,
1499
- chalk.redBright,
1500
- chalk.yellowBright,
1501
- chalk.greenBright,
1502
- chalk.cyanBright,
1503
- chalk.blueBright,
1504
- chalk.magentaBright,
1505
- ];
1506
- const colorIndex = Math.floor((index / total) * colors.length);
1507
- return colors[colorIndex].bold(char);
1508
- }
1509
-
1510
- // Convert PNG to colored ASCII art (unused but kept for future use)
1511
- async function _pngToAscii(imagePath: string, width: number = 80): Promise<string[]> {
1512
- try {
1513
- const image = await Jimp.read(imagePath);
1514
-
1515
- // Resize image to fit terminal width
1516
- const aspectRatio = image.getHeight() / image.getWidth();
1517
- const height = Math.floor(width * aspectRatio * 0.5); // 0.5 for character aspect ratio
1518
- image.resize(width, height);
1519
-
1520
- const asciiLines: string[] = [];
1521
- const chars = [" ", "ā–‘", "ā–’", "ā–“", "ā–ˆ"]; // ASCII characters from light to dark
1522
-
1523
- for (let y = 0; y < image.getHeight(); y++) {
1524
- let line = " "; // Add some left padding
1525
- for (let x = 0; x < image.getWidth(); x++) {
1526
- const color = Jimp.intToRGBA(image.getPixelColor(x, y));
1527
- const brightness = (color.r + color.g + color.b) / 3;
1528
- const charIndex = Math.floor((brightness / 255) * (chars.length - 1));
1529
- const char = chars[charIndex];
1530
-
1531
- // Apply color from image
1532
- const rgb = chalk.rgb(color.r, color.g, color.b);
1533
- line += rgb(char);
1534
- }
1535
- asciiLines.push(line);
1536
- }
1537
-
1538
- return asciiLines;
1539
- } catch (error) {
1540
- console.error(chalk.red("Error loading PNG image:"), error);
1541
- return [];
1542
- }
1543
- }
1544
-
1545
- // Show welcome screen with ASCII art
1546
- async function showWelcome() {
1547
- showLogo();
1548
- }
1549
-
1550
- // Create a new project from template
1551
- async function createProject(projectName: string, showWelcomeScreen: boolean = true, forceOverwrite: boolean = false) {
1552
- const templatePath = path.join(BOILERPLATE_ROOT, "template");
1553
- const targetPath = path.resolve(process.cwd(), projectName);
1554
-
1555
- if (fs.existsSync(targetPath)) {
1556
- if (!forceOverwrite) {
1557
- console.log(chalk.yellow(`āš ļø Directory "${projectName}" already exists!`));
1558
- const { overwrite } = await inquirer.prompt([
1559
- {
1560
- type: "confirm",
1561
- name: "overwrite",
1562
- message: chalk.white("Do you want to overwrite it? (This will delete existing files)"),
1563
- default: false,
1564
- },
1565
- ]);
1566
-
1567
- if (!overwrite) {
1568
- console.log(chalk.gray("Cancelled. Choose a different name."));
1569
- process.exit(0);
1570
- }
1571
- }
1572
-
1573
- // Remove existing directory if overwriting
1574
- console.log(chalk.yellow(`Removing existing directory "${projectName}"...`));
1575
- fs.rmSync(targetPath, { recursive: true, force: true });
1576
- }
1577
-
1578
- if (showWelcomeScreen) {
1579
- showLogo();
1580
- }
1581
- console.log(chalk.blue.bold(`šŸš€ Creating new StackPatch project: ${chalk.white(projectName)}\n`));
1582
-
1583
- const tracker = new ProgressTracker();
1584
- tracker.addStep("Copying project template");
1585
- tracker.addStep("Processing project files");
1586
- tracker.addStep("Installing dependencies");
1587
-
1588
- // Step 1: Copy template
1589
- tracker.startStep(0);
1590
- await fse.copy(templatePath, targetPath);
1591
- tracker.completeStep(0);
1592
-
1593
- // Step 2: Replace placeholders in files
1594
- tracker.startStep(1);
1595
- // Detect app directory for template processing
1596
- const appDir = detectAppDirectory(targetPath);
1597
- const filesToProcess = [
1598
- "package.json",
1599
- `${appDir}/layout.tsx`,
1600
- `${appDir}/page.tsx`,
1601
- "README.md",
1602
- ];
1603
-
1604
- for (const file of filesToProcess) {
1605
- const filePath = path.join(targetPath, file);
1606
- if (fs.existsSync(filePath)) {
1607
- let content = fs.readFileSync(filePath, "utf-8");
1608
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
1609
- fs.writeFileSync(filePath, content, "utf-8");
1610
- }
1611
- }
1612
- tracker.completeStep(1);
1613
-
1614
- // Step 3: Install dependencies
1615
- tracker.startStep(2);
1616
- const installResult = spawnSync("pnpm", ["install"], {
1617
- cwd: targetPath,
1618
- stdio: "pipe",
1619
- });
1620
-
1621
- if (installResult.status !== 0) {
1622
- tracker.failStep(2);
1623
- console.log(chalk.yellow("\nāš ļø Dependency installation had issues. You can run 'pnpm install' manually."));
1624
- } else {
1625
- tracker.completeStep(2);
1626
- }
1627
-
1628
- console.log(chalk.green(`\nāœ… Project "${projectName}" created successfully!`));
1629
-
1630
- // Automatically add auth-ui after creating the project
1631
- console.log(chalk.blue.bold(`\nšŸ” Adding authentication to your project...\n`));
1632
-
1633
- const authSrc = path.join(BOILERPLATE_ROOT, PATCHES["auth-ui"].path);
1634
- const authCopyResult = await copyFiles(authSrc, targetPath);
1635
-
1636
- if (authCopyResult.success) {
1637
- const addedFiles = authCopyResult.addedFiles;
1638
- const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
1639
- // Install auth dependencies (only if missing)
1640
- installDependencies(targetPath, PATCHES["auth-ui"].dependencies);
1641
-
1642
- // Ask which OAuth providers to configure
1643
- const selectedProviders = await askOAuthProviders();
1644
-
1645
- // Setup authentication with selected providers
1646
- const success = await setupAuth(targetPath, selectedProviders);
1647
-
1648
- if (success) {
1649
- await withSpinner("Updating layout with AuthSessionProvider", () => {
1650
- const result = updateLayoutForAuth(targetPath);
1651
- if (result.modified && result.originalContent) {
1652
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
1653
- }
1654
- return true;
1655
- });
1656
-
1657
- await withSpinner("Adding Toaster component", () => {
1658
- const result = updateLayoutForToaster(targetPath);
1659
- if (result.modified && result.originalContent) {
1660
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
1661
- }
1662
- return true;
1663
- });
1664
-
1665
- await withSpinner("Setting up protected routes", () => {
1666
- copyProtectedRouteFiles(targetPath);
1667
- return true;
1668
- });
1669
-
1670
- // Show OAuth setup instructions
1671
- await showOAuthSetupInstructions(targetPath, selectedProviders);
1672
-
1673
- // Create manifest
1674
- const manifest: StackPatchManifest = {
1675
- version: MANIFEST_VERSION,
1676
- patchName: "auth-ui",
1677
- target: targetPath,
1678
- timestamp: new Date().toISOString(),
1679
- files: {
1680
- added: addedFiles,
1681
- modified: modifiedFiles,
1682
- backedUp: [],
1683
- },
1684
- dependencies: PATCHES["auth-ui"].dependencies,
1685
- oauthProviders: selectedProviders,
1686
- };
1687
- writeManifest(targetPath, manifest);
1688
-
1689
- console.log(chalk.green("\nāœ… Authentication added successfully!"));
1690
- } else {
1691
- console.log(chalk.yellow("\nāš ļø Authentication setup had some issues. You can run 'npx stackpatch add auth-ui' manually."));
1692
- }
1693
- } else {
1694
- console.log(chalk.yellow("\nāš ļø Could not add authentication. You can run 'npx stackpatch add auth-ui' manually."));
1695
- }
1696
-
1697
- console.log(chalk.blue("\nšŸ“¦ Next steps:"));
1698
- console.log(chalk.white(` ${chalk.cyan("cd")} ${chalk.yellow(projectName)}`));
1699
- console.log(chalk.white(` ${chalk.cyan("pnpm")} ${chalk.yellow("dev")}`));
1700
- console.log(chalk.white(` Test authentication at: ${chalk.cyan("http://localhost:3000/auth/login")}`));
1701
- console.log(chalk.gray("\nšŸ“š See README.md for OAuth setup and protected routes\n"));
1702
- }
1703
-
1704
- // ---------------- Main CLI ----------------
1705
- async function main() {
1706
- const args = process.argv.slice(2);
1707
- const command = args[0];
1708
- const projectName = args[1];
1709
- const skipPrompts = args.includes("--yes") || args.includes("-y");
1710
-
1711
- // Show logo on startup
1712
- showLogo();
1713
-
1714
- // Handle: bun create stackpatch@latest (no project name)
1715
- // Show welcome and prompt for project name
1716
- if (!command || command.startsWith("-")) {
1717
- const { name } = await inquirer.prompt([
1718
- {
1719
- type: "input",
1720
- name: "name",
1721
- message: chalk.white("Enter your project name or path (relative to current directory)"),
1722
- default: "my-stackpatch-app",
1723
- validate: (input: string) => {
1724
- if (!input.trim()) {
1725
- return "Project name cannot be empty";
1726
- }
1727
- return true;
1728
- },
1729
- },
1730
- ]);
1731
- await createProject(name.trim(), false, skipPrompts); // Don't show welcome again
1732
- return;
1733
- }
1734
-
1735
- // Handle: npx stackpatch revert
1736
- if (command === "revert") {
1737
- let target = process.cwd();
1738
-
1739
- // Auto-detect target directory
1740
- const hasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
1741
- const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
1742
-
1743
- if (!hasAppDir && !hasPagesDir) {
1744
- const parent = path.resolve(target, "..");
1745
- if (
1746
- fs.existsSync(path.join(parent, "app")) ||
1747
- fs.existsSync(path.join(parent, "src", "app")) ||
1748
- fs.existsSync(path.join(parent, "pages")) ||
1749
- fs.existsSync(path.join(parent, "src", "pages"))
1750
- ) {
1751
- target = parent;
1752
- }
1753
- }
1754
-
1755
- const manifest = readManifest(target);
1756
- if (!manifest) {
1757
- console.log(chalk.red("āŒ No StackPatch installation found to revert."));
1758
- console.log(chalk.yellow(" Make sure you're in the correct directory where you ran 'stackpatch add'."));
1759
- process.exit(1);
1760
- }
1761
-
1762
- console.log(chalk.blue.bold("\nšŸ”„ Reverting StackPatch installation\n"));
1763
- console.log(chalk.white(` Patch: ${chalk.cyan(manifest.patchName)}`));
1764
- console.log(chalk.white(` Installed: ${chalk.gray(new Date(manifest.timestamp).toLocaleString())}\n`));
1765
-
1766
- // Show what will be reverted
1767
- console.log(chalk.white(" Files to remove: ") + chalk.cyan(`${manifest.files.added.length}`));
1768
- console.log(chalk.white(" Files to restore: ") + chalk.cyan(`${manifest.files.modified.length}`));
1769
- if (manifest.dependencies.length > 0) {
1770
- console.log(chalk.white(" Dependencies to remove: ") + chalk.cyan(`${manifest.dependencies.join(", ")}`));
1771
- }
1772
- console.log();
1773
-
1774
- const { confirm } = await inquirer.prompt([
1775
- {
1776
- type: "confirm",
1777
- name: "confirm",
1778
- message: "Are you sure you want to revert this installation? This will remove all added files, restore modified files, and remove dependencies.",
1779
- default: false,
1780
- },
1781
- ]);
1782
-
1783
- if (!confirm) {
1784
- console.log(chalk.yellow("\n← Revert cancelled"));
1785
- return;
1786
- }
1787
-
1788
- console.log(chalk.blue("\nšŸ”„ Starting revert process...\n"));
1789
-
1790
- let removedCount = 0;
1791
- let restoredCount = 0;
1792
- let failedRemovals: string[] = [];
1793
- let failedRestorations: string[] = [];
1794
- const directoriesToCheck: Set<string> = new Set();
1795
-
1796
- // Step 1: Get list of valid StackPatch files from boilerplate
1797
- // This ensures we only remove files that are actually from StackPatch
1798
- const boilerplatePath = path.join(BOILERPLATE_ROOT, manifest.patchName === "auth-ui" ? "auth" : manifest.patchName);
1799
- const validStackPatchFiles = new Set<string>();
1800
-
1801
- function collectBoilerplateFiles(srcDir: string, baseDir: string, targetBase: string): void {
1802
- if (!fs.existsSync(srcDir)) return;
1803
-
1804
- const files = fs.readdirSync(srcDir, { withFileTypes: true });
1805
- for (const file of files) {
1806
- const srcFilePath = path.join(srcDir, file.name);
1807
- if (file.isDirectory()) {
1808
- collectBoilerplateFiles(srcFilePath, baseDir, targetBase);
1809
- } else {
1810
- const relativePath = path.relative(baseDir, srcFilePath);
1811
- const targetPath = targetBase
1812
- ? path.join(targetBase, relativePath).replace(/\\/g, "/")
1813
- : relativePath.replace(/\\/g, "/");
1814
- validStackPatchFiles.add(targetPath);
1815
- }
1816
- }
1817
- }
1818
-
1819
- // Collect files from boilerplate app directory
1820
- const appDir = detectAppDirectory(target);
1821
- const componentsDir = detectComponentsDirectory(target);
1822
- const boilerplateAppPath = path.join(boilerplatePath, "app");
1823
- const boilerplateComponentsPath = path.join(boilerplatePath, "components");
1824
-
1825
- if (fs.existsSync(boilerplateAppPath)) {
1826
- collectBoilerplateFiles(boilerplateAppPath, boilerplateAppPath, appDir);
1827
- }
1828
- if (fs.existsSync(boilerplateComponentsPath)) {
1829
- collectBoilerplateFiles(boilerplateComponentsPath, boilerplateComponentsPath, componentsDir);
1830
- }
1831
-
1832
- // Collect root-level files
1833
- if (fs.existsSync(boilerplatePath)) {
1834
- const entries = fs.readdirSync(boilerplatePath, { withFileTypes: true });
1835
- for (const entry of entries) {
1836
- if (entry.name !== "app" && entry.name !== "components") {
1837
- const srcPath = path.join(boilerplatePath, entry.name);
1838
- if (entry.isDirectory()) {
1839
- collectBoilerplateFiles(srcPath, srcPath, "");
1840
- } else {
1841
- validStackPatchFiles.add(entry.name);
1842
- }
1843
- }
1844
- }
1845
- }
1846
-
1847
- // Step 1: Remove added files (only if they're actually from StackPatch boilerplate)
1848
- console.log(chalk.white("šŸ“ Removing added files..."));
1849
- for (const filePath of manifest.files.added) {
1850
- // Only remove if this file is actually in the boilerplate
1851
- if (!validStackPatchFiles.has(filePath)) {
1852
- console.log(chalk.gray(` ⊘ Skipped (not in boilerplate): ${filePath}`));
1853
- continue;
1854
- }
1855
-
1856
- const fullPath = path.join(target, filePath);
1857
- if (fs.existsSync(fullPath)) {
1858
- try {
1859
- fs.unlinkSync(fullPath);
1860
- console.log(chalk.green(` āœ“ Removed: ${filePath}`));
1861
- removedCount++;
1862
-
1863
- // Track parent directories for cleanup
1864
- const parentDirs = getParentDirectories(fullPath, target);
1865
- parentDirs.forEach(dir => directoriesToCheck.add(dir));
1866
- } catch (error) {
1867
- console.log(chalk.yellow(` ⚠ Could not remove: ${filePath}`));
1868
- failedRemovals.push(filePath);
1869
- }
1870
- } else {
1871
- console.log(chalk.gray(` ⊘ Already removed: ${filePath}`));
1872
- }
1873
- }
1874
-
1875
- // Step 2: Remove .env.local and .env.example if they were created by StackPatch
1876
- console.log(chalk.white("\nšŸ” Removing environment files..."));
1877
- const envFilesToRemove = manifest.files.envFiles || [];
1878
-
1879
- // Fallback: check for common env files if not tracked in manifest (for older manifests)
1880
- if (envFilesToRemove.length === 0) {
1881
- const commonEnvFiles = [".env.local", ".env.example"];
1882
- for (const envFile of commonEnvFiles) {
1883
- const envPath = path.join(target, envFile);
1884
- if (fs.existsSync(envPath)) {
1885
- try {
1886
- // Check if this file was created by StackPatch (contains NEXTAUTH_SECRET)
1887
- const content = fs.readFileSync(envPath, "utf-8");
1888
- if (content.includes("NEXTAUTH_SECRET") || content.includes("NEXTAUTH_URL")) {
1889
- envFilesToRemove.push(envFile);
1890
- }
1891
- } catch {
1892
- // Ignore errors
1893
- }
1894
- }
1895
- }
1896
- }
1897
-
1898
- for (const envFile of envFilesToRemove) {
1899
- const envPath = path.join(target, envFile);
1900
- if (fs.existsSync(envPath)) {
1901
- try {
1902
- fs.unlinkSync(envPath);
1903
- console.log(chalk.green(` āœ“ Removed: ${envFile}`));
1904
- removedCount++;
1905
- } catch (error) {
1906
- console.log(chalk.yellow(` ⚠ Could not remove: ${envFile}`));
1907
- failedRemovals.push(envFile);
1908
- }
1909
- } else {
1910
- console.log(chalk.gray(` ⊘ Already removed: ${envFile}`));
1911
- }
1912
- }
1913
-
1914
- // Step 3: Restore modified files from originalContent in manifest
1915
- // This is more reliable than backups since it contains the true original content
1916
- console.log(chalk.white("\nšŸ“ Restoring modified files..."));
1917
- for (const modified of manifest.files.modified) {
1918
- const originalPath = path.join(target, modified.path);
1919
-
1920
- if (modified.originalContent !== undefined) {
1921
- try {
1922
- // Restore from originalContent in manifest (most reliable)
1923
- const originalDir = path.dirname(originalPath);
1924
- if (!fs.existsSync(originalDir)) {
1925
- fs.mkdirSync(originalDir, { recursive: true });
1926
- }
1927
- fs.writeFileSync(originalPath, modified.originalContent, "utf-8");
1928
- console.log(chalk.green(` āœ“ Restored: ${modified.path}`));
1929
- restoredCount++;
1930
- } catch (error) {
1931
- // Fallback to backup file if originalContent restore fails
1932
- const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
1933
- if (fs.existsSync(backupPath)) {
1934
- try {
1935
- restoreFile(backupPath, originalPath);
1936
- console.log(chalk.green(` āœ“ Restored (from backup): ${modified.path}`));
1937
- restoredCount++;
1938
- } catch (backupError) {
1939
- console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path}`));
1940
- failedRestorations.push(modified.path);
1941
- }
1942
- } else {
1943
- console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path} (no backup found)`));
1944
- failedRestorations.push(modified.path);
1945
- }
1946
- }
1947
- } else {
1948
- // Fallback: try to restore from backup file
1949
- const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
1950
- if (fs.existsSync(backupPath)) {
1951
- try {
1952
- restoreFile(backupPath, originalPath);
1953
- console.log(chalk.green(` āœ“ Restored (from backup): ${modified.path}`));
1954
- restoredCount++;
1955
- } catch (error) {
1956
- console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path}`));
1957
- failedRestorations.push(modified.path);
1958
- }
1959
- } else {
1960
- console.log(chalk.yellow(` ⚠ Backup not found and no originalContent: ${modified.path}`));
1961
- failedRestorations.push(modified.path);
1962
- }
1963
- }
1964
-
1965
- // Safety check: If file still contains StackPatch components after restore, manually remove them
1966
- if (fs.existsSync(originalPath) && modified.path.includes("layout.tsx")) {
1967
- try {
1968
- let content = fs.readFileSync(originalPath, "utf-8");
1969
- let needsUpdate = false;
1970
-
1971
- // Remove AuthSessionProvider import
1972
- if (content.includes("AuthSessionProvider") && content.includes("session-provider")) {
1973
- content = content.replace(/import\s*{\s*AuthSessionProvider\s*}\s*from\s*["'][^"']*session-provider[^"']*["'];\s*\n?/g, "");
1974
- needsUpdate = true;
1975
- }
1976
-
1977
- // Remove Toaster import
1978
- if (content.includes("Toaster") && content.includes("toaster")) {
1979
- content = content.replace(/import\s*{\s*Toaster\s*}\s*from\s*["'][^"']*toaster[^"']*["'];\s*\n?/g, "");
1980
- needsUpdate = true;
1981
- }
1982
-
1983
- // Remove AuthSessionProvider wrapper
1984
- if (content.includes("<AuthSessionProvider>") && content.includes("</AuthSessionProvider>")) {
1985
- content = content.replace(/<AuthSessionProvider>\s*/g, "");
1986
- content = content.replace(/\s*<\/AuthSessionProvider>/g, "");
1987
- needsUpdate = true;
1988
- }
1989
-
1990
- // Remove Toaster component
1991
- if (content.includes("<Toaster")) {
1992
- content = content.replace(/<Toaster\s*\/?>\s*\n?\s*/g, "");
1993
- needsUpdate = true;
1994
- }
1995
-
1996
- if (needsUpdate) {
1997
- fs.writeFileSync(originalPath, content, "utf-8");
1998
- console.log(chalk.green(` āœ“ Cleaned up StackPatch components from: ${modified.path}`));
1999
- }
2000
- } catch (error) {
2001
- // Ignore errors in cleanup
2002
- }
2003
- }
2004
- }
2005
-
2006
- // Step 4: Remove dependencies from package.json
2007
- if (manifest.dependencies.length > 0) {
2008
- console.log(chalk.white("\nšŸ“¦ Removing dependencies from package.json..."));
2009
- const removed = removeDependencies(target, manifest.dependencies);
2010
- if (removed) {
2011
- console.log(chalk.green(` āœ“ Removed dependencies: ${manifest.dependencies.join(", ")}`));
2012
- console.log(chalk.yellow(" ⚠ Run 'pnpm install' to update node_modules"));
2013
- } else {
2014
- console.log(chalk.gray(" ⊘ Dependencies not found in package.json"));
2015
- }
2016
- }
2017
-
2018
- // Step 5: Clean up empty directories (only if they only contained StackPatch files)
2019
- console.log(chalk.white("\n🧹 Cleaning up empty directories..."));
2020
- const sortedDirs = Array.from(directoriesToCheck).sort((a, b) => b.length - a.length); // Sort by depth (deepest first)
2021
- let removedDirCount = 0;
2022
-
2023
- for (const dir of sortedDirs) {
2024
- if (fs.existsSync(dir)) {
2025
- try {
2026
- const entries = fs.readdirSync(dir);
2027
- if (entries.length === 0) {
2028
- // Only remove if directory is empty
2029
- // We know it was created by StackPatch because we're tracking it
2030
- fs.rmdirSync(dir);
2031
- removedDirCount++;
2032
- console.log(chalk.green(` āœ“ Removed empty directory: ${path.relative(target, dir)}`));
2033
- }
2034
- // If directory has other files, we don't remove it (silently skip)
2035
- } catch {
2036
- // Ignore errors
2037
- }
2038
- }
2039
- }
2040
-
2041
- if (removedDirCount === 0) {
2042
- console.log(chalk.gray(" ⊘ No empty directories to remove"));
2043
- }
2044
-
2045
- // Step 6: Remove manifest and backups
2046
- console.log(chalk.white("\nšŸ—‘ļø Removing StackPatch tracking files..."));
2047
- const stackpatchDir = path.join(target, ".stackpatch");
2048
- if (fs.existsSync(stackpatchDir)) {
2049
- try {
2050
- fs.rmSync(stackpatchDir, { recursive: true, force: true });
2051
- console.log(chalk.green(" āœ“ Removed .stackpatch directory"));
2052
- } catch (error) {
2053
- console.log(chalk.yellow(" ⚠ Could not remove .stackpatch directory"));
2054
- }
2055
- }
2056
-
2057
- // Step 7: Verification
2058
- console.log(chalk.white("\nāœ… Verification..."));
2059
- const remainingManifest = readManifest(target);
2060
- if (remainingManifest) {
2061
- console.log(chalk.red(" āŒ Warning: Manifest still exists. Revert may be incomplete."));
2062
- } else {
2063
- console.log(chalk.green(" āœ“ Manifest removed successfully"));
2064
- }
2065
-
2066
- // Summary
2067
- console.log(chalk.blue.bold("\nšŸ“Š Revert Summary:"));
2068
- console.log(chalk.white(` Files removed: ${chalk.green(removedCount)}`));
2069
- console.log(chalk.white(` Files restored: ${chalk.green(restoredCount)}`));
2070
- if (failedRemovals.length > 0) {
2071
- console.log(chalk.yellow(` Failed removals: ${failedRemovals.length}`));
2072
- failedRemovals.forEach(file => console.log(chalk.gray(` - ${file}`)));
2073
- }
2074
- if (failedRestorations.length > 0) {
2075
- console.log(chalk.yellow(` Failed restorations: ${failedRestorations.length}`));
2076
- failedRestorations.forEach(file => console.log(chalk.gray(` - ${file}`)));
2077
- }
2078
-
2079
- if (failedRemovals.length === 0 && failedRestorations.length === 0 && !remainingManifest) {
2080
- console.log(chalk.green("\nāœ… Revert complete! Your project has been fully restored to its original state."));
2081
- if (manifest.dependencies.length > 0) {
2082
- console.log(chalk.yellow("\nāš ļø Remember to run 'pnpm install' to update your node_modules."));
2083
- }
2084
- } else {
2085
- console.log(chalk.yellow("\nāš ļø Revert completed with some warnings. Please review the output above."));
2086
- }
2087
-
2088
- return;
2089
- }
2090
-
2091
- // Handle: bun create stackpatch@latest my-app
2092
- // When bun runs create, it passes project name as first arg (not "create")
2093
- // Check if first arg looks like a project name (not a known command)
2094
- // Always ask for project name first, even if provided
2095
- if (command && !["add", "create", "revert"].includes(command) && !PATCHES[command] && !command.startsWith("-")) {
2096
- // Likely called as: bun create stackpatch@latest my-app
2097
- // But we'll ask for project name anyway to be consistent
2098
- await showWelcome();
2099
- const { name } = await inquirer.prompt([
2100
- {
2101
- type: "input",
2102
- name: "name",
2103
- message: chalk.white("Enter your project name or path (relative to current directory)"),
2104
- default: command || "my-stackpatch-app", // Use provided name as default
2105
- validate: (input: string) => {
2106
- if (!input.trim()) {
2107
- return "Project name cannot be empty";
2108
- }
2109
- return true;
2110
- },
2111
- },
2112
- ]);
2113
- await createProject(name.trim(), false, skipPrompts); // Welcome already shown
2114
- return;
2115
- }
2116
-
2117
- // Handle: npx stackpatch create my-app
2118
- if (command === "create") {
2119
- if (!projectName) {
2120
- showWelcome();
2121
- const { name } = await inquirer.prompt([
2122
- {
2123
- type: "input",
2124
- name: "name",
2125
- message: chalk.white("Enter your project name or path (relative to current directory)"),
2126
- default: "my-stackpatch-app",
2127
- validate: (input: string) => {
2128
- if (!input.trim()) {
2129
- return "Project name cannot be empty";
2130
- }
2131
- return true;
2132
- },
2133
- },
2134
- ]);
2135
- await createProject(name.trim(), false); // Logo already shown
2136
- return;
2137
- }
2138
- await createProject(projectName, false, skipPrompts); // Logo already shown
2139
- return;
2140
- }
2141
-
2142
- // Handle: npx stackpatch add auth-ui
2143
- const patchName = args[1];
2144
- if (command === "add" && patchName) {
2145
- if (!PATCHES[patchName]) {
2146
- console.log(chalk.red(`āŒ Unknown patch: ${patchName}`));
2147
- console.log(chalk.yellow(`Available patches: ${Object.keys(PATCHES).join(", ")}`));
2148
- process.exit(1);
2149
- }
2150
-
2151
- // Auto-detect target directory (current working directory or common locations)
2152
- let target = process.cwd();
2153
-
2154
- // Check if we're in a Next.js app (has app/, src/app/, pages/, or src/pages/ directory)
2155
- const hasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
2156
- const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
2157
-
2158
- if (!hasAppDir && !hasPagesDir) {
2159
- // Try parent directory
2160
- const parent = path.resolve(target, "..");
2161
- if (
2162
- fs.existsSync(path.join(parent, "app")) ||
2163
- fs.existsSync(path.join(parent, "src", "app")) ||
2164
- fs.existsSync(path.join(parent, "pages")) ||
2165
- fs.existsSync(path.join(parent, "src", "pages"))
2166
- ) {
2167
- target = parent;
2168
- } else {
2169
- // Try common monorepo locations: apps/, packages/, or root
2170
- const possiblePaths = [
2171
- path.join(target, "apps"),
2172
- path.join(parent, "apps"),
2173
- path.join(target, "packages"),
2174
- path.join(parent, "packages"),
2175
- ];
2176
-
2177
- let foundApp = false;
2178
- for (const possiblePath of possiblePaths) {
2179
- if (fs.existsSync(possiblePath)) {
2180
- // Look for Next.js apps in this directory
2181
- const entries = fs.readdirSync(possiblePath, { withFileTypes: true });
2182
- for (const entry of entries) {
2183
- if (entry.isDirectory()) {
2184
- const appPath = path.join(possiblePath, entry.name);
2185
- if (
2186
- fs.existsSync(path.join(appPath, "app")) ||
2187
- fs.existsSync(path.join(appPath, "src", "app")) ||
2188
- fs.existsSync(path.join(appPath, "pages")) ||
2189
- fs.existsSync(path.join(appPath, "src", "pages"))
2190
- ) {
2191
- target = appPath;
2192
- foundApp = true;
2193
- break;
2194
- }
2195
- }
2196
- }
2197
- if (foundApp) break;
2198
- }
2199
- }
2200
-
2201
- if (!foundApp) {
2202
- console.log(chalk.yellow("āš ļø Could not auto-detect Next.js app directory."));
2203
- const { target: userTarget } = await inquirer.prompt([
2204
- {
2205
- type: "input",
2206
- name: "target",
2207
- message: "Enter the path to your Next.js app folder:",
2208
- default: target,
2209
- },
2210
- ]);
2211
- target = path.resolve(userTarget);
2212
- }
2213
- }
2214
- }
2215
-
2216
- const src = path.join(BOILERPLATE_ROOT, PATCHES[patchName].path);
2217
-
2218
- console.log(chalk.blue.bold("\nšŸš€ StackPatch CLI\n"));
2219
- console.log(chalk.blue(`Copying ${patchName} patch to ${target}...\n`));
2220
-
2221
- const copyResult = await copyFiles(src, target);
2222
- if (!copyResult.success) process.exit(1);
2223
-
2224
- const addedFiles = copyResult.addedFiles;
2225
- const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
2226
- let selectedProviders: string[] = [];
2227
-
2228
- // Install dependencies (only if missing)
2229
- installDependencies(target, PATCHES[patchName].dependencies);
2230
-
2231
- // For auth patches, ask for OAuth providers and setup
2232
- if (patchName === "auth" || patchName === "auth-ui") {
2233
- showLogo();
2234
- console.log(chalk.blue.bold(`\nšŸ” Setting up authentication\n`));
2235
-
2236
- // Ask which OAuth providers to configure
2237
- selectedProviders = await askOAuthProviders();
2238
-
2239
- const success = await setupAuth(target, selectedProviders);
2240
-
2241
- if (success) {
2242
- await withSpinner("Updating layout with AuthSessionProvider", () => {
2243
- const result = updateLayoutForAuth(target);
2244
- if (result.modified && result.originalContent) {
2245
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
2246
- }
2247
- return true;
2248
- });
2249
-
2250
- await withSpinner("Adding Toaster component", () => {
2251
- const result = updateLayoutForToaster(target);
2252
- if (result.modified && result.originalContent) {
2253
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
2254
- }
2255
- return true;
2256
- });
2257
-
2258
- await withSpinner("Setting up protected routes", () => {
2259
- copyProtectedRouteFiles(target);
2260
- return true;
2261
- });
2262
-
2263
- // OAuth instructions are shown in setupAuth function
2264
- }
2265
- }
2266
-
2267
- // Create manifest for tracking
2268
- const manifest: StackPatchManifest = {
2269
- version: MANIFEST_VERSION,
2270
- patchName,
2271
- target,
2272
- timestamp: new Date().toISOString(),
2273
- files: {
2274
- added: addedFiles,
2275
- modified: modifiedFiles,
2276
- backedUp: [],
2277
- },
2278
- dependencies: PATCHES[patchName].dependencies,
2279
- oauthProviders: selectedProviders,
2280
- };
2281
- writeManifest(target, manifest);
2282
-
2283
- // Final next steps
2284
- console.log(chalk.blue("\nšŸŽ‰ Patch setup complete!"));
2285
- console.log(chalk.green("\nšŸ“ Next Steps:"));
2286
- console.log(chalk.white(" 1. Configure OAuth providers (see instructions above)"));
2287
- console.log(chalk.white(" 2. Set up database for email/password auth (see comments in code)"));
2288
- console.log(chalk.white(" 3. Check out the auth navbar demo in ") + chalk.cyan("components/auth-navbar.tsx"));
2289
- console.log(chalk.white(" 4. Protect your routes (see README.md)"));
2290
- console.log(chalk.white(" 5. Run your Next.js dev server: ") + chalk.cyan("pnpm dev"));
2291
- console.log(chalk.white(" 6. Test authentication at: ") + chalk.cyan("http://localhost:3000/auth/login\n"));
2292
-
2293
- console.log(chalk.blue.bold("šŸ“š Documentation:"));
2294
- console.log(chalk.white(" - See ") + chalk.cyan("README.md") + chalk.white(" for complete setup guide\n"));
2295
-
2296
- console.log(chalk.yellow("āš ļø Important:"));
2297
- console.log(chalk.white(" - Email/password auth is in DEMO mode"));
2298
- console.log(chalk.white(" - Demo credentials: ") + chalk.gray("demo@example.com / demo123"));
2299
- console.log(chalk.white(" - See code comments in ") + chalk.cyan("app/api/auth/[...nextauth]/route.ts") + chalk.white(" to implement real auth\n"));
2300
- return;
2301
- }
2302
-
2303
- // If no command, show help or interactive mode
2304
- if (!command) {
2305
- await showWelcome();
2306
- console.log(chalk.yellow("Usage:"));
2307
- console.log(chalk.white(" ") + chalk.cyan("npm create stackpatch@latest") + chalk.gray(" [project-name]"));
2308
- console.log(chalk.white(" ") + chalk.cyan("npx create-stackpatch@latest") + chalk.gray(" [project-name]"));
2309
- console.log(chalk.white(" ") + chalk.cyan("bunx create-stackpatch@latest") + chalk.gray(" [project-name]"));
2310
- console.log(chalk.white(" ") + chalk.cyan("npx stackpatch create") + chalk.gray(" [project-name]"));
2311
- console.log(chalk.white(" ") + chalk.cyan("npx stackpatch add") + chalk.white(" <patch-name>"));
2312
- console.log(chalk.white(" ") + chalk.cyan("npx stackpatch revert") + chalk.gray(" - Revert a patch installation"));
2313
- console.log(chalk.white("\nExamples:"));
2314
- console.log(chalk.gray(" npm create stackpatch@latest my-app"));
2315
- console.log(chalk.gray(" npx create-stackpatch@latest my-app"));
2316
- console.log(chalk.gray(" bunx create-stackpatch@latest my-app"));
2317
- console.log(chalk.gray(" npx stackpatch create my-app"));
2318
- console.log(chalk.gray(" npx stackpatch add auth-ui"));
2319
- console.log(chalk.gray("\n"));
2320
- process.exit(0);
2321
- }
2322
-
2323
- // Interactive mode (fallback)
2324
- console.log(chalk.blue.bold("\nšŸš€ Welcome to StackPatch CLI\n"));
2325
-
2326
- let selectedPatch: string | null = null;
2327
- let goBack = false;
2328
-
2329
- // 1ļøāƒ£ Select patch with back option
2330
- do {
2331
- const response = await inquirer.prompt([
2332
- {
2333
- type: "list",
2334
- name: "patch",
2335
- message: "Which patch do you want to add?",
2336
- choices: [
2337
- ...Object.keys(PATCHES)
2338
- .filter(p => p !== "auth-ui") // Don't show duplicate
2339
- .map(p => ({ name: p, value: p })),
2340
- new inquirer.Separator(),
2341
- {
2342
- name: chalk.gray("← Go back / Cancel"),
2343
- value: "back",
2344
- },
2345
- ],
2346
- },
2347
- ]);
2348
-
2349
- if (response.patch === "back") {
2350
- goBack = true;
2351
- break;
2352
- }
2353
-
2354
- selectedPatch = response.patch;
2355
- } while (!selectedPatch);
2356
-
2357
- if (goBack || !selectedPatch) {
2358
- console.log(chalk.yellow("\n← Cancelled"));
2359
- return;
2360
- }
2361
-
2362
- const patch = selectedPatch;
2363
-
2364
- // 2ļøāƒ£ Enter target Next.js app folder
2365
- const { target } = await inquirer.prompt([
2366
- {
2367
- type: "input",
2368
- name: "target",
2369
- message:
2370
- "Enter the relative path to your Next.js app folder (e.g., ../../apps/stackpatch-frontend):",
2371
- default: process.cwd(),
2372
- },
2373
- ]);
2374
-
2375
- const src = path.join(BOILERPLATE_ROOT, PATCHES[patch].path);
2376
- const dest = path.resolve(target);
2377
-
2378
- console.log(chalk.blue(`\nCopying ${patch} boilerplate to ${dest}...\n`));
2379
-
2380
- const copyResult = await copyFiles(src, dest);
2381
- if (!copyResult.success) return;
2382
-
2383
- const addedFiles = copyResult.addedFiles;
2384
- const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
2385
- let selectedProviders: string[] = [];
2386
-
2387
- // 3ļøāƒ£ Install dependencies (only if missing)
2388
- installDependencies(dest, PATCHES[patch].dependencies);
2389
-
2390
- // 4ļøāƒ£ For auth patches, ask for OAuth providers and setup
2391
- if (patch === "auth" || patch === "auth-ui") {
2392
- console.log(chalk.blue.bold(`\nšŸ” Setting up authentication\n`));
2393
-
2394
- // Ask which OAuth providers to configure
2395
- const selectedProviders = await askOAuthProviders();
2396
-
2397
- const success = await setupAuth(dest, selectedProviders);
2398
-
2399
- if (success) {
2400
- await withSpinner("Updating layout with AuthSessionProvider", () => {
2401
- const result = updateLayoutForAuth(dest);
2402
- if (result.modified && result.originalContent) {
2403
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
2404
- }
2405
- return true;
2406
- });
2407
-
2408
- await withSpinner("Adding Toaster component", () => {
2409
- const result = updateLayoutForToaster(dest);
2410
- if (result.modified && result.originalContent) {
2411
- modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
2412
- }
2413
- return true;
2414
- });
2415
- }
2416
-
2417
- // Create manifest
2418
- const manifest: StackPatchManifest = {
2419
- version: MANIFEST_VERSION,
2420
- patchName: patch,
2421
- target: dest,
2422
- timestamp: new Date().toISOString(),
2423
- files: {
2424
- added: addedFiles,
2425
- modified: modifiedFiles,
2426
- backedUp: [],
2427
- },
2428
- dependencies: PATCHES[patch].dependencies,
2429
- oauthProviders: selectedProviders,
2430
- };
2431
- writeManifest(dest, manifest);
2432
- }
2433
-
2434
- // 5ļøāƒ£ Final next steps
2435
- console.log(chalk.blue("\nšŸŽ‰ Patch setup complete!"));
2436
- console.log(chalk.green("- Run your Next.js dev server: pnpm dev"));
2437
- console.log(chalk.green("- Start building your features!\n"));
2438
- }
2439
-
2440
- main().catch((error) => {
2441
- console.error(chalk.red("āŒ Error:"), error);
2442
- process.exit(1);
2443
- });