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.
Files changed (2) hide show
  1. package/bin/stackpatch.ts +402 -111
  2. 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
- const destPath = path.join(dest, entry.name);
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
- // Step 3: Update UI components
801
- tracker.startStep(3);
889
+ // Step 3: Update UI components
890
+ tracker.startStep(3);
802
891
  await updateAuthButtonWithProviders(target, selectedProviders);
803
- tracker.completeStep(3);
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
- console.log(chalk.blue.bold("๐Ÿ“ Required Redirect URIs:"));
963
+ console.log(chalk.blue.bold("๐Ÿ“ Required Redirect URIs:"));
875
964
  if (selectedProviders.includes("google")) {
876
- console.log(chalk.white(" For Google: ") + chalk.bold("http://localhost:3000/api/auth/callback/google"));
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
- console.log(chalk.white(" For GitHub: ") + chalk.bold("http://localhost:3000/api/auth/callback/github"));
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
- // Automatically add auth-ui after creating the project
1542
- console.log(chalk.blue.bold(`\n๐Ÿ” Adding authentication to your project...\n`));
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
- const authSrc = path.join(BOILERPLATE_ROOT, PATCHES["auth-ui"].path);
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
- // Install auth dependencies (only if missing)
1551
- installDependencies(targetPath, PATCHES["auth-ui"].dependencies);
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
- if (success) {
1560
- await withSpinner("Updating layout with AuthSessionProvider", () => {
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
- return true;
1566
- });
1654
+ return true;
1655
+ });
1567
1656
 
1568
- await withSpinner("Adding Toaster component", () => {
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
- return true;
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 and restore modified 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
- // Remove added files
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(`โœ“ Removed: ${filePath}`));
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(`โš  Could not remove: ${filePath}`));
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
- // Restore modified files from backups
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(`โœ“ Restored: ${modified.path}`));
1923
+ console.log(chalk.green(` โœ“ Restored: ${modified.path}`));
1924
+ restoredCount++;
1764
1925
  } catch (error) {
1765
- console.log(chalk.yellow(`โš  Could not restore: ${modified.path}`));
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 manifest and backups
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
- fs.rmSync(stackpatchDir, { recursive: true, force: true });
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
- console.log(chalk.green("\nโœ… Revert complete! Your project has been restored to its original state."));
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.0.9",
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",