Skip to content

Commit 26a6e1f

Browse files
authored
Merge pull request #45 from MWDelaney/fix-rebrand
Fix rebrand script; add Windows workarounds
2 parents 102bef6 + 27e13ae commit 26a6e1f

File tree

2 files changed

+167
-27
lines changed

2 files changed

+167
-27
lines changed

.github/scripts/zeropoint-rebrand.js

Lines changed: 164 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,84 @@
33
* Simple setup script to customize the template for a new project.
44
*/
55

6-
import { existsSync, unlinkSync, readFileSync, writeFileSync } from 'fs';
6+
import { existsSync, unlinkSync, readFileSync, writeFileSync, readdirSync, statSync, copyFileSync } from 'fs';
77
import { execSync } from 'child_process';
88
import { createInterface } from 'readline';
9+
import { join, extname } from 'path';
910

11+
/**
12+
* Cross-platform helper to detect the operating system
13+
* @returns {string} 'windows', 'darwin', or 'linux'
14+
*/
15+
function getOS() {
16+
return process.platform === 'win32' ? 'windows' : process.platform;
17+
}
18+
19+
/**
20+
* Cross-platform file search and replace function
21+
* Replaces Unix find/sed commands with pure Node.js implementation
22+
* @param {string} search - Text to search for (case-sensitive)
23+
* @param {string} replace - Text to replace with
24+
*/
25+
function replaceInFilesNative(search, replace) {
26+
const targetExtensions = ['.md', '.json', '.js', '.njk', '.yml', '.toml'];
27+
const excludeDirs = ['node_modules', '.git'];
28+
29+
function processDirectory(dir) {
30+
try {
31+
const items = readdirSync(dir);
32+
33+
for (const item of items) {
34+
const fullPath = join(dir, item);
35+
36+
// Skip excluded directories
37+
if (excludeDirs.includes(item)) continue;
38+
39+
try {
40+
const stat = statSync(fullPath);
41+
42+
if (stat.isDirectory()) {
43+
processDirectory(fullPath);
44+
} else if (stat.isFile() && targetExtensions.includes(extname(item))) {
45+
// Process file
46+
try {
47+
const content = readFileSync(fullPath, 'utf8');
48+
const newContent = content.replace(new RegExp(escapeRegExp(search), 'g'), replace);
49+
50+
if (content !== newContent) {
51+
writeFileSync(fullPath, newContent, 'utf8');
52+
}
53+
} catch (fileError) {
54+
console.warn(`Could not process file ${fullPath}: ${fileError.message}`);
55+
}
56+
}
57+
} catch (statError) {
58+
console.warn(`Could not stat ${fullPath}: ${statError.message}`);
59+
}
60+
}
61+
} catch (dirError) {
62+
console.warn(`Could not read directory ${dir}: ${dirError.message}`);
63+
}
64+
}
65+
66+
processDirectory('.');
67+
}
68+
69+
/**
70+
* Escapes special regex characters in a string
71+
* @param {string} string - String to escape
72+
* @returns {string} Escaped string safe for use in regex
73+
*/
74+
function escapeRegExp(string) {
75+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76+
}
77+
78+
/**
79+
* Prompts user for input with optional default value
80+
* @param {string} question - The question to ask
81+
* @param {string} defaultValue - Default value if user presses enter
82+
* @returns {Promise<string>} User's response or default value
83+
*/
1084
async function askQuestion(question, defaultValue = '') {
1185
const rl = createInterface({ input: process.stdin, output: process.stdout });
1286
return new Promise(resolve => {
@@ -18,9 +92,16 @@ async function askQuestion(question, defaultValue = '') {
1892
});
1993
}
2094

95+
/**
96+
* Extracts git author name and repository name from git config
97+
* Falls back to sensible defaults if git info is unavailable
98+
* @returns {Object} Object with author and repoName properties
99+
*/
21100
function getGitInfo() {
22101
try {
102+
// Get git author from global config
23103
const gitAuthor = execSync('git config user.name', { encoding: 'utf8' }).trim();
104+
// Get remote origin URL and extract repo name
24105
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
25106
const match = remoteUrl.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
26107

@@ -29,71 +110,130 @@ function getGitInfo() {
29110
repoName: match ? match[2] : 'my-website'
30111
};
31112
} catch {
113+
// Return defaults if git commands fail
32114
return { author: 'username', repoName: 'my-website' };
33115
}
34116
}
35117

118+
/**
119+
* Performs case-sensitive find and replace across project files
120+
* Uses native Node.js for cross-platform compatibility instead of Unix commands
121+
* @param {string} search - Text to search for (case-sensitive)
122+
* @param {string} replace - Text to replace with
123+
*/
36124
function replaceInFiles(search, replace) {
37-
try {
38-
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`);
39-
execSync(`find . -name "*.bak" -not -path "./node_modules/*" -not -path "./.git/*" -delete 2>/dev/null || true`);
40-
} catch (error) {
41-
console.warn(`Could not replace ${search}: ${error.message}`);
125+
const os = getOS();
126+
127+
// Use native Node.js implementation for better cross-platform support
128+
if (os === 'windows') {
129+
replaceInFilesNative(search, replace);
130+
} else {
131+
// Keep Unix approach for Unix systems for performance
132+
try {
133+
// Find relevant files and perform sed replacement with backup
134+
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`);
135+
// Clean up backup files created by sed
136+
execSync(`find . -name "*.bak" -not -path "./node_modules/*" -not -path "./.git/*" -delete 2>/dev/null || true`);
137+
} catch (error) {
138+
// Fallback to native implementation if Unix commands fail
139+
console.warn(`Unix commands failed, falling back to native implementation: ${error.message}`);
140+
replaceInFilesNative(search, replace);
141+
}
42142
}
43143
}
44144

