Skip to content

feat: add ai command #531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions packages/rn-new/bin/rn-new.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
239 changes: 239 additions & 0 deletions packages/rn-new/lib/ai-interface.js
Original file line number Diff line number Diff line change
@@ -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();
10 changes: 6 additions & 4 deletions packages/rn-new/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading