const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const { exec, spawn } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const multer = require('multer');
const AdmZip = require('adm-zip');
const archiver = require('archiver');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const upload = multer({ dest: 'uploads/' });
// Store active builds and terminals
const activeBuilds = new Map();
const activeTerminals = new Map();
// Middleware
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
// Serve all static files from root
app.use(express.static(__dirname));
// ==================== API Routes ====================
// 1. Home page
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// 2. Get all projects
app.get('/api/projects', async (req, res) => {
try {
const projects = await fs.readdir('/workspace');
res.json({ projects });
} catch (error) {
res.json({ projects: [] });
}
});
// 3. Create new project
app.post('/api/project/create', async (req, res) => {
const { name, template = 'empty' } = req.body;
const projectPath = path.join('/workspace', name);
try {
await fs.mkdir(projectPath, { recursive: true });
await createAndroidProject(projectPath, name);
res.json({ success: true, message: 'Project created', path: projectPath });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 4. Get project files
app.get('/api/files/:project/*', async (req, res) => {
const { project, '0': filePath } = req.params;
const fullPath = path.join('/workspace', project, filePath || '');
try {
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
const files = await fs.readdir(fullPath);
res.json({ type: 'directory', files });
} else {
const content = await fs.readFile(fullPath, 'utf-8');
res.json({ type: 'file', content });
}
} catch (error) {
res.status(404).json({ error: 'Not found' });
}
});
// 5. Save file
app.post('/api/file/:project/*', async (req, res) => {
const { project, '0': filePath } = req.params;
const { content } = req.body;
const fullPath = path.join('/workspace', project, filePath);
try {
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 6. Build project
app.post('/api/build/:project', (req, res) => {
const { project } = req.params;
const projectPath = path.join('/workspace', project);
const buildId = Date.now().toString();
const buildProcess = spawn('./gradlew', ['assembleDebug'], {
cwd: projectPath,
env: {
...process.env,
ANDROID_HOME: '/android-sdk',
JAVA_HOME: '/usr/lib/jvm/java-17-openjdk-amd64'
}
});
let output = '';
buildProcess.stdout.on('data', (data) => {
output += data.toString();
});
buildProcess.stderr.on('data', (data) => {
output += data.toString();
});
buildProcess.on('close', (code) => {
const success = code === 0;
const apkPath = path.join(projectPath, 'app/build/outputs/apk/debug/app-debug.apk');
activeBuilds.set(buildId, {
success,
output,
apkPath: success ? apkPath : null,
project
});
res.json({
buildId,
success,
output,
message: success ? 'Build successful!' : 'Build failed'
});
});
});
// 7. Get build status
app.get('/api/build/:buildId', (req, res) => {
const { buildId } = req.params;
const build = activeBuilds.get(buildId);
if (build) {
res.json(build);
} else {
res.status(404).json({ error: 'Build not found' });
}
});
// 8. Download APK
app.get('/api/apk/:project', async (req, res) => {
const { project } = req.params;
const apkPath = path.join('/workspace', project, 'app/build/outputs/apk/debug/app-debug.apk');
try {
await fs.access(apkPath);
res.download(apkPath, `${project}.apk`);
} catch (error) {
res.status(404).json({ error: 'APK not found. Build first!' });
}
});
// 9. Upload logo
app.post('/api/logo/upload/:project', upload.single('logo'), async (req, res) => {
const { project } = req.params;
const logoPath = req.file.path;
try {
// Convert to Android icon sizes and copy to project
const resDir = path.join('/workspace', project, 'app/src/main/res');
// Create mipmap directories
const sizes = {
'mipmap-hdpi': 72,
'mipmap-mdpi': 48,
'mipmap-xhdpi': 96,
'mipmap-xxhdpi': 144,
'mipmap-xxxhdpi': 192
};
for (const [dir, size] of Object.entries(sizes)) {
const destDir = path.join(resDir, dir);
await fs.mkdir(destDir, { recursive: true });
// Copy and rename to ic_launcher.png
const destPath = path.join(destDir, 'ic_launcher.png');
await fs.copyFile(logoPath, destPath);
}
res.json({ success: true, message: 'Logo updated successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 10. Terminal WebSocket
wss.on('connection', (ws) => {
console.log('Terminal connected');
const { spawn } = require('node-pty');
const shell = spawn('bash', [], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: '/workspace',
env: process.env
});
const terminalId = Date.now().toString();
activeTerminals.set(terminalId, shell);
shell.on('data', (data) => {
ws.send(JSON.stringify({ type: 'data', data: data.toString() }));
});
ws.on('message', (msg) => {
try {
const { type, data } = JSON.parse(msg);
if (type === 'input') {
shell.write(data);
} else if (type === 'resize') {
shell.resize(data.cols, data.rows);
}
} catch (e) {
console.error('Terminal error:', e);
}
});
ws.on('close', () => {
shell.kill();
activeTerminals.delete(terminalId);
});
});
// ==================== Helper Functions ====================
async function createAndroidProject(projectPath, name) {
const packageName = `com.${name.toLowerCase().replace(/[^a-z]/g, '')}.app`;
// Create directory structure
const mainPath = path.join(projectPath, 'app/src/main');
const javaPath = path.join(mainPath, 'java', ...packageName.split('.'));
const resPath = path.join(mainPath, 'res');
const layoutPath = path.join(resPath, 'layout');
const drawablePath = path.join(resPath, 'drawable');
const valuesPath = path.join(resPath, 'values');
const mipmapPath = path.join(resPath, 'mipmap-hdpi');
await fs.mkdir(javaPath, { recursive: true });
await fs.mkdir(layoutPath, { recursive: true });
await fs.mkdir(drawablePath, { recursive: true });
await fs.mkdir(valuesPath, { recursive: true });
await fs.mkdir(mipmapPath, { recursive: true });
// 1. settings.gradle
await fs.writeFile(path.join(projectPath, 'settings.gradle'),
`rootProject.name = "${name}"\ninclude ':app'`);
// 2. build.gradle (project)
await fs.writeFile(path.join(projectPath, 'build.gradle'), `
// Top-level build file
buildscript {
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
`);
// 3. gradle.properties
await fs.writeFile(path.join(projectPath, 'gradle.properties'), `
org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=true
kotlin.code.style=official
`);
// 4. gradlew wrapper
const gradlew = `#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
$DIR/gradlew "$@"
`;
await fs.writeFile(path.join(projectPath, 'gradlew'), gradlew);
await fs.chmod(path.join(projectPath, 'gradlew'), 0o755);
// 5. app/build.gradle
await fs.writeFile(path.join(projectPath, 'app/build.gradle'), `
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 34
defaultConfig {
applicationId "${packageName}"
minSdk 29
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
`);
// 6. AndroidManifest.xml
await fs.writeFile(path.join(mainPath, 'AndroidManifest.xml'), `
`);
// 7. MainActivity.kt
await fs.writeFile(path.join(javaPath, 'MainActivity.kt'), `
package ${packageName}
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Your app starts here
supportActionBar?.title = "${name}"
}
}
`);
// 8. activity_main.xml
await fs.writeFile(path.join(layoutPath, 'activity_main.xml'), `
`);
// 9. strings.xml
await fs.writeFile(path.join(valuesPath, 'strings.xml'), `
${name}
`);
// 10. colors.xml
await fs.writeFile(path.join(valuesPath, 'colors.xml'), `
#FFBB86FC
#FF6200EE
#FF3700B3
#FF03DAC5
#FF018786
#FF000000
#FFFFFFFF
`);
// 11. themes.xml
await fs.writeFile(path.join(valuesPath, 'themes.xml'), `
`);
}
// Start server
server.listen(7860, () => {
console.log('🚀 Server running on http://localhost:7860');
console.log('📱 Android 10 Preview Ready');
console.log('🔧 Build System Active');
});