145+
/**
146+
* Main rebranding function - orchestrates the entire process
147+
* Checks for template marker, gathers user input, performs replacements, and cleans up
148+
*/
45149
async function rebrand() {
46-
// Exit if already rebranded
150+
// Check if running in CI environment (GitHub Actions)
151+
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
152+
153+
// Exit early if already rebranded (no .template file)
47154
if (!existsSync('.template')) {
155+
if (isCI) {
156+
console.log('⚠️ Template already rebranded, skipping...');
157+
}
48158
process.exit(0);
49159
}
50160

51-
// Safety check - make sure we're in a ZeroPoint template
161+
// Safety check - ensure we're working with a ZeroPoint template
52162
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
53163
if (packageJson.name !== 'ZeroPoint') {
164+
// Clean up template marker if this isn't a ZeroPoint template
54165
unlinkSync('.template');
166+
if (isCI) {
167+
console.log('⚠️ Not a ZeroPoint template, skipping...');
168+
}
55169
process.exit(0);
56170
}
57171

58172
console.log('🪐 Welcome to ZeroPoint! Let\'s customize this for your project.\n');
59173

60-
// Get user input
174+
// Get git information for defaults
61175
const gitInfo = getGitInfo();
62-
const projectName = await askQuestion('Project name?', gitInfo.repoName);
63-
const author = await askQuestion('Your name/username?', gitInfo.author);
64-
const description = await askQuestion('Project description (optional)?');
65-
const proceed = await askQuestion('Ready to rebrand? (y/n)', 'y');
66176

177+
let projectName, author, description, proceed;
178+
179+
if (isCI) {
180+
// In CI: use git defaults and proceed automatically
181+
projectName = gitInfo.repoName;
182+
author = gitInfo.author;
183+
description = '';
184+
proceed = 'y';
185+
186+
console.log(`Using defaults for CI:`);
187+
console.log(`- Project name: ${projectName}`);
188+
console.log(`- Author: ${author}`);
189+
console.log(`- Description: (empty)`);
190+
} else {
191+
// Interactive mode: ask user for input
192+
projectName = await askQuestion('Project name?', gitInfo.repoName);
193+
author = await askQuestion('Your name/username?', gitInfo.author);
194+
description = await askQuestion('Project description (optional)?');
195+
proceed = await askQuestion('Ready to rebrand? (y/n)', 'y');
196+
}
197+
198+
// Exit if user doesn't want to proceed
67199
if (!['y', 'yes'].includes(proceed.toLowerCase())) {
68200
console.log('Cancelled. Run npm run dev again to retry.');
69201
process.exit(0);
70202
}
71203

72204
console.log('\n🔄 Updating files...');
73205

74-
// Replace text in files
75-
// Replace all occurrences of 'ZeroPoint' with the project name, with spaces removed
76-
replaceInFiles('ZeroPoint', projectName.replace(/\s+/g, ''));
77-
replaceInFiles('MWDelaney', author);
78-
replaceInFiles('zeropoint', projectName.toLowerCase().replace(/\\s+/g, '-'));
206+
// Perform text replacements across all project files (case-sensitive)
207+
replaceInFiles('ZeroPoint', projectName.replace(/\s+/g, '')); // Remove spaces from project name
208+
replaceInFiles('MWDelaney', author); // Replace default author
209+
replaceInFiles('zeropoint', projectName.toLowerCase().replace(/\s+/g, '-')); // Lowercase with hyphens
79210

80-
// Update package.json
211+
// Update package.json with new details
81212
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
82213
if (description) pkg.description = description;
83-
delete pkg.scripts['pre🪐']; // Remove the rebranding script
214+
delete pkg.scripts['pre🪐']; // Remove the rebranding script from package.json
84215
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
85216

86-
// Copy README files
217+
// Handle README files - backup current and use template version
87218
if (existsSync('README.md')) {
88-
execSync('cp README.md README.template.md');
219+
try {
220+
copyFileSync('README.md', 'README.template.md'); // Backup current README
221+
} catch (error) {
222+
console.warn(`Could not backup README.md: ${error.message}`);
223+
}
89224
}
90225
if (existsSync('README.ZeroPoint.md')) {
91-
execSync('cp README.ZeroPoint.md README.md');
226+
try {
227+
copyFileSync('README.ZeroPoint.md', 'README.md'); // Use ZeroPoint README as new README
228+
} catch (error) {
229+
console.warn(`Could not copy README.ZeroPoint.md: ${error.message}`);
230+
}
92231
}
93232

94-
// Clean up
233+
// Clean up template marker file
95234
unlinkSync('.template');
96235
console.log('✅ Rebranding complete! Starting development server...\n');
97236
}
98237

238+
// Run the rebranding process
99239
await rebrand();

.github/workflows/zeropoint-rebrand.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ jobs:
1616
node-version-file: '.nvmrc'
1717

1818
- name: 🏷️ Run rebranding script
19-
run: |
20-
# Run the rebranding script with automated inputs (just press enter for defaults and 'y' to proceed)
21-
echo -e "\n\n\ny" | node .github/scripts/zeropoint-rebrand.js
19+
run: node .github/scripts/zeropoint-rebrand.js
20+
env:
21+
CI: 'true' # This tells the script to run in automated mode
2222

2323
- name: 🚀 Commit changes and delete this workflow
2424
uses: EndBug/add-and-commit@v9

0 commit comments

Comments
 (0)