| 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/' }); |
|
|
| |
| const activeBuilds = new Map(); |
| const activeTerminals = new Map(); |
|
|
| |
| app.use(express.json({ limit: '50mb' })); |
| app.use(express.urlencoded({ extended: true, limit: '50mb' })); |
|
|
| |
| app.use(express.static(__dirname)); |
|
|
| |
|
|
| |
| app.get('/', (req, res) => { |
| res.sendFile(path.join(__dirname, 'index.html')); |
| }); |
|
|
| |
| app.get('/api/projects', async (req, res) => { |
| try { |
| const projects = await fs.readdir('/workspace'); |
| res.json({ projects }); |
| } catch (error) { |
| res.json({ projects: [] }); |
| } |
| }); |
|
|
| |
| 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 }); |
| } |
| }); |
|
|
| |
| 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' }); |
| } |
| }); |
|
|
| |
| 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 }); |
| } |
| }); |
|
|
| |
| 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' |
| }); |
| }); |
| }); |
|
|
| |
| 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' }); |
| } |
| }); |
|
|
| |
| 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!' }); |
| } |
| }); |
|
|
| |
| app.post('/api/logo/upload/:project', upload.single('logo'), async (req, res) => { |
| const { project } = req.params; |
| const logoPath = req.file.path; |
| |
| try { |
| |
| const resDir = path.join('/workspace', project, 'app/src/main/res'); |
| |
| |
| 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 }); |
| |
| |
| 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 }); |
| } |
| }); |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
|
|
| async function createAndroidProject(projectPath, name) { |
| const packageName = `com.${name.toLowerCase().replace(/[^a-z]/g, '')}.app`; |
| |
| |
| 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 }); |
|
|
| |
| await fs.writeFile(path.join(projectPath, 'settings.gradle'), |
| `rootProject.name = "${name}"\ninclude ':app'`); |
|
|
| |
| 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 |
| } |
| `); |
|
|
| |
| await fs.writeFile(path.join(projectPath, 'gradle.properties'), ` |
| org.gradle.jvmargs=-Xmx2048m |
| android.useAndroidX=true |
| kotlin.code.style=official |
| `); |
|
|
| |
| 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); |
|
|
| |
| 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' |
| } |
| `); |
|
|
| |
| await fs.writeFile(path.join(mainPath, 'AndroidManifest.xml'), ` |
| <?xml version="1.0" encoding="utf-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| package="${packageName}"> |
| |
| <application |
| android:allowBackup="true" |
| android:icon="@mipmap/ic_launcher" |
| android:label="${name}" |
| android:theme="@style/Theme.${name}"> |
| <activity |
| android:name=".MainActivity" |
| android:exported="true"> |
| <intent-filter> |
| <action android:name="android.intent.action.MAIN" /> |
| <category android:name="android.intent.category.LAUNCHER" /> |
| </intent-filter> |
| </activity> |
| </application> |
| |
| </manifest> |
| `); |
|
|
| |
| 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}" |
| } |
| } |
| `); |
|
|
| |
| await fs.writeFile(path.join(layoutPath, 'activity_main.xml'), ` |
| <?xml version="1.0" encoding="utf-8"?> |
| <LinearLayout |
| xmlns:android="http://schemas.android.com/apk/res/android" |
| android:layout_width="match_parent" |
| android:layout_height="match_parent" |
| android:orientation="vertical" |
| android:gravity="center" |
| android:padding="20dp"> |
| |
| <ImageView |
| android:id="@+id/app_logo" |
| android:layout_width="120dp" |
| android:layout_height="120dp" |
| android:src="@drawable/ic_launcher_foreground" |
| android:layout_marginBottom="20dp" /> |
| |
| <TextView |
| android:layout_width="wrap_content" |
| android:layout_height="wrap_content" |
| android:text="${name}" |
| android:textSize="28sp" |
| android:textStyle="bold" |
| android:textColor="#3DDC84" |
| android:layout_marginBottom="8dp" /> |
| |
| <TextView |
| android:layout_width="wrap_content" |
| android:layout_height="wrap_content" |
| android:text="Android 10 (API 29)" |
| android:textSize="16sp" |
| android:textColor="#666666" |
| android:layout_marginBottom="24dp" /> |
| |
| <TextView |
| android:layout_width="wrap_content" |
| android:layout_height="wrap_content" |
| android:text="Welcome to your Android app!" |
| android:textSize="14sp" |
| android:textColor="#333333" /> |
| |
| </LinearLayout> |
| `); |
|
|
| |
| await fs.writeFile(path.join(valuesPath, 'strings.xml'), ` |
| <resources> |
| <string name="app_name">${name}</string> |
| </resources> |
| `); |
|
|
| |
| await fs.writeFile(path.join(valuesPath, 'colors.xml'), ` |
| <?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <color name="purple_200">#FFBB86FC</color> |
| <color name="purple_500">#FF6200EE</color> |
| <color name="purple_700">#FF3700B3</color> |
| <color name="teal_200">#FF03DAC5</color> |
| <color name="teal_700">#FF018786</color> |
| <color name="black">#FF000000</color> |
| <color name="white">#FFFFFFFF</color> |
| </resources> |
| `); |
|
|
| |
| await fs.writeFile(path.join(valuesPath, 'themes.xml'), ` |
| <resources xmlns:tools="http://schemas.android.com/tools"> |
| <style name="Theme.${name}" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> |
| <item name="colorPrimary">@color/purple_500</item> |
| <item name="colorPrimaryVariant">@color/purple_700</item> |
| <item name="colorOnPrimary">@color/white</item> |
| <item name="colorSecondary">@color/teal_200</item> |
| <item name="colorSecondaryVariant">@color/teal_700</item> |
| <item name="colorOnSecondary">@color/black</item> |
| <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> |
| </style> |
| </resources> |
| `); |
| } |
|
|
| |
| server.listen(7860, () => { |
| console.log('๐ Server running on http://localhost:7860'); |
| console.log('๐ฑ Android 10 Preview Ready'); |
| console.log('๐ง Build System Active'); |
| }); |