stackpatch 1.1.4 → 1.1.6
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/README.md +76 -69
- package/bin/stackpatch.js +79 -0
- package/bin/stackpatch.ts +2445 -3
- package/boilerplate/auth/app/api/auth/[...nextauth]/route.ts +124 -0
- package/boilerplate/auth/app/api/auth/signup/route.ts +45 -0
- package/boilerplate/auth/app/auth/login/page.tsx +24 -50
- package/boilerplate/auth/app/auth/signup/page.tsx +56 -69
- package/boilerplate/auth/app/dashboard/page.tsx +82 -0
- package/boilerplate/auth/app/login/page.tsx +136 -0
- package/boilerplate/auth/app/page.tsx +48 -0
- package/boilerplate/auth/components/auth-button.tsx +43 -0
- package/boilerplate/auth/components/auth-navbar.tsx +118 -0
- package/boilerplate/auth/components/protected-route.tsx +74 -0
- package/boilerplate/auth/components/session-provider.tsx +11 -0
- package/boilerplate/auth/middleware.ts +51 -0
- package/package.json +5 -6
- package/boilerplate/auth/app/stackpatch/page.tsx +0 -269
- package/boilerplate/auth/components/auth-wrapper.tsx +0 -61
- package/src/auth/generator.ts +0 -569
- package/src/auth/index.ts +0 -372
- package/src/auth/setup.ts +0 -293
- package/src/commands/add.ts +0 -112
- package/src/commands/create.ts +0 -128
- package/src/commands/revert.ts +0 -389
- package/src/config.ts +0 -52
- package/src/fileOps/copy.ts +0 -224
- package/src/fileOps/layout.ts +0 -304
- package/src/fileOps/protected.ts +0 -67
- package/src/index.ts +0 -215
- package/src/manifest.ts +0 -87
- package/src/ui/logo.ts +0 -24
- package/src/ui/progress.ts +0 -82
- package/src/utils/dependencies.ts +0 -114
- package/src/utils/deps-check.ts +0 -45
- package/src/utils/files.ts +0 -58
- package/src/utils/paths.ts +0 -217
- package/src/utils/scanner.ts +0 -109
package/src/commands/add.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import inquirer from "inquirer";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import { BOILERPLATE_ROOT, PATCHES, MANIFEST_VERSION, type StackPatchManifest } from "../config.js";
|
|
6
|
-
import { detectTargetDirectory } from "../utils/paths.js";
|
|
7
|
-
import { installDependencies } from "../utils/dependencies.js";
|
|
8
|
-
import { withSpinner } from "../ui/progress.js";
|
|
9
|
-
import { showLogo } from "../ui/logo.js";
|
|
10
|
-
import { copyFiles } from "../fileOps/copy.js";
|
|
11
|
-
import { writeManifest } from "../manifest.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Add a patch to an existing project
|
|
15
|
-
*/
|
|
16
|
-
export async function addPatch(patchName: string, targetDir?: string): Promise<void> {
|
|
17
|
-
if (!PATCHES[patchName]) {
|
|
18
|
-
console.log(chalk.red(`❌ Unknown patch: ${patchName}`));
|
|
19
|
-
console.log(chalk.yellow(`Available patches: ${Object.keys(PATCHES).join(", ")}`));
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Auto-detect target directory
|
|
24
|
-
let target = targetDir ? path.resolve(targetDir) : detectTargetDirectory();
|
|
25
|
-
|
|
26
|
-
// If still can't find, ask user
|
|
27
|
-
const hasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
|
|
28
|
-
const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
|
|
29
|
-
|
|
30
|
-
if (!hasAppDir && !hasPagesDir) {
|
|
31
|
-
console.log(chalk.yellow("⚠️ Could not auto-detect Next.js app directory."));
|
|
32
|
-
const { userTarget } = await inquirer.prompt([
|
|
33
|
-
{
|
|
34
|
-
type: "input",
|
|
35
|
-
name: "userTarget",
|
|
36
|
-
message: "Enter the path to your Next.js app folder:",
|
|
37
|
-
default: target,
|
|
38
|
-
},
|
|
39
|
-
]);
|
|
40
|
-
target = path.resolve(userTarget);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// For auth patches, use new setup flow
|
|
44
|
-
if (patchName === "auth" || patchName === "auth-ui") {
|
|
45
|
-
showLogo();
|
|
46
|
-
|
|
47
|
-
// Use new setup flow
|
|
48
|
-
const { setupAuthNew } = await import("../auth/index.js");
|
|
49
|
-
const success = await setupAuthNew(target);
|
|
50
|
-
|
|
51
|
-
if (!success) {
|
|
52
|
-
console.log(chalk.yellow("\n⚠️ Auth setup was cancelled or failed."));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Manifest is already created in setupAuthNew
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// For other patches, use old flow
|
|
61
|
-
const src = path.join(BOILERPLATE_ROOT, PATCHES[patchName].path);
|
|
62
|
-
|
|
63
|
-
console.log(chalk.blue.bold("\n🚀 StackPatch CLI\n"));
|
|
64
|
-
console.log(chalk.blue(`Copying ${patchName} patch to ${target}...\n`));
|
|
65
|
-
|
|
66
|
-
const copyResult = await copyFiles(src, target);
|
|
67
|
-
if (!copyResult.success) process.exit(1);
|
|
68
|
-
|
|
69
|
-
const addedFiles = copyResult.addedFiles;
|
|
70
|
-
const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
|
|
71
|
-
|
|
72
|
-
// Install dependencies (only if missing)
|
|
73
|
-
installDependencies(target, PATCHES[patchName].dependencies);
|
|
74
|
-
|
|
75
|
-
// Create manifest for tracking
|
|
76
|
-
const manifest: StackPatchManifest = {
|
|
77
|
-
version: MANIFEST_VERSION,
|
|
78
|
-
patchName,
|
|
79
|
-
target,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
files: {
|
|
82
|
-
added: addedFiles,
|
|
83
|
-
modified: modifiedFiles,
|
|
84
|
-
backedUp: [],
|
|
85
|
-
},
|
|
86
|
-
dependencies: PATCHES[patchName].dependencies,
|
|
87
|
-
oauthProviders: [],
|
|
88
|
-
};
|
|
89
|
-
writeManifest(target, manifest);
|
|
90
|
-
|
|
91
|
-
// Final next steps
|
|
92
|
-
console.log(chalk.blue("\n🎉 Patch setup complete!"));
|
|
93
|
-
console.log(chalk.green("\n📝 Next Steps:"));
|
|
94
|
-
console.log(chalk.white(" 1. Configure OAuth providers (see instructions above)"));
|
|
95
|
-
console.log(chalk.white(" 2. Set up database for email/password auth (see comments in code)"));
|
|
96
|
-
console.log(chalk.white(" 3. Visit the landing page at ") + chalk.cyan("/stackpatch"));
|
|
97
|
-
console.log(chalk.white(" 4. Protect your routes (see README.md)"));
|
|
98
|
-
console.log(chalk.white(" 5. Run your Next.js dev server: ") + chalk.cyan("pnpm dev"));
|
|
99
|
-
console.log(chalk.white(" 6. Test authentication at: ") + chalk.cyan("http://localhost:3000/auth/login\n"));
|
|
100
|
-
|
|
101
|
-
console.log(chalk.blue.bold("📚 Documentation:"));
|
|
102
|
-
console.log(chalk.white(" - See ") + chalk.cyan("README.md") + chalk.white(" for complete setup guide\n"));
|
|
103
|
-
|
|
104
|
-
console.log(chalk.yellow("⚠️ Important:"));
|
|
105
|
-
console.log(chalk.white(" - Email/password auth is in DEMO mode"));
|
|
106
|
-
console.log(chalk.white(" - Demo credentials: ") + chalk.gray("demo@example.com / demo123"));
|
|
107
|
-
console.log(
|
|
108
|
-
chalk.white(" - See code comments in ") +
|
|
109
|
-
chalk.cyan("https://better-auth.com") +
|
|
110
|
-
chalk.white(" for implementation details\n")
|
|
111
|
-
);
|
|
112
|
-
}
|
package/src/commands/create.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fse from "fs-extra";
|
|
4
|
-
import inquirer from "inquirer";
|
|
5
|
-
import chalk from "chalk";
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
|
-
import { BOILERPLATE_ROOT, PATCHES, MANIFEST_VERSION, type StackPatchManifest } from "../config.js";
|
|
8
|
-
import { detectAppDirectory } from "../utils/paths.js";
|
|
9
|
-
import { installDependencies } from "../utils/dependencies.js";
|
|
10
|
-
import { ProgressTracker, withSpinner } from "../ui/progress.js";
|
|
11
|
-
import { showLogo } from "../ui/logo.js";
|
|
12
|
-
import { writeManifest } from "../manifest.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Create a new project from template
|
|
16
|
-
*/
|
|
17
|
-
export async function createProject(
|
|
18
|
-
projectName: string,
|
|
19
|
-
showWelcomeScreen: boolean = true,
|
|
20
|
-
forceOverwrite: boolean = false
|
|
21
|
-
): Promise<void> {
|
|
22
|
-
const templatePath = path.join(BOILERPLATE_ROOT, "template");
|
|
23
|
-
const targetPath = path.resolve(process.cwd(), projectName);
|
|
24
|
-
|
|
25
|
-
if (fs.existsSync(targetPath)) {
|
|
26
|
-
if (!forceOverwrite) {
|
|
27
|
-
console.log(chalk.yellow(`⚠️ Directory "${projectName}" already exists!`));
|
|
28
|
-
const { overwrite } = await inquirer.prompt([
|
|
29
|
-
{
|
|
30
|
-
type: "list",
|
|
31
|
-
name: "overwrite",
|
|
32
|
-
message: chalk.white("Do you want to overwrite it? (This will delete existing files)"),
|
|
33
|
-
choices: [
|
|
34
|
-
{ name: "Yes, overwrite", value: "yes" },
|
|
35
|
-
{ name: "No, cancel", value: "no" },
|
|
36
|
-
],
|
|
37
|
-
default: "no",
|
|
38
|
-
},
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
if (overwrite !== "yes") {
|
|
42
|
-
console.log(chalk.gray("Cancelled. Choose a different name."));
|
|
43
|
-
process.exit(0);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Remove existing directory if overwriting
|
|
48
|
-
console.log(chalk.yellow(`Removing existing directory "${projectName}"...`));
|
|
49
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (showWelcomeScreen) {
|
|
53
|
-
showLogo();
|
|
54
|
-
}
|
|
55
|
-
console.log(chalk.blue.bold(`🚀 Creating new StackPatch project: ${chalk.white(projectName)}\n`));
|
|
56
|
-
|
|
57
|
-
const tracker = new ProgressTracker();
|
|
58
|
-
tracker.addStep("Copying project template");
|
|
59
|
-
tracker.addStep("Processing project files");
|
|
60
|
-
tracker.addStep("Installing dependencies");
|
|
61
|
-
|
|
62
|
-
// Step 1: Copy template
|
|
63
|
-
tracker.startStep(0);
|
|
64
|
-
await fse.copy(templatePath, targetPath);
|
|
65
|
-
tracker.completeStep(0);
|
|
66
|
-
|
|
67
|
-
// Step 2: Replace placeholders in files
|
|
68
|
-
tracker.startStep(1);
|
|
69
|
-
// Detect app directory for template processing
|
|
70
|
-
const appDir = detectAppDirectory(targetPath);
|
|
71
|
-
const filesToProcess = [
|
|
72
|
-
"package.json",
|
|
73
|
-
`${appDir}/layout.tsx`,
|
|
74
|
-
`${appDir}/page.tsx`,
|
|
75
|
-
"README.md",
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
for (const file of filesToProcess) {
|
|
79
|
-
const filePath = path.join(targetPath, file);
|
|
80
|
-
if (fs.existsSync(filePath)) {
|
|
81
|
-
let content = fs.readFileSync(filePath, "utf-8");
|
|
82
|
-
content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
83
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
tracker.completeStep(1);
|
|
87
|
-
|
|
88
|
-
// Step 3: Install dependencies
|
|
89
|
-
tracker.startStep(2);
|
|
90
|
-
const installResult = spawnSync("pnpm", ["install"], {
|
|
91
|
-
cwd: targetPath,
|
|
92
|
-
stdio: "pipe",
|
|
93
|
-
env: {
|
|
94
|
-
...process.env,
|
|
95
|
-
// Prevent Git credential prompts
|
|
96
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
97
|
-
GIT_ASKPASS: "",
|
|
98
|
-
// Prevent npm/pnpm credential prompts
|
|
99
|
-
NPM_CONFIG_PROGRESS: "false",
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (installResult.status !== 0) {
|
|
104
|
-
tracker.failStep(2);
|
|
105
|
-
console.log(chalk.yellow("\n⚠️ Dependency installation had issues. You can run 'pnpm install' manually."));
|
|
106
|
-
} else {
|
|
107
|
-
tracker.completeStep(2);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log(chalk.green(`\n✅ Project "${projectName}" created successfully!`));
|
|
111
|
-
|
|
112
|
-
// Automatically add auth-ui after creating the project
|
|
113
|
-
console.log(chalk.blue.bold(`\n🔐 Adding authentication to your project...\n`));
|
|
114
|
-
|
|
115
|
-
// Use new setup flow
|
|
116
|
-
const { setupAuthNew } = await import("../auth/index.js");
|
|
117
|
-
const success = await setupAuthNew(targetPath);
|
|
118
|
-
|
|
119
|
-
if (!success) {
|
|
120
|
-
console.log(chalk.yellow("\n⚠️ Authentication setup was cancelled. You can run 'npx stackpatch add auth' manually."));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
console.log(chalk.blue("\n📦 Next steps:"));
|
|
124
|
-
console.log(chalk.white(` ${chalk.cyan("cd")} ${chalk.yellow(projectName)}`));
|
|
125
|
-
console.log(chalk.white(` ${chalk.cyan("pnpm")} ${chalk.yellow("dev")}`));
|
|
126
|
-
console.log(chalk.white(` Test authentication at: ${chalk.cyan("http://localhost:3000/auth/login")}`));
|
|
127
|
-
console.log(chalk.gray("\n📚 See README.md for OAuth setup and protected routes\n"));
|
|
128
|
-
}
|
package/src/commands/revert.ts
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import inquirer from "inquirer";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import { BOILERPLATE_ROOT } from "../config.js";
|
|
6
|
-
import { readManifest } from "../manifest.js";
|
|
7
|
-
import { restoreFile } from "../manifest.js";
|
|
8
|
-
import { detectAppDirectory, detectComponentsDirectory, getParentDirectories } from "../utils/paths.js";
|
|
9
|
-
import { removeDependencies } from "../utils/dependencies.js";
|
|
10
|
-
import { removeEmptyDirectories } from "../utils/files.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Revert a StackPatch installation
|
|
14
|
-
*/
|
|
15
|
-
export async function revertPatch(targetDir?: string): Promise<void> {
|
|
16
|
-
let target = targetDir ? path.resolve(targetDir) : process.cwd();
|
|
17
|
-
|
|
18
|
-
// Auto-detect target directory
|
|
19
|
-
const hasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
|
|
20
|
-
const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
|
|
21
|
-
|
|
22
|
-
if (!hasAppDir && !hasPagesDir) {
|
|
23
|
-
const parent = path.resolve(target, "..");
|
|
24
|
-
if (
|
|
25
|
-
fs.existsSync(path.join(parent, "app")) ||
|
|
26
|
-
fs.existsSync(path.join(parent, "src", "app")) ||
|
|
27
|
-
fs.existsSync(path.join(parent, "pages")) ||
|
|
28
|
-
fs.existsSync(path.join(parent, "src", "pages"))
|
|
29
|
-
) {
|
|
30
|
-
target = parent;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const manifest = readManifest(target);
|
|
35
|
-
if (!manifest) {
|
|
36
|
-
console.log(chalk.red("❌ No StackPatch installation found to revert."));
|
|
37
|
-
console.log(chalk.yellow(" Make sure you're in the correct directory where you ran 'stackpatch add'."));
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(chalk.blue.bold("\n🔄 Reverting StackPatch installation\n"));
|
|
42
|
-
console.log(chalk.white(` Patch: ${chalk.cyan(manifest.patchName)}`));
|
|
43
|
-
console.log(chalk.white(` Installed: ${chalk.gray(new Date(manifest.timestamp).toLocaleString())}\n`));
|
|
44
|
-
|
|
45
|
-
// Show what will be reverted
|
|
46
|
-
console.log(chalk.white(" Files to remove: ") + chalk.cyan(`${manifest.files.added.length}`));
|
|
47
|
-
console.log(chalk.white(" Files to restore: ") + chalk.cyan(`${manifest.files.modified.length}`));
|
|
48
|
-
if (manifest.dependencies.length > 0) {
|
|
49
|
-
console.log(chalk.white(" Dependencies to remove: ") + chalk.cyan(`${manifest.dependencies.join(", ")}`));
|
|
50
|
-
}
|
|
51
|
-
console.log();
|
|
52
|
-
|
|
53
|
-
const { confirm } = await inquirer.prompt([
|
|
54
|
-
{
|
|
55
|
-
type: "list",
|
|
56
|
-
name: "confirm",
|
|
57
|
-
message:
|
|
58
|
-
"Are you sure you want to revert this installation? This will remove all added files, restore modified files, and remove dependencies.",
|
|
59
|
-
choices: [
|
|
60
|
-
{ name: "Yes, revert the installation", value: "yes" },
|
|
61
|
-
{ name: "No, cancel", value: "no" },
|
|
62
|
-
],
|
|
63
|
-
default: "no",
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
if (confirm !== "yes") {
|
|
68
|
-
console.log(chalk.yellow("\n← Revert cancelled"));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log(chalk.blue("\n🔄 Starting revert process...\n"));
|
|
73
|
-
|
|
74
|
-
let removedCount = 0;
|
|
75
|
-
let restoredCount = 0;
|
|
76
|
-
let failedRemovals: string[] = [];
|
|
77
|
-
let failedRestorations: string[] = [];
|
|
78
|
-
const directoriesToCheck: Set<string> = new Set();
|
|
79
|
-
|
|
80
|
-
// Step 1: Get list of valid StackPatch files from boilerplate
|
|
81
|
-
// This ensures we only remove files that are actually from StackPatch
|
|
82
|
-
const boilerplatePath = path.join(
|
|
83
|
-
BOILERPLATE_ROOT,
|
|
84
|
-
manifest.patchName === "auth-ui" ? "auth" : manifest.patchName
|
|
85
|
-
);
|
|
86
|
-
const validStackPatchFiles = new Set<string>();
|
|
87
|
-
|
|
88
|
-
function collectBoilerplateFiles(srcDir: string, baseDir: string, targetBase: string): void {
|
|
89
|
-
if (!fs.existsSync(srcDir)) return;
|
|
90
|
-
|
|
91
|
-
const files = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
92
|
-
for (const file of files) {
|
|
93
|
-
const srcFilePath = path.join(srcDir, file.name);
|
|
94
|
-
if (file.isDirectory()) {
|
|
95
|
-
collectBoilerplateFiles(srcFilePath, baseDir, targetBase);
|
|
96
|
-
} else {
|
|
97
|
-
const relativePath = path.relative(baseDir, srcFilePath);
|
|
98
|
-
const targetPath = targetBase
|
|
99
|
-
? path.join(targetBase, relativePath).replace(/\\/g, "/")
|
|
100
|
-
: relativePath.replace(/\\/g, "/");
|
|
101
|
-
validStackPatchFiles.add(targetPath);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Collect files from boilerplate app directory
|
|
107
|
-
const appDir = detectAppDirectory(target);
|
|
108
|
-
const componentsDir = detectComponentsDirectory(target);
|
|
109
|
-
const boilerplateAppPath = path.join(boilerplatePath, "app");
|
|
110
|
-
const boilerplateComponentsPath = path.join(boilerplatePath, "components");
|
|
111
|
-
|
|
112
|
-
if (fs.existsSync(boilerplateAppPath)) {
|
|
113
|
-
collectBoilerplateFiles(boilerplateAppPath, boilerplateAppPath, appDir);
|
|
114
|
-
}
|
|
115
|
-
if (fs.existsSync(boilerplateComponentsPath)) {
|
|
116
|
-
collectBoilerplateFiles(boilerplateComponentsPath, boilerplateComponentsPath, componentsDir);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Collect root-level files
|
|
120
|
-
if (fs.existsSync(boilerplatePath)) {
|
|
121
|
-
const entries = fs.readdirSync(boilerplatePath, { withFileTypes: true });
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
if (entry.name !== "app" && entry.name !== "components") {
|
|
124
|
-
const srcPath = path.join(boilerplatePath, entry.name);
|
|
125
|
-
if (entry.isDirectory()) {
|
|
126
|
-
collectBoilerplateFiles(srcPath, srcPath, "");
|
|
127
|
-
} else {
|
|
128
|
-
validStackPatchFiles.add(entry.name);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Step 1: Remove added files (only if they're actually from StackPatch boilerplate)
|
|
135
|
-
console.log(chalk.white("📁 Removing added files..."));
|
|
136
|
-
const filesToRemove = new Set<string>(); // Track files we actually removed
|
|
137
|
-
|
|
138
|
-
for (const filePath of manifest.files.added) {
|
|
139
|
-
// Only remove if this file is actually in the boilerplate
|
|
140
|
-
if (!validStackPatchFiles.has(filePath)) {
|
|
141
|
-
console.log(chalk.gray(` ⊘ Skipped (not in boilerplate): ${filePath}`));
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const fullPath = path.join(target, filePath);
|
|
146
|
-
if (fs.existsSync(fullPath)) {
|
|
147
|
-
try {
|
|
148
|
-
// Double-check: Only remove if file content matches boilerplate (safety check)
|
|
149
|
-
const boilerplateFilePath = path.join(boilerplatePath, filePath.replace(appDir + "/", "app/").replace(componentsDir + "/", "components/"));
|
|
150
|
-
if (fs.existsSync(boilerplateFilePath)) {
|
|
151
|
-
fs.unlinkSync(fullPath);
|
|
152
|
-
console.log(chalk.green(` ✓ Removed: ${filePath}`));
|
|
153
|
-
removedCount++;
|
|
154
|
-
filesToRemove.add(filePath);
|
|
155
|
-
|
|
156
|
-
// Track parent directories for cleanup (only for files we actually removed)
|
|
157
|
-
const parentDirs = getParentDirectories(fullPath, target);
|
|
158
|
-
parentDirs.forEach((dir) => directoriesToCheck.add(dir));
|
|
159
|
-
} else {
|
|
160
|
-
console.log(chalk.yellow(` ⚠ Skipped (safety check failed): ${filePath}`));
|
|
161
|
-
}
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.log(chalk.yellow(` ⚠ Could not remove: ${filePath}`));
|
|
164
|
-
failedRemovals.push(filePath);
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
console.log(chalk.gray(` ⊘ Already removed: ${filePath}`));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Step 2: Remove .env.local and .env.example if they were created by StackPatch
|
|
172
|
-
console.log(chalk.white("\n🔐 Removing environment files..."));
|
|
173
|
-
const envFilesToRemove = manifest.files.envFiles || [];
|
|
174
|
-
|
|
175
|
-
// Fallback: check for common env files if not tracked in manifest (for older manifests)
|
|
176
|
-
if (envFilesToRemove.length === 0) {
|
|
177
|
-
const commonEnvFiles = [".env.local", ".env.example"];
|
|
178
|
-
for (const envFile of commonEnvFiles) {
|
|
179
|
-
const envPath = path.join(target, envFile);
|
|
180
|
-
if (fs.existsSync(envPath)) {
|
|
181
|
-
try {
|
|
182
|
-
// Check if this file was created by StackPatch (contains BETTER_AUTH_SECRET)
|
|
183
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
184
|
-
if (content.includes("BETTER_AUTH_SECRET") || content.includes("BETTER_AUTH_URL")) {
|
|
185
|
-
envFilesToRemove.push(envFile);
|
|
186
|
-
}
|
|
187
|
-
} catch {
|
|
188
|
-
// Ignore errors
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (const envFile of envFilesToRemove) {
|
|
195
|
-
const envPath = path.join(target, envFile);
|
|
196
|
-
if (fs.existsSync(envPath)) {
|
|
197
|
-
try {
|
|
198
|
-
fs.unlinkSync(envPath);
|
|
199
|
-
console.log(chalk.green(` ✓ Removed: ${envFile}`));
|
|
200
|
-
removedCount++;
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.log(chalk.yellow(` ⚠ Could not remove: ${envFile}`));
|
|
203
|
-
failedRemovals.push(envFile);
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
console.log(chalk.gray(` ⊘ Already removed: ${envFile}`));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Step 3: Restore modified files from originalContent in manifest
|
|
211
|
-
// The manifest contains the original content before StackPatch modifications
|
|
212
|
-
console.log(chalk.white("\n📝 Restoring modified files..."));
|
|
213
|
-
|
|
214
|
-
for (const modified of manifest.files.modified) {
|
|
215
|
-
const originalPath = path.join(target, modified.path);
|
|
216
|
-
|
|
217
|
-
if (!fs.existsSync(originalPath)) {
|
|
218
|
-
console.log(chalk.gray(` ⊘ File not found (may have been deleted): ${modified.path}`));
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (modified.originalContent !== undefined) {
|
|
223
|
-
try {
|
|
224
|
-
// Restore from originalContent in manifest (most reliable - this is the exact original content)
|
|
225
|
-
const originalDir = path.dirname(originalPath);
|
|
226
|
-
if (!fs.existsSync(originalDir)) {
|
|
227
|
-
fs.mkdirSync(originalDir, { recursive: true });
|
|
228
|
-
}
|
|
229
|
-
fs.writeFileSync(originalPath, modified.originalContent, "utf-8");
|
|
230
|
-
console.log(chalk.green(` ✓ Restored: ${modified.path}`));
|
|
231
|
-
restoredCount++;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
// Fallback to backup file if originalContent restore fails
|
|
234
|
-
const backupPath = path.join(
|
|
235
|
-
target,
|
|
236
|
-
".stackpatch",
|
|
237
|
-
"backups",
|
|
238
|
-
modified.path.replace(/\//g, "_").replace(/\\/g, "_")
|
|
239
|
-
);
|
|
240
|
-
if (fs.existsSync(backupPath)) {
|
|
241
|
-
try {
|
|
242
|
-
restoreFile(backupPath, originalPath);
|
|
243
|
-
console.log(chalk.green(` ✓ Restored (from backup): ${modified.path}`));
|
|
244
|
-
restoredCount++;
|
|
245
|
-
} catch (backupError) {
|
|
246
|
-
console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path}`));
|
|
247
|
-
failedRestorations.push(modified.path);
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path} (no backup found)`));
|
|
251
|
-
failedRestorations.push(modified.path);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
// Fallback: try to restore from backup file
|
|
256
|
-
const backupPath = path.join(
|
|
257
|
-
target,
|
|
258
|
-
".stackpatch",
|
|
259
|
-
"backups",
|
|
260
|
-
modified.path.replace(/\//g, "_").replace(/\\/g, "_")
|
|
261
|
-
);
|
|
262
|
-
if (fs.existsSync(backupPath)) {
|
|
263
|
-
try {
|
|
264
|
-
restoreFile(backupPath, originalPath);
|
|
265
|
-
console.log(chalk.green(` ✓ Restored (from backup): ${modified.path}`));
|
|
266
|
-
restoredCount++;
|
|
267
|
-
} catch (error) {
|
|
268
|
-
console.log(chalk.yellow(` ⚠ Could not restore: ${modified.path}`));
|
|
269
|
-
failedRestorations.push(modified.path);
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
console.log(chalk.yellow(` ⚠ Backup not found and no originalContent: ${modified.path}`));
|
|
273
|
-
failedRestorations.push(modified.path);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Step 4: Remove dependencies from package.json
|
|
279
|
-
if (manifest.dependencies.length > 0) {
|
|
280
|
-
console.log(chalk.white("\n📦 Removing dependencies from package.json..."));
|
|
281
|
-
const removed = removeDependencies(target, manifest.dependencies);
|
|
282
|
-
if (removed) {
|
|
283
|
-
console.log(chalk.green(` ✓ Removed dependencies: ${manifest.dependencies.join(", ")}`));
|
|
284
|
-
console.log(chalk.yellow(" ⚠ Run 'pnpm install' to update node_modules"));
|
|
285
|
-
} else {
|
|
286
|
-
console.log(chalk.gray(" ⊘ Dependencies not found in package.json"));
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Step 5: Clean up empty directories (only if they only contained StackPatch files)
|
|
291
|
-
console.log(chalk.white("\n🧹 Cleaning up empty directories..."));
|
|
292
|
-
const sortedDirs = Array.from(directoriesToCheck).sort((a, b) => b.length - a.length); // Sort by depth (deepest first)
|
|
293
|
-
let removedDirCount = 0;
|
|
294
|
-
|
|
295
|
-
// Only check directories that are known StackPatch directories
|
|
296
|
-
const stackPatchDirs = new Set<string>();
|
|
297
|
-
for (const filePath of filesToRemove) {
|
|
298
|
-
const dir = path.dirname(path.join(target, filePath));
|
|
299
|
-
let currentDir = dir;
|
|
300
|
-
while (currentDir !== target && currentDir.length > target.length) {
|
|
301
|
-
stackPatchDirs.add(currentDir);
|
|
302
|
-
currentDir = path.dirname(currentDir);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const dir of sortedDirs) {
|
|
307
|
-
// Only remove directories that are in our StackPatch directories set
|
|
308
|
-
if (!stackPatchDirs.has(dir)) {
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (fs.existsSync(dir)) {
|
|
313
|
-
try {
|
|
314
|
-
const entries = fs.readdirSync(dir);
|
|
315
|
-
if (entries.length === 0) {
|
|
316
|
-
// Only remove if directory is empty AND it's a known StackPatch directory
|
|
317
|
-
fs.rmdirSync(dir);
|
|
318
|
-
removedDirCount++;
|
|
319
|
-
console.log(chalk.green(` ✓ Removed empty directory: ${path.relative(target, dir)}`));
|
|
320
|
-
} else {
|
|
321
|
-
// Directory has other files - check if they're all StackPatch files
|
|
322
|
-
const allStackPatchFiles = entries.every((entry) => {
|
|
323
|
-
const entryPath = path.join(dir, entry);
|
|
324
|
-
const relativePath = path.relative(target, entryPath).replace(/\\/g, "/");
|
|
325
|
-
return filesToRemove.has(relativePath) || validStackPatchFiles.has(relativePath);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (allStackPatchFiles && entries.length > 0) {
|
|
329
|
-
// All files are StackPatch files, but directory isn't empty - this shouldn't happen
|
|
330
|
-
// Skip removal to be safe
|
|
331
|
-
console.log(chalk.gray(` ⊘ Skipped (has other files): ${path.relative(target, dir)}`));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// If directory has other files, we don't remove it (silently skip)
|
|
335
|
-
} catch {
|
|
336
|
-
// Ignore errors
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (removedDirCount === 0) {
|
|
342
|
-
console.log(chalk.gray(" ⊘ No empty directories to remove"));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Step 6: Remove manifest and backups
|
|
346
|
-
console.log(chalk.white("\n🗑️ Removing StackPatch tracking files..."));
|
|
347
|
-
const stackpatchDir = path.join(target, ".stackpatch");
|
|
348
|
-
if (fs.existsSync(stackpatchDir)) {
|
|
349
|
-
try {
|
|
350
|
-
fs.rmSync(stackpatchDir, { recursive: true, force: true });
|
|
351
|
-
console.log(chalk.green(" ✓ Removed .stackpatch directory"));
|
|
352
|
-
} catch (error) {
|
|
353
|
-
console.log(chalk.yellow(" ⚠ Could not remove .stackpatch directory"));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Step 7: Verification
|
|
358
|
-
console.log(chalk.white("\n✅ Verification..."));
|
|
359
|
-
const remainingManifest = readManifest(target);
|
|
360
|
-
if (remainingManifest) {
|
|
361
|
-
console.log(chalk.red(" ❌ Warning: Manifest still exists. Revert may be incomplete."));
|
|
362
|
-
} else {
|
|
363
|
-
console.log(chalk.green(" ✓ Manifest removed successfully"));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Summary
|
|
367
|
-
console.log(chalk.blue.bold("\n📊 Revert Summary:"));
|
|
368
|
-
console.log(chalk.white(` Files removed: ${chalk.green(removedCount)}`));
|
|
369
|
-
console.log(chalk.white(` Files restored: ${chalk.green(restoredCount)}`));
|
|
370
|
-
if (failedRemovals.length > 0) {
|
|
371
|
-
console.log(chalk.yellow(` Failed removals: ${failedRemovals.length}`));
|
|
372
|
-
failedRemovals.forEach((file) => console.log(chalk.gray(` - ${file}`)));
|
|
373
|
-
}
|
|
374
|
-
if (failedRestorations.length > 0) {
|
|
375
|
-
console.log(chalk.yellow(` Failed restorations: ${failedRestorations.length}`));
|
|
376
|
-
failedRestorations.forEach((file) => console.log(chalk.gray(` - ${file}`)));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (failedRemovals.length === 0 && failedRestorations.length === 0 && !remainingManifest) {
|
|
380
|
-
console.log(
|
|
381
|
-
chalk.green("\n✅ Revert complete! Your project has been fully restored to its original state.")
|
|
382
|
-
);
|
|
383
|
-
if (manifest.dependencies.length > 0) {
|
|
384
|
-
console.log(chalk.yellow("\n⚠️ Remember to run 'pnpm install' to update your node_modules."));
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
console.log(chalk.yellow("\n⚠️ Revert completed with some warnings. Please review the output above."));
|
|
388
|
-
}
|
|
389
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Configuration constants and types for StackPatch CLI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Get directory path - Bun supports import.meta.dir
|
|
8
|
-
// @ts-expect-error - Bun-specific API, not in Node types
|
|
9
|
-
const CLI_DIR = import.meta.dir || path.dirname(new URL(import.meta.url).pathname);
|
|
10
|
-
|
|
11
|
-
// Resolve boilerplate path - use local boilerplate inside CLI package
|
|
12
|
-
export const BOILERPLATE_ROOT = path.resolve(CLI_DIR, "../boilerplate");
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Available patches and their configurations
|
|
16
|
-
*/
|
|
17
|
-
export const PATCHES: Record<string, { path: string; dependencies: string[] }> = {
|
|
18
|
-
auth: {
|
|
19
|
-
path: "auth",
|
|
20
|
-
dependencies: ["better-auth", "react-hot-toast"],
|
|
21
|
-
},
|
|
22
|
-
"auth-ui": {
|
|
23
|
-
path: "auth",
|
|
24
|
-
dependencies: ["better-auth", "react-hot-toast"],
|
|
25
|
-
},
|
|
26
|
-
// Example for future patches:
|
|
27
|
-
// stripe: { path: "stripe", dependencies: ["stripe"] },
|
|
28
|
-
// redux: { path: "redux", dependencies: ["@reduxjs/toolkit", "react-redux"] },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Manifest structure for tracking changes
|
|
33
|
-
*/
|
|
34
|
-
export interface StackPatchManifest {
|
|
35
|
-
version: string;
|
|
36
|
-
patchName: string;
|
|
37
|
-
target: string;
|
|
38
|
-
timestamp: string;
|
|
39
|
-
files: {
|
|
40
|
-
added: string[];
|
|
41
|
-
modified: {
|
|
42
|
-
path: string;
|
|
43
|
-
originalContent: string;
|
|
44
|
-
}[];
|
|
45
|
-
backedUp: string[];
|
|
46
|
-
envFiles?: string[]; // Track .env.local and .env.example if created
|
|
47
|
-
};
|
|
48
|
-
dependencies: string[];
|
|
49
|
-
oauthProviders: string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const MANIFEST_VERSION = "1.0.0";
|