stackpatch 1.0.9 โ 1.1.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 +402 -111
- package/package.json +1 -1
package/bin/stackpatch.ts
CHANGED
|
@@ -44,6 +44,7 @@ interface StackPatchManifest {
|
|
|
44
44
|
originalContent: string;
|
|
45
45
|
}[];
|
|
46
46
|
backedUp: string[];
|
|
47
|
+
envFiles?: string[]; // Track .env.local and .env.example if created
|
|
47
48
|
};
|
|
48
49
|
dependencies: string[];
|
|
49
50
|
oauthProviders: string[];
|
|
@@ -276,7 +277,7 @@ async function copyFiles(src: string, dest: string): Promise<{ success: boolean;
|
|
|
276
277
|
}
|
|
277
278
|
} else {
|
|
278
279
|
// For other files/directories (middleware, etc.), check in root
|
|
279
|
-
|
|
280
|
+
const destPath = path.join(dest, entry.name);
|
|
280
281
|
if (fs.existsSync(destPath)) {
|
|
281
282
|
conflicts.push(destPath);
|
|
282
283
|
}
|
|
@@ -302,53 +303,56 @@ async function copyFiles(src: string, dest: string): Promise<{ success: boolean;
|
|
|
302
303
|
}
|
|
303
304
|
}
|
|
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
|
+
|
|
305
326
|
// Copy files with smart app directory handling
|
|
306
327
|
for (const entry of entries) {
|
|
307
328
|
const srcPath = path.join(src, entry.name);
|
|
308
329
|
|
|
309
330
|
if (entry.name === "app") {
|
|
331
|
+
// Track files from SOURCE boilerplate before copying
|
|
332
|
+
trackSourceFiles(srcPath, srcPath, appDir);
|
|
333
|
+
|
|
310
334
|
// Copy app directory contents to the detected app directory location
|
|
311
335
|
await fse.ensureDir(appDirPath);
|
|
312
336
|
await fse.copy(srcPath, appDirPath, { overwrite: true });
|
|
313
|
-
|
|
314
|
-
// Track all files in app directory
|
|
315
|
-
function trackFiles(dir: string, baseDir: string) {
|
|
316
|
-
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
317
|
-
for (const file of files) {
|
|
318
|
-
const filePath = path.join(dir, file.name);
|
|
319
|
-
if (file.isDirectory()) {
|
|
320
|
-
trackFiles(filePath, baseDir);
|
|
321
|
-
} else {
|
|
322
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
323
|
-
addedFiles.push(path.join(appDir, relativePath).replace(/\\/g, "/"));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
trackFiles(appDirPath, appDirPath);
|
|
328
337
|
} else if (entry.name === "components") {
|
|
338
|
+
// Track files from SOURCE boilerplate before copying
|
|
339
|
+
trackSourceFiles(srcPath, srcPath, componentsDir);
|
|
340
|
+
|
|
329
341
|
// Copy components directory to the detected components directory location
|
|
330
342
|
await fse.ensureDir(componentsDirPath);
|
|
331
343
|
await fse.copy(srcPath, componentsDirPath, { overwrite: true });
|
|
332
|
-
|
|
333
|
-
// Track all files in components directory
|
|
334
|
-
function trackFiles(dir: string, baseDir: string) {
|
|
335
|
-
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
336
|
-
for (const file of files) {
|
|
337
|
-
const filePath = path.join(dir, file.name);
|
|
338
|
-
if (file.isDirectory()) {
|
|
339
|
-
trackFiles(filePath, baseDir);
|
|
340
|
-
} else {
|
|
341
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
342
|
-
addedFiles.push(path.join(componentsDir, relativePath).replace(/\\/g, "/"));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
trackFiles(componentsDirPath, componentsDirPath);
|
|
347
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
|
+
|
|
348
353
|
// Copy other files/directories (middleware, etc.) to root
|
|
349
354
|
const destPath = path.join(dest, entry.name);
|
|
350
355
|
await fse.copy(srcPath, destPath, { overwrite: true });
|
|
351
|
-
addedFiles.push(entry.name);
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
|
|
@@ -574,6 +578,91 @@ function installDependencies(target: string, deps: string[]) {
|
|
|
574
578
|
}
|
|
575
579
|
}
|
|
576
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
|
+
|
|
577
666
|
// Update layout.tsx to include Toaster
|
|
578
667
|
function updateLayoutForToaster(target: string): { success: boolean; modified: boolean; filePath: string; originalContent?: string } {
|
|
579
668
|
const appDir = detectAppDirectory(target);
|
|
@@ -797,10 +886,10 @@ async function setupAuth(target: string, selectedProviders: string[]): Promise<b
|
|
|
797
886
|
await updateNextAuthWithProviders(nextAuthRoutePath, selectedProviders);
|
|
798
887
|
tracker.completeStep(2);
|
|
799
888
|
|
|
800
|
-
|
|
801
|
-
|
|
889
|
+
// Step 3: Update UI components
|
|
890
|
+
tracker.startStep(3);
|
|
802
891
|
await updateAuthButtonWithProviders(target, selectedProviders);
|
|
803
|
-
|
|
892
|
+
tracker.completeStep(3);
|
|
804
893
|
|
|
805
894
|
tracker.completeStep(0);
|
|
806
895
|
|
|
@@ -871,12 +960,12 @@ async function showOAuthSetupInstructions(target: string, selectedProviders: str
|
|
|
871
960
|
}
|
|
872
961
|
|
|
873
962
|
if (selectedProviders.includes("google") || selectedProviders.includes("github")) {
|
|
874
|
-
|
|
963
|
+
console.log(chalk.blue.bold("๐ Required Redirect URIs:"));
|
|
875
964
|
if (selectedProviders.includes("google")) {
|
|
876
|
-
|
|
965
|
+
console.log(chalk.white(" For Google: ") + chalk.bold("http://localhost:3000/api/auth/callback/google"));
|
|
877
966
|
}
|
|
878
967
|
if (selectedProviders.includes("github")) {
|
|
879
|
-
|
|
968
|
+
console.log(chalk.white(" For GitHub: ") + chalk.bold("http://localhost:3000/api/auth/callback/github"));
|
|
880
969
|
}
|
|
881
970
|
console.log(chalk.gray("\n For production, also add your production domain URLs\n"));
|
|
882
971
|
}
|
|
@@ -1538,17 +1627,17 @@ async function createProject(projectName: string, showWelcomeScreen: boolean = t
|
|
|
1538
1627
|
|
|
1539
1628
|
console.log(chalk.green(`\nโ
Project "${projectName}" created successfully!`));
|
|
1540
1629
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1630
|
+
// Automatically add auth-ui after creating the project
|
|
1631
|
+
console.log(chalk.blue.bold(`\n๐ Adding authentication to your project...\n`));
|
|
1543
1632
|
|
|
1544
|
-
|
|
1633
|
+
const authSrc = path.join(BOILERPLATE_ROOT, PATCHES["auth-ui"].path);
|
|
1545
1634
|
const authCopyResult = await copyFiles(authSrc, targetPath);
|
|
1546
1635
|
|
|
1547
1636
|
if (authCopyResult.success) {
|
|
1548
1637
|
const addedFiles = authCopyResult.addedFiles;
|
|
1549
1638
|
const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
|
|
1550
|
-
|
|
1551
|
-
|
|
1639
|
+
// Install auth dependencies (only if missing)
|
|
1640
|
+
installDependencies(targetPath, PATCHES["auth-ui"].dependencies);
|
|
1552
1641
|
|
|
1553
1642
|
// Ask which OAuth providers to configure
|
|
1554
1643
|
const selectedProviders = await askOAuthProviders();
|
|
@@ -1556,22 +1645,22 @@ async function createProject(projectName: string, showWelcomeScreen: boolean = t
|
|
|
1556
1645
|
// Setup authentication with selected providers
|
|
1557
1646
|
const success = await setupAuth(targetPath, selectedProviders);
|
|
1558
1647
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1648
|
+
if (success) {
|
|
1649
|
+
await withSpinner("Updating layout with AuthSessionProvider", () => {
|
|
1561
1650
|
const result = updateLayoutForAuth(targetPath);
|
|
1562
1651
|
if (result.modified && result.originalContent) {
|
|
1563
1652
|
modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
|
|
1564
1653
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1654
|
+
return true;
|
|
1655
|
+
});
|
|
1567
1656
|
|
|
1568
|
-
|
|
1657
|
+
await withSpinner("Adding Toaster component", () => {
|
|
1569
1658
|
const result = updateLayoutForToaster(targetPath);
|
|
1570
1659
|
if (result.modified && result.originalContent) {
|
|
1571
1660
|
modifiedFiles.push({ path: result.filePath, originalContent: result.originalContent });
|
|
1572
1661
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1662
|
+
return true;
|
|
1663
|
+
});
|
|
1575
1664
|
|
|
1576
1665
|
await withSpinner("Setting up protected routes", () => {
|
|
1577
1666
|
copyProtectedRouteFiles(targetPath);
|
|
@@ -1643,57 +1732,6 @@ async function main() {
|
|
|
1643
1732
|
return;
|
|
1644
1733
|
}
|
|
1645
1734
|
|
|
1646
|
-
// Handle: bun create stackpatch@latest my-app
|
|
1647
|
-
// When bun runs create, it passes project name as first arg (not "create")
|
|
1648
|
-
// Check if first arg looks like a project name (not a known command)
|
|
1649
|
-
// Always ask for project name first, even if provided
|
|
1650
|
-
if (command && !["add", "create"].includes(command) && !PATCHES[command] && !command.startsWith("-")) {
|
|
1651
|
-
// Likely called as: bun create stackpatch@latest my-app
|
|
1652
|
-
// But we'll ask for project name anyway to be consistent
|
|
1653
|
-
await showWelcome();
|
|
1654
|
-
const { name } = await inquirer.prompt([
|
|
1655
|
-
{
|
|
1656
|
-
type: "input",
|
|
1657
|
-
name: "name",
|
|
1658
|
-
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
1659
|
-
default: command || "my-stackpatch-app", // Use provided name as default
|
|
1660
|
-
validate: (input: string) => {
|
|
1661
|
-
if (!input.trim()) {
|
|
1662
|
-
return "Project name cannot be empty";
|
|
1663
|
-
}
|
|
1664
|
-
return true;
|
|
1665
|
-
},
|
|
1666
|
-
},
|
|
1667
|
-
]);
|
|
1668
|
-
await createProject(name.trim(), false, skipPrompts); // Welcome already shown
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
// Handle: npx stackpatch create my-app
|
|
1673
|
-
if (command === "create") {
|
|
1674
|
-
if (!projectName) {
|
|
1675
|
-
showWelcome();
|
|
1676
|
-
const { name } = await inquirer.prompt([
|
|
1677
|
-
{
|
|
1678
|
-
type: "input",
|
|
1679
|
-
name: "name",
|
|
1680
|
-
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
1681
|
-
default: "my-stackpatch-app",
|
|
1682
|
-
validate: (input: string) => {
|
|
1683
|
-
if (!input.trim()) {
|
|
1684
|
-
return "Project name cannot be empty";
|
|
1685
|
-
}
|
|
1686
|
-
return true;
|
|
1687
|
-
},
|
|
1688
|
-
},
|
|
1689
|
-
]);
|
|
1690
|
-
await createProject(name.trim(), false); // Logo already shown
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
await createProject(projectName, false, skipPrompts); // Logo already shown
|
|
1694
|
-
return;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
1735
|
// Handle: npx stackpatch revert
|
|
1698
1736
|
if (command === "revert") {
|
|
1699
1737
|
let target = process.cwd();
|
|
@@ -1725,11 +1763,19 @@ async function main() {
|
|
|
1725
1763
|
console.log(chalk.white(` Patch: ${chalk.cyan(manifest.patchName)}`));
|
|
1726
1764
|
console.log(chalk.white(` Installed: ${chalk.gray(new Date(manifest.timestamp).toLocaleString())}\n`));
|
|
1727
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
|
+
|
|
1728
1774
|
const { confirm } = await inquirer.prompt([
|
|
1729
1775
|
{
|
|
1730
1776
|
type: "confirm",
|
|
1731
1777
|
name: "confirm",
|
|
1732
|
-
message: "Are you sure you want to revert this installation? This will remove all added files
|
|
1778
|
+
message: "Are you sure you want to revert this installation? This will remove all added files, restore modified files, and remove dependencies.",
|
|
1733
1779
|
default: false,
|
|
1734
1780
|
},
|
|
1735
1781
|
]);
|
|
@@ -1739,20 +1785,134 @@ async function main() {
|
|
|
1739
1785
|
return;
|
|
1740
1786
|
}
|
|
1741
1787
|
|
|
1742
|
-
|
|
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..."));
|
|
1743
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
|
+
|
|
1744
1856
|
const fullPath = path.join(target, filePath);
|
|
1745
1857
|
if (fs.existsSync(fullPath)) {
|
|
1746
1858
|
try {
|
|
1747
1859
|
fs.unlinkSync(fullPath);
|
|
1748
|
-
console.log(chalk.green(
|
|
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));
|
|
1749
1866
|
} catch (error) {
|
|
1750
|
-
console.log(chalk.yellow(
|
|
1867
|
+
console.log(chalk.yellow(` โ Could not remove: ${filePath}`));
|
|
1868
|
+
failedRemovals.push(filePath);
|
|
1751
1869
|
}
|
|
1870
|
+
} else {
|
|
1871
|
+
console.log(chalk.gray(` โ Already removed: ${filePath}`));
|
|
1752
1872
|
}
|
|
1753
1873
|
}
|
|
1754
1874
|
|
|
1755
|
-
//
|
|
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 backups
|
|
1915
|
+
console.log(chalk.white("\n๐ Restoring modified files..."));
|
|
1756
1916
|
for (const modified of manifest.files.modified) {
|
|
1757
1917
|
const backupPath = path.join(target, ".stackpatch", "backups", modified.path.replace(/\//g, "_").replace(/\\/g, "_"));
|
|
1758
1918
|
const originalPath = path.join(target, modified.path);
|
|
@@ -1760,20 +1920,151 @@ async function main() {
|
|
|
1760
1920
|
if (fs.existsSync(backupPath)) {
|
|
1761
1921
|
try {
|
|
1762
1922
|
restoreFile(backupPath, originalPath);
|
|
1763
|
-
console.log(chalk.green(
|
|
1923
|
+
console.log(chalk.green(` โ Restored: ${modified.path}`));
|
|
1924
|
+
restoredCount++;
|
|
1764
1925
|
} catch (error) {
|
|
1765
|
-
console.log(chalk.yellow(
|
|
1926
|
+
console.log(chalk.yellow(` โ Could not restore: ${modified.path}`));
|
|
1927
|
+
failedRestorations.push(modified.path);
|
|
1766
1928
|
}
|
|
1929
|
+
} else {
|
|
1930
|
+
console.log(chalk.yellow(` โ Backup not found: ${modified.path}`));
|
|
1931
|
+
failedRestorations.push(modified.path);
|
|
1767
1932
|
}
|
|
1768
1933
|
}
|
|
1769
1934
|
|
|
1770
|
-
// Remove
|
|
1935
|
+
// Step 4: Remove dependencies from package.json
|
|
1936
|
+
if (manifest.dependencies.length > 0) {
|
|
1937
|
+
console.log(chalk.white("\n๐ฆ Removing dependencies from package.json..."));
|
|
1938
|
+
const removed = removeDependencies(target, manifest.dependencies);
|
|
1939
|
+
if (removed) {
|
|
1940
|
+
console.log(chalk.green(` โ Removed dependencies: ${manifest.dependencies.join(", ")}`));
|
|
1941
|
+
console.log(chalk.yellow(" โ Run 'pnpm install' to update node_modules"));
|
|
1942
|
+
} else {
|
|
1943
|
+
console.log(chalk.gray(" โ Dependencies not found in package.json"));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Step 5: Clean up empty directories (only if they only contained StackPatch files)
|
|
1948
|
+
console.log(chalk.white("\n๐งน Cleaning up empty directories..."));
|
|
1949
|
+
const sortedDirs = Array.from(directoriesToCheck).sort((a, b) => b.length - a.length); // Sort by depth (deepest first)
|
|
1950
|
+
let removedDirCount = 0;
|
|
1951
|
+
|
|
1952
|
+
for (const dir of sortedDirs) {
|
|
1953
|
+
if (fs.existsSync(dir)) {
|
|
1954
|
+
try {
|
|
1955
|
+
const entries = fs.readdirSync(dir);
|
|
1956
|
+
if (entries.length === 0) {
|
|
1957
|
+
// Only remove if directory is empty
|
|
1958
|
+
// We know it was created by StackPatch because we're tracking it
|
|
1959
|
+
fs.rmdirSync(dir);
|
|
1960
|
+
removedDirCount++;
|
|
1961
|
+
console.log(chalk.green(` โ Removed empty directory: ${path.relative(target, dir)}`));
|
|
1962
|
+
}
|
|
1963
|
+
// If directory has other files, we don't remove it (silently skip)
|
|
1964
|
+
} catch {
|
|
1965
|
+
// Ignore errors
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (removedDirCount === 0) {
|
|
1971
|
+
console.log(chalk.gray(" โ No empty directories to remove"));
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Step 6: Remove manifest and backups
|
|
1975
|
+
console.log(chalk.white("\n๐๏ธ Removing StackPatch tracking files..."));
|
|
1771
1976
|
const stackpatchDir = path.join(target, ".stackpatch");
|
|
1772
1977
|
if (fs.existsSync(stackpatchDir)) {
|
|
1773
|
-
|
|
1978
|
+
try {
|
|
1979
|
+
fs.rmSync(stackpatchDir, { recursive: true, force: true });
|
|
1980
|
+
console.log(chalk.green(" โ Removed .stackpatch directory"));
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
console.log(chalk.yellow(" โ Could not remove .stackpatch directory"));
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// Step 7: Verification
|
|
1987
|
+
console.log(chalk.white("\nโ
Verification..."));
|
|
1988
|
+
const remainingManifest = readManifest(target);
|
|
1989
|
+
if (remainingManifest) {
|
|
1990
|
+
console.log(chalk.red(" โ Warning: Manifest still exists. Revert may be incomplete."));
|
|
1991
|
+
} else {
|
|
1992
|
+
console.log(chalk.green(" โ Manifest removed successfully"));
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Summary
|
|
1996
|
+
console.log(chalk.blue.bold("\n๐ Revert Summary:"));
|
|
1997
|
+
console.log(chalk.white(` Files removed: ${chalk.green(removedCount)}`));
|
|
1998
|
+
console.log(chalk.white(` Files restored: ${chalk.green(restoredCount)}`));
|
|
1999
|
+
if (failedRemovals.length > 0) {
|
|
2000
|
+
console.log(chalk.yellow(` Failed removals: ${failedRemovals.length}`));
|
|
2001
|
+
failedRemovals.forEach(file => console.log(chalk.gray(` - ${file}`)));
|
|
2002
|
+
}
|
|
2003
|
+
if (failedRestorations.length > 0) {
|
|
2004
|
+
console.log(chalk.yellow(` Failed restorations: ${failedRestorations.length}`));
|
|
2005
|
+
failedRestorations.forEach(file => console.log(chalk.gray(` - ${file}`)));
|
|
1774
2006
|
}
|
|
1775
2007
|
|
|
1776
|
-
|
|
2008
|
+
if (failedRemovals.length === 0 && failedRestorations.length === 0 && !remainingManifest) {
|
|
2009
|
+
console.log(chalk.green("\nโ
Revert complete! Your project has been fully restored to its original state."));
|
|
2010
|
+
if (manifest.dependencies.length > 0) {
|
|
2011
|
+
console.log(chalk.yellow("\nโ ๏ธ Remember to run 'pnpm install' to update your node_modules."));
|
|
2012
|
+
}
|
|
2013
|
+
} else {
|
|
2014
|
+
console.log(chalk.yellow("\nโ ๏ธ Revert completed with some warnings. Please review the output above."));
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Handle: bun create stackpatch@latest my-app
|
|
2021
|
+
// When bun runs create, it passes project name as first arg (not "create")
|
|
2022
|
+
// Check if first arg looks like a project name (not a known command)
|
|
2023
|
+
// Always ask for project name first, even if provided
|
|
2024
|
+
if (command && !["add", "create", "revert"].includes(command) && !PATCHES[command] && !command.startsWith("-")) {
|
|
2025
|
+
// Likely called as: bun create stackpatch@latest my-app
|
|
2026
|
+
// But we'll ask for project name anyway to be consistent
|
|
2027
|
+
await showWelcome();
|
|
2028
|
+
const { name } = await inquirer.prompt([
|
|
2029
|
+
{
|
|
2030
|
+
type: "input",
|
|
2031
|
+
name: "name",
|
|
2032
|
+
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
2033
|
+
default: command || "my-stackpatch-app", // Use provided name as default
|
|
2034
|
+
validate: (input: string) => {
|
|
2035
|
+
if (!input.trim()) {
|
|
2036
|
+
return "Project name cannot be empty";
|
|
2037
|
+
}
|
|
2038
|
+
return true;
|
|
2039
|
+
},
|
|
2040
|
+
},
|
|
2041
|
+
]);
|
|
2042
|
+
await createProject(name.trim(), false, skipPrompts); // Welcome already shown
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// Handle: npx stackpatch create my-app
|
|
2047
|
+
if (command === "create") {
|
|
2048
|
+
if (!projectName) {
|
|
2049
|
+
showWelcome();
|
|
2050
|
+
const { name } = await inquirer.prompt([
|
|
2051
|
+
{
|
|
2052
|
+
type: "input",
|
|
2053
|
+
name: "name",
|
|
2054
|
+
message: chalk.white("Enter your project name or path (relative to current directory)"),
|
|
2055
|
+
default: "my-stackpatch-app",
|
|
2056
|
+
validate: (input: string) => {
|
|
2057
|
+
if (!input.trim()) {
|
|
2058
|
+
return "Project name cannot be empty";
|
|
2059
|
+
}
|
|
2060
|
+
return true;
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
2063
|
+
]);
|
|
2064
|
+
await createProject(name.trim(), false); // Logo already shown
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
await createProject(projectName, false, skipPrompts); // Logo already shown
|
|
1777
2068
|
return;
|
|
1778
2069
|
}
|
|
1779
2070
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stackpatch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Composable frontend features for modern React & Next.js apps - Add authentication, UI components, and more with zero configuration",
|
|
5
5
|
"main": "bin/stackpatch.ts",
|
|
6
6
|
"type": "module",
|