From 27e13aee87282584872fcc4820cecdbd1523ead8 Mon Sep 17 00:00:00 2001 From: mwdelaney Date: Tue, 14 Oct 2025 10:25:20 -0400 Subject: [PATCH] Fix rebrand script; add Windows workarounds --- .github/scripts/zeropoint-rebrand.js | 188 +++++++++++++++++++++--- .github/workflows/zeropoint-rebrand.yml | 6 +- 2 files changed, 167 insertions(+), 27 deletions(-) diff --git a/.github/scripts/zeropoint-rebrand.js b/.github/scripts/zeropoint-rebrand.js index eb383e5..a4d15a0 100644 --- a/.github/scripts/zeropoint-rebrand.js +++ b/.github/scripts/zeropoint-rebrand.js @@ -3,10 +3,84 @@ * Simple setup script to customize the template for a new project. */ -import { existsSync, unlinkSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, unlinkSync, readFileSync, writeFileSync, readdirSync, statSync, copyFileSync } from 'fs'; import { execSync } from 'child_process'; import { createInterface } from 'readline'; +import { join, extname } from 'path'; +/** + * Cross-platform helper to detect the operating system + * @returns {string} 'windows', 'darwin', or 'linux' + */ +function getOS() { + return process.platform === 'win32' ? 'windows' : process.platform; +} + +/** + * Cross-platform file search and replace function + * Replaces Unix find/sed commands with pure Node.js implementation + * @param {string} search - Text to search for (case-sensitive) + * @param {string} replace - Text to replace with + */ +function replaceInFilesNative(search, replace) { + const targetExtensions = ['.md', '.json', '.js', '.njk', '.yml', '.toml']; + const excludeDirs = ['node_modules', '.git']; + + function processDirectory(dir) { + try { + const items = readdirSync(dir); + + for (const item of items) { + const fullPath = join(dir, item); + + // Skip excluded directories + if (excludeDirs.includes(item)) continue; + + try { + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + processDirectory(fullPath); + } else if (stat.isFile() && targetExtensions.includes(extname(item))) { + // Process file + try { + const content = readFileSync(fullPath, 'utf8'); + const newContent = content.replace(new RegExp(escapeRegExp(search), 'g'), replace); + + if (content !== newContent) { + writeFileSync(fullPath, newContent, 'utf8'); + } + } catch (fileError) { + console.warn(`Could not process file ${fullPath}: ${fileError.message}`); + } + } + } catch (statError) { + console.warn(`Could not stat ${fullPath}: ${statError.message}`); + } + } + } catch (dirError) { + console.warn(`Could not read directory ${dir}: ${dirError.message}`); + } + } + + processDirectory('.'); +} + +/** + * Escapes special regex characters in a string + * @param {string} string - String to escape + * @returns {string} Escaped string safe for use in regex + */ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Prompts user for input with optional default value + * @param {string} question - The question to ask + * @param {string} defaultValue - Default value if user presses enter + * @returns {Promise} User's response or default value + */ async function askQuestion(question, defaultValue = '') { const rl = createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => { @@ -18,9 +92,16 @@ async function askQuestion(question, defaultValue = '') { }); } +/** + * Extracts git author name and repository name from git config + * Falls back to sensible defaults if git info is unavailable + * @returns {Object} Object with author and repoName properties + */ function getGitInfo() { try { + // Get git author from global config const gitAuthor = execSync('git config user.name', { encoding: 'utf8' }).trim(); + // Get remote origin URL and extract repo name const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim(); const match = remoteUrl.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/); @@ -29,41 +110,92 @@ function getGitInfo() { repoName: match ? match[2] : 'my-website' }; } catch { + // Return defaults if git commands fail return { author: 'username', repoName: 'my-website' }; } } +/** + * Performs case-sensitive find and replace across project files + * Uses native Node.js for cross-platform compatibility instead of Unix commands + * @param {string} search - Text to search for (case-sensitive) + * @param {string} replace - Text to replace with + */ function replaceInFiles(search, replace) { - try { - execSync(`find . -type f \\( -name "*.md" -o -name "*.json" -o -name "*.js" -o -name "*.njk" -o -name "*.yml" -o -name "*.toml" \\) -not -path "./node_modules/*" -not -path "./.git/*" -exec sed -i.bak "s/${search}/${replace}/g" {} + 2>/dev/null || true`); - execSync(`find . -name "*.bak" -not -path "./node_modules/*" -not -path "./.git/*" -delete 2>/dev/null || true`); - } catch (error) { - console.warn(`Could not replace ${search}: ${error.message}`); + const os = getOS(); + + // Use native Node.js implementation for better cross-platform support + if (os === 'windows') { + replaceInFilesNative(search, replace); + } else { + // Keep Unix approach for Unix systems for performance + try { + // Find relevant files and perform sed replacement with backup + execSync(`find . -type f \\( -name "*.md" -o -name "*.json" -o -name "*.js" -o -name "*.njk" -o -name "*.yml" -o -name "*.toml" \\) -not -path "./node_modules/*" -not -path "./.git/*" -exec sed -i.bak "s/${escapeRegExp(search)}/${replace}/g" {} + 2>/dev/null || true`); + // Clean up backup files created by sed + execSync(`find . -name "*.bak" -not -path "./node_modules/*" -not -path "./.git/*" -delete 2>/dev/null || true`); + } catch (error) { + // Fallback to native implementation if Unix commands fail + console.warn(`Unix commands failed, falling back to native implementation: ${error.message}`); + replaceInFilesNative(search, replace); + } } } +/** + * Main rebranding function - orchestrates the entire process + * Checks for template marker, gathers user input, performs replacements, and cleans up + */ async function rebrand() { - // Exit if already rebranded + // Check if running in CI environment (GitHub Actions) + const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; + + // Exit early if already rebranded (no .template file) if (!existsSync('.template')) { + if (isCI) { + console.log('โš ๏ธ Template already rebranded, skipping...'); + } process.exit(0); } - // Safety check - make sure we're in a ZeroPoint template + // Safety check - ensure we're working with a ZeroPoint template const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); if (packageJson.name !== 'ZeroPoint') { + // Clean up template marker if this isn't a ZeroPoint template unlinkSync('.template'); + if (isCI) { + console.log('โš ๏ธ Not a ZeroPoint template, skipping...'); + } process.exit(0); } console.log('๐Ÿช Welcome to ZeroPoint! Let\'s customize this for your project.\n'); - // Get user input + // Get git information for defaults const gitInfo = getGitInfo(); - const projectName = await askQuestion('Project name?', gitInfo.repoName); - const author = await askQuestion('Your name/username?', gitInfo.author); - const description = await askQuestion('Project description (optional)?'); - const proceed = await askQuestion('Ready to rebrand? (y/n)', 'y'); + let projectName, author, description, proceed; + + if (isCI) { + // In CI: use git defaults and proceed automatically + projectName = gitInfo.repoName; + author = gitInfo.author; + description = ''; + proceed = 'y'; + + console.log(`Using defaults for CI:`); + console.log(`- Project name: ${projectName}`); + console.log(`- Author: ${author}`); + console.log(`- Description: (empty)`); + } else { + // Interactive mode: ask user for input + projectName = await askQuestion('Project name?', gitInfo.repoName); + author = await askQuestion('Your name/username?', gitInfo.author); + description = await askQuestion('Project description (optional)?'); + proceed = await askQuestion('Ready to rebrand? (y/n)', 'y'); + } + + // Exit if user doesn't want to proceed if (!['y', 'yes'].includes(proceed.toLowerCase())) { console.log('Cancelled. Run npm run dev again to retry.'); process.exit(0); @@ -71,29 +203,37 @@ async function rebrand() { console.log('\n๐Ÿ”„ Updating files...'); - // Replace text in files - // Replace all occurrences of 'ZeroPoint' with the project name, with spaces removed - replaceInFiles('ZeroPoint', projectName.replace(/\s+/g, '')); - replaceInFiles('MWDelaney', author); - replaceInFiles('zeropoint', projectName.toLowerCase().replace(/\\s+/g, '-')); + // Perform text replacements across all project files (case-sensitive) + replaceInFiles('ZeroPoint', projectName.replace(/\s+/g, '')); // Remove spaces from project name + replaceInFiles('MWDelaney', author); // Replace default author + replaceInFiles('zeropoint', projectName.toLowerCase().replace(/\s+/g, '-')); // Lowercase with hyphens - // Update package.json + // Update package.json with new details const pkg = JSON.parse(readFileSync('package.json', 'utf8')); if (description) pkg.description = description; - delete pkg.scripts['pre๐Ÿช']; // Remove the rebranding script + delete pkg.scripts['pre๐Ÿช']; // Remove the rebranding script from package.json writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); - // Copy README files + // Handle README files - backup current and use template version if (existsSync('README.md')) { - execSync('cp README.md README.template.md'); + try { + copyFileSync('README.md', 'README.template.md'); // Backup current README + } catch (error) { + console.warn(`Could not backup README.md: ${error.message}`); + } } if (existsSync('README.ZeroPoint.md')) { - execSync('cp README.ZeroPoint.md README.md'); + try { + copyFileSync('README.ZeroPoint.md', 'README.md'); // Use ZeroPoint README as new README + } catch (error) { + console.warn(`Could not copy README.ZeroPoint.md: ${error.message}`); + } } - // Clean up + // Clean up template marker file unlinkSync('.template'); console.log('โœ… Rebranding complete! Starting development server...\n'); } +// Run the rebranding process await rebrand(); diff --git a/.github/workflows/zeropoint-rebrand.yml b/.github/workflows/zeropoint-rebrand.yml index 0428783..e87b2cb 100644 --- a/.github/workflows/zeropoint-rebrand.yml +++ b/.github/workflows/zeropoint-rebrand.yml @@ -16,9 +16,9 @@ jobs: node-version-file: '.nvmrc' - name: ๐Ÿท๏ธ Run rebranding script - run: | - # Run the rebranding script with automated inputs (just press enter for defaults and 'y' to proceed) - echo -e "\n\n\ny" | node .github/scripts/zeropoint-rebrand.js + run: node .github/scripts/zeropoint-rebrand.js + env: + CI: 'true' # This tells the script to run in automated mode - name: ๐Ÿš€ Commit changes and delete this workflow uses: EndBug/add-and-commit@v9