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' ;
77import { execSync } from 'child_process' ;
88import { 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+ */
1084async 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+ */
21100function 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 ( / [: / ] ( [ ^ / ] + ) \/ ( [ ^ / ] + ?) (?: \. g i t ) ? $ / ) ;
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+ */
36124function 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+ */
45149async 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
99239await rebrand ( ) ;
0 commit comments