diff --git a/packages/rn-new/bin/rn-new.js b/packages/rn-new/bin/rn-new.js index 895d178d..0d1c84f0 100755 --- a/packages/rn-new/bin/rn-new.js +++ b/packages/rn-new/bin/rn-new.js @@ -1,9 +1,26 @@ #!/usr/bin/env node -try { - require('create-expo-stack/bin/create-expo-stack.js'); -} catch (error) { - console.error('Error: Could not find create-expo-stack package.'); - console.error('Please ensure create-expo-stack is installed globally.'); - process.exit(1); +const args = process.argv.slice(2); +const isAiCommand = args.includes('ai'); + +if (isAiCommand) { + // Remove 'ai' from args and pass remaining args to AI interface + const filteredArgs = args.filter((arg) => arg !== 'ai'); + process.argv = ['node', 'rn-new', ...filteredArgs]; + + try { + require('../lib/ai-interface.js'); + } catch (error) { + console.error('Error: Could not find AI interface module.'); + console.error('Please ensure create-expo-stack is installed properly.'); + process.exit(1); + } +} else { + try { + require('create-expo-stack/bin/create-expo-stack.js'); + } catch (error) { + console.error('Error: Could not find create-expo-stack package.'); + console.error('Please ensure create-expo-stack is installed globally.'); + process.exit(1); + } } diff --git a/packages/rn-new/lib/ai-interface.js b/packages/rn-new/lib/ai-interface.js new file mode 100644 index 00000000..960bf672 --- /dev/null +++ b/packages/rn-new/lib/ai-interface.js @@ -0,0 +1,239 @@ +#!/usr/bin/env node + +const readline = require('readline'); +const { spawn } = require('child_process'); + +class AIInterface { + constructor() { + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true + }); + } + + async showWelcome() { + console.log('\nšŸ¤– AI-Powered React Native Project Generator'); + console.log('━'.repeat(50)); + console.log("\nDescribe your project and I'll create it for you!"); + console.log('\nExample: "Create an Expo app with Nativewind and Supabase. I want file-based routing and tabs."\n'); + } + + async getMultilineInput() { + return new Promise((resolve) => { + console.log('šŸ“ Describe your project (press Enter to submit):'); + console.log(''); + process.stdout.write('> '); + + let input = ''; + let firstLine = true; + + const handleLine = (line) => { + if (firstLine && line.trim() === '') { + // Empty first line, continue waiting + process.stdout.write('> '); + return; + } + + if (firstLine) { + firstLine = false; + input = line; + this.rl.removeListener('line', handleLine); + resolve(input.trim()); + } + }; + + this.rl.on('line', handleLine); + }); + } + + parseNaturalLanguage(input) { + const options = {}; + const lowerInput = input.toLowerCase(); + + // Extract project name from input + const nameMatch = input.match(/(?:name(?:d)?|call(?:ed)?)\s+["']?([^"'\s,\.!?]+)["']?/i); + if (nameMatch) { + options.projectName = nameMatch[1]; + } + + // Styling libraries + if (lowerInput.includes('nativewind') || lowerInput.includes('native wind')) { + options.nativewind = true; + } else if (lowerInput.includes('tamagui')) { + options.tamagui = true; + } else if (lowerInput.includes('unistyles')) { + options.unistyles = true; + } else if (lowerInput.includes('restyle')) { + options.restyle = true; + } + + // Navigation + if ( + lowerInput.includes('expo router') || + lowerInput.includes('file-based routing') || + lowerInput.includes('file based routing') + ) { + options.expoRouter = true; + } else if (lowerInput.includes('react navigation') || lowerInput.includes('react-navigation')) { + options.reactNavigation = true; + } + + // Navigation types + if (lowerInput.includes('tabs') && !lowerInput.includes('drawer')) { + options.tabs = true; + } else if (lowerInput.includes('drawer') && lowerInput.includes('tabs')) { + options['drawer+tabs'] = true; + } + + // Authentication + if (lowerInput.includes('supabase')) { + options.supabase = true; + } else if (lowerInput.includes('firebase')) { + options.firebase = true; + } + + // State management + if (lowerInput.includes('zustand')) { + options.zustand = true; + } + + // Other features + if ( + lowerInput.includes('i18n') || + lowerInput.includes('internationalization') || + lowerInput.includes('translation') + ) { + options.i18next = true; + } + + return options; + } + + buildCommand(options, projectName) { + let command = 'npx'; + let args = ['create-expo-stack@latest']; + + // Add project name + if (projectName || options.projectName) { + args.push(projectName || options.projectName); + } + + // Add all the flags + Object.keys(options).forEach((key) => { + if (key !== 'projectName' && options[key]) { + args.push(`--${key}`); + } + }); + + return { command, args }; + } + + async executeCommand(command, args) { + console.log('\nšŸš€ Creating your project...'); + console.log(`Running: ${command} ${args.join(' ')}\n`); + + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: 'inherit', + shell: true + }); + + child.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + child.on('error', reject); + }); + } + + async run() { + try { + // Check if running in test mode with provided input + const testInput = process.env.AI_TEST_INPUT; + + if (testInput) { + // Test mode - don't show interactive interface + console.log('🧠 Analyzing test input:', testInput); + + const options = this.parseNaturalLanguage(testInput); + + // Show what was detected + console.log('\nšŸ“‹ Detected configuration:'); + if (options.projectName) console.log(` • Project name: ${options.projectName}`); + if (options.nativewind) console.log(' • Styling: NativeWind (Tailwind CSS)'); + if (options.tamagui) console.log(' • Styling: Tamagui'); + if (options.unistyles) console.log(' • Styling: Unistyles'); + if (options.restyle) console.log(' • Styling: Restyle'); + if (options.expoRouter) console.log(' • Navigation: Expo Router (file-based)'); + if (options.reactNavigation) console.log(' • Navigation: React Navigation'); + if (options.tabs) console.log(' • Navigation type: Tabs'); + if (options['drawer+tabs']) console.log(' • Navigation type: Drawer + Tabs'); + if (options.supabase) console.log(' • Authentication: Supabase'); + if (options.firebase) console.log(' • Authentication: Firebase'); + if (options.zustand) console.log(' • State management: Zustand'); + if (options.i18next) console.log(' • Internationalization: i18next'); + + const { command, args } = this.buildCommand(options); + console.log('\nšŸš€ Would execute:', command, args.join(' ')); + console.log('\nāœ… Test completed successfully! šŸŽ‰'); + + return; + } + + await this.showWelcome(); + + const input = await this.getMultilineInput(); + + if (!input.trim()) { + console.log('\nāŒ No input provided. Exiting...'); + process.exit(0); + } + + console.log('\n🧠 Analyzing your requirements...'); + + const options = this.parseNaturalLanguage(input); + + // Show what was detected + console.log('\nšŸ“‹ Detected configuration:'); + if (options.projectName) console.log(` • Project name: ${options.projectName}`); + if (options.nativewind) console.log(' • Styling: NativeWind (Tailwind CSS)'); + if (options.tamagui) console.log(' • Styling: Tamagui'); + if (options.unistyles) console.log(' • Styling: Unistyles'); + if (options.restyle) console.log(' • Styling: Restyle'); + if (options.expoRouter) console.log(' • Navigation: Expo Router (file-based)'); + if (options.reactNavigation) console.log(' • Navigation: React Navigation'); + if (options.tabs) console.log(' • Navigation type: Tabs'); + if (options['drawer+tabs']) console.log(' • Navigation type: Drawer + Tabs'); + if (options.supabase) console.log(' • Authentication: Supabase'); + if (options.firebase) console.log(' • Authentication: Firebase'); + if (options.zustand) console.log(' • State management: Zustand'); + if (options.i18next) console.log(' • Internationalization: i18next'); + + const { command, args } = this.buildCommand(options); + + await this.executeCommand(command, args); + + console.log('\nāœ… Project created successfully! šŸŽ‰'); + } catch (error) { + console.error('\nāŒ Error:', error.message); + process.exit(1); + } finally { + this.rl.close(); + } + } +} + +// Handle Ctrl+C gracefully +process.on('SIGINT', () => { + console.log('\n\nšŸ‘‹ Goodbye!'); + process.exit(0); +}); + +// Run the AI interface +const aiInterface = new AIInterface(); +aiInterface.run(); diff --git a/packages/rn-new/package.json b/packages/rn-new/package.json index 10809e3a..6ccfa147 100644 --- a/packages/rn-new/package.json +++ b/packages/rn-new/package.json @@ -11,12 +11,14 @@ "license": "MIT", "bin": "./bin/rn-new.js", "files": [ - "bin" + "bin", + "lib" ], "scripts": { - "build": "bun run clean && bun run compile", - "clean": "rm -rf ./bin", - "compile": "mkdir -p bin && printf '#!/usr/bin/env node\n\ntry {\n require(\"create-expo-stack/bin/create-expo-stack.js\");\n} catch (error) {\n console.error(\"Error: Could not find create-expo-stack package.\");\n console.error(\"Please ensure create-expo-stack is installed globally.\");\n process.exit(1);\n}\n' > bin/rn-new.js && chmod +x bin/rn-new.js", + "build": "bun run clean && bun run compile && bun run copy-ai-interface", + "clean": "rm -rf ./bin ./lib", + "compile": "mkdir -p bin && cp src/rn-new.js bin/rn-new.js && chmod +x bin/rn-new.js", + "copy-ai-interface": "mkdir -p lib && cp src/ai-interface.js lib/ai-interface.js", "prepublishOnly": "bun run build" }, "dependencies": { diff --git a/packages/rn-new/src/ai-interface.js b/packages/rn-new/src/ai-interface.js new file mode 100644 index 00000000..960bf672 --- /dev/null +++ b/packages/rn-new/src/ai-interface.js @@ -0,0 +1,239 @@ +#!/usr/bin/env node + +const readline = require('readline'); +const { spawn } = require('child_process'); + +class AIInterface { + constructor() { + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true + }); + } + + async showWelcome() { + console.log('\nšŸ¤– AI-Powered React Native Project Generator'); + console.log('━'.repeat(50)); + console.log("\nDescribe your project and I'll create it for you!"); + console.log('\nExample: "Create an Expo app with Nativewind and Supabase. I want file-based routing and tabs."\n'); + } + + async getMultilineInput() { + return new Promise((resolve) => { + console.log('šŸ“ Describe your project (press Enter to submit):'); + console.log(''); + process.stdout.write('> '); + + let input = ''; + let firstLine = true; + + const handleLine = (line) => { + if (firstLine && line.trim() === '') { + // Empty first line, continue waiting + process.stdout.write('> '); + return; + } + + if (firstLine) { + firstLine = false; + input = line; + this.rl.removeListener('line', handleLine); + resolve(input.trim()); + } + }; + + this.rl.on('line', handleLine); + }); + } + + parseNaturalLanguage(input) { + const options = {}; + const lowerInput = input.toLowerCase(); + + // Extract project name from input + const nameMatch = input.match(/(?:name(?:d)?|call(?:ed)?)\s+["']?([^"'\s,\.!?]+)["']?/i); + if (nameMatch) { + options.projectName = nameMatch[1]; + } + + // Styling libraries + if (lowerInput.includes('nativewind') || lowerInput.includes('native wind')) { + options.nativewind = true; + } else if (lowerInput.includes('tamagui')) { + options.tamagui = true; + } else if (lowerInput.includes('unistyles')) { + options.unistyles = true; + } else if (lowerInput.includes('restyle')) { + options.restyle = true; + } + + // Navigation + if ( + lowerInput.includes('expo router') || + lowerInput.includes('file-based routing') || + lowerInput.includes('file based routing') + ) { + options.expoRouter = true; + } else if (lowerInput.includes('react navigation') || lowerInput.includes('react-navigation')) { + options.reactNavigation = true; + } + + // Navigation types + if (lowerInput.includes('tabs') && !lowerInput.includes('drawer')) { + options.tabs = true; + } else if (lowerInput.includes('drawer') && lowerInput.includes('tabs')) { + options['drawer+tabs'] = true; + } + + // Authentication + if (lowerInput.includes('supabase')) { + options.supabase = true; + } else if (lowerInput.includes('firebase')) { + options.firebase = true; + } + + // State management + if (lowerInput.includes('zustand')) { + options.zustand = true; + } + + // Other features + if ( + lowerInput.includes('i18n') || + lowerInput.includes('internationalization') || + lowerInput.includes('translation') + ) { + options.i18next = true; + } + + return options; + } + + buildCommand(options, projectName) { + let command = 'npx'; + let args = ['create-expo-stack@latest']; + + // Add project name + if (projectName || options.projectName) { + args.push(projectName || options.projectName); + } + + // Add all the flags + Object.keys(options).forEach((key) => { + if (key !== 'projectName' && options[key]) { + args.push(`--${key}`); + } + }); + + return { command, args }; + } + + async executeCommand(command, args) { + console.log('\nšŸš€ Creating your project...'); + console.log(`Running: ${command} ${args.join(' ')}\n`); + + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: 'inherit', + shell: true + }); + + child.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + child.on('error', reject); + }); + } + + async run() { + try { + // Check if running in test mode with provided input + const testInput = process.env.AI_TEST_INPUT; + + if (testInput) { + // Test mode - don't show interactive interface + console.log('🧠 Analyzing test input:', testInput); + + const options = this.parseNaturalLanguage(testInput); + + // Show what was detected + console.log('\nšŸ“‹ Detected configuration:'); + if (options.projectName) console.log(` • Project name: ${options.projectName}`); + if (options.nativewind) console.log(' • Styling: NativeWind (Tailwind CSS)'); + if (options.tamagui) console.log(' • Styling: Tamagui'); + if (options.unistyles) console.log(' • Styling: Unistyles'); + if (options.restyle) console.log(' • Styling: Restyle'); + if (options.expoRouter) console.log(' • Navigation: Expo Router (file-based)'); + if (options.reactNavigation) console.log(' • Navigation: React Navigation'); + if (options.tabs) console.log(' • Navigation type: Tabs'); + if (options['drawer+tabs']) console.log(' • Navigation type: Drawer + Tabs'); + if (options.supabase) console.log(' • Authentication: Supabase'); + if (options.firebase) console.log(' • Authentication: Firebase'); + if (options.zustand) console.log(' • State management: Zustand'); + if (options.i18next) console.log(' • Internationalization: i18next'); + + const { command, args } = this.buildCommand(options); + console.log('\nšŸš€ Would execute:', command, args.join(' ')); + console.log('\nāœ… Test completed successfully! šŸŽ‰'); + + return; + } + + await this.showWelcome(); + + const input = await this.getMultilineInput(); + + if (!input.trim()) { + console.log('\nāŒ No input provided. Exiting...'); + process.exit(0); + } + + console.log('\n🧠 Analyzing your requirements...'); + + const options = this.parseNaturalLanguage(input); + + // Show what was detected + console.log('\nšŸ“‹ Detected configuration:'); + if (options.projectName) console.log(` • Project name: ${options.projectName}`); + if (options.nativewind) console.log(' • Styling: NativeWind (Tailwind CSS)'); + if (options.tamagui) console.log(' • Styling: Tamagui'); + if (options.unistyles) console.log(' • Styling: Unistyles'); + if (options.restyle) console.log(' • Styling: Restyle'); + if (options.expoRouter) console.log(' • Navigation: Expo Router (file-based)'); + if (options.reactNavigation) console.log(' • Navigation: React Navigation'); + if (options.tabs) console.log(' • Navigation type: Tabs'); + if (options['drawer+tabs']) console.log(' • Navigation type: Drawer + Tabs'); + if (options.supabase) console.log(' • Authentication: Supabase'); + if (options.firebase) console.log(' • Authentication: Firebase'); + if (options.zustand) console.log(' • State management: Zustand'); + if (options.i18next) console.log(' • Internationalization: i18next'); + + const { command, args } = this.buildCommand(options); + + await this.executeCommand(command, args); + + console.log('\nāœ… Project created successfully! šŸŽ‰'); + } catch (error) { + console.error('\nāŒ Error:', error.message); + process.exit(1); + } finally { + this.rl.close(); + } + } +} + +// Handle Ctrl+C gracefully +process.on('SIGINT', () => { + console.log('\n\nšŸ‘‹ Goodbye!'); + process.exit(0); +}); + +// Run the AI interface +const aiInterface = new AIInterface(); +aiInterface.run(); diff --git a/packages/rn-new/src/rn-new.js b/packages/rn-new/src/rn-new.js new file mode 100755 index 00000000..0d1c84f0 --- /dev/null +++ b/packages/rn-new/src/rn-new.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const args = process.argv.slice(2); +const isAiCommand = args.includes('ai'); + +if (isAiCommand) { + // Remove 'ai' from args and pass remaining args to AI interface + const filteredArgs = args.filter((arg) => arg !== 'ai'); + process.argv = ['node', 'rn-new', ...filteredArgs]; + + try { + require('../lib/ai-interface.js'); + } catch (error) { + console.error('Error: Could not find AI interface module.'); + console.error('Please ensure create-expo-stack is installed properly.'); + process.exit(1); + } +} else { + try { + require('create-expo-stack/bin/create-expo-stack.js'); + } catch (error) { + console.error('Error: Could not find create-expo-stack package.'); + console.error('Please ensure create-expo-stack is installed globally.'); + process.exit(1); + } +}