v2.0.0: Built-in Termux — full Linux environment, no external app needed

This commit is contained in:
admin
2026-05-19 17:37:40 +04:00
Unverified
parent a9f53e45dd
commit e7015b129a
8 changed files with 504 additions and 100 deletions

View File

@@ -631,6 +631,16 @@ data: [DONE]
## Changelog
### v2.0.0 (2026-05-19)
- **Built-in Termux** — full Linux environment inside the app, no external Termux install needed
- One-time ~30MB download of Termux bootstrap (bash, coreutils, apt, 25+ packages)
- Auto-detects CPU architecture (arm64-v8a, armeabi-v7a, x86, x86_64)
- Path patching: fixes all `/data/data/com.termux` references to work from app prefix
- `BootstrapPlugin` — native download, ZIP extraction, symlink creation, path patching
- `ShellPlugin` upgraded — uses bundled `bash` instead of limited `/system/bin/sh`
- Install build tools: `pkg install aapt2 ecj dx apksigner`
- APK stays ~1MB — bootstrap downloaded on first use, never embedded
### v1.4.0 (2026-05-19)
- **Agentic Feedback Loop** — build errors auto-sent back to AI for fixing (up to 3 retries)
- **Termux Integration** — auto-detects Termux, uses its `PATH` (aapt2, d8, ecj, tsu/su)

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 9
versionName "1.4.0"
versionCode 10
versionName "2.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -0,0 +1,406 @@
package ai.z.chat;
import android.content.Context;
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@CapacitorPlugin(name = "Bootstrap")
public class BootstrapPlugin extends Plugin {
private static final String TAG = "BootstrapPlugin";
private static final String BOOTSTRAP_URL_AARCH64 =
"https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-aarch64.zip";
private static final String BOOTSTRAP_URL_ARM =
"https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-arm.zip";
private static final String BOOTSTRAP_URL_X86_64 =
"https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-x86_64.zip";
private static final String BOOTSTRAP_URL_X86 =
"https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-i686.zip";
private static final String TERMUX_PREFIX = "/data/data/com.termux/files/usr";
private static final int BUFFER_SIZE = 8192;
private String filesDir;
private String prefixDir;
private String stagingDir;
private String homeDir;
private String binDir;
private boolean isInstalling = false;
@Override
public void load() {
filesDir = getContext().getFilesDir().getAbsolutePath();
prefixDir = filesDir + "/usr";
stagingDir = filesDir + "/usr-staging";
homeDir = filesDir + "/home";
binDir = prefixDir + "/bin";
}
@PluginMethod
public void getStatus(PluginCall call) {
JSObject status = new JSObject();
boolean installed = new File(binDir + "/bash").exists() || new File(binDir + "/sh").exists();
status.put("installed", installed);
status.put("prefixDir", prefixDir);
status.put("binDir", binDir);
status.put("homeDir", homeDir);
status.put("arch", getArch());
status.put("isInstalling", isInstalling);
if (installed) {
status.put("shellPath", new File(binDir + "/bash").exists() ? binDir + "/bash" : binDir + "/sh");
}
call.resolve(status);
}
@PluginMethod
public void install(PluginCall call) {
if (isInstalling) {
call.reject("Installation already in progress");
return;
}
boolean installed = new File(binDir + "/bash").exists();
if (installed) {
call.resolve(new JSObject().put("installed", true).put("message", "Already installed"));
return;
}
call.setKeepAlive(true);
isInstalling = true;
new Thread(() -> {
try {
doInstall(call);
} catch (Exception e) {
Log.e(TAG, "Install failed", e);
isInstalling = false;
try {
call.reject("Install failed: " + e.getMessage());
} catch (Exception ignored) {}
}
}).start();
}
private void doInstall(PluginCall call) throws Exception {
String arch = getArch();
String bootstrapUrl = getBootstrapUrl(arch);
sendProgress(call, "Downloading bootstrap for " + arch + "...", 0);
File zipFile = new File(getContext().getCacheDir(), "bootstrap.zip");
downloadFile(bootstrapUrl, zipFile, (downloaded, total) -> {
int percent = total > 0 ? (int)(downloaded * 100 / total) : 0;
String sizeMB = String.format("%.1f", downloaded / (1024.0 * 1024.0));
sendProgress(call, "Downloading... " + sizeMB + " MB (" + percent + "%)", percent / 3);
});
sendProgress(call, "Extracting bootstrap...", 35);
new File(stagingDir).mkdirs();
List<String[]> symlinks = extractBootstrap(zipFile, stagingDir, (extracted, total) -> {
int percent = 35 + (int)(extracted * 30 / Math.max(total, 1));
sendProgress(call, "Extracting... " + extracted + "/" + total + " files", percent);
});
sendProgress(call, "Creating symlinks (" + symlinks.size() + ")...", 68);
createSymlinks(symlinks, stagingDir);
sendProgress(call, "Patching paths...", 75);
patchPaths(stagingDir);
sendProgress(call, "Setting permissions...", 85);
setPermissions(new File(stagingDir, "bin"));
setPermissions(new File(stagingDir, "libexec"));
new File(homeDir).mkdirs();
new File(prefixDir + "/tmp").mkdirs();
sendProgress(call, "Finalizing...", 92);
File staging = new File(stagingDir);
File prefix = new File(prefixDir);
if (prefix.exists()) {
deleteRecursive(prefix);
}
boolean renamed = staging.renameTo(prefix);
if (!renamed) {
throw new RuntimeException("Failed to rename staging to prefix");
}
writeEnvFile();
writeProfileFile();
zipFile.delete();
sendProgress(call, "Termux environment ready!", 100);
isInstalling = false;
JSObject result = new JSObject();
result.put("installed", true);
result.put("prefixDir", prefixDir);
result.put("shellPath", binDir + "/bash");
result.put("message", "Bootstrap installed successfully");
call.resolve(result);
}
private void downloadFile(String urlStr, File outputFile, ProgressCallback callback) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(60000);
conn.setRequestProperty("Accept-Language", "en-US,en");
conn.connect();
int total = conn.getContentLength();
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
FileOutputStream out = new FileOutputStream(outputFile);
byte[] buffer = new byte[BUFFER_SIZE];
long downloaded = 0;
int read;
long lastNotify = 0;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
downloaded += read;
long now = System.currentTimeMillis();
if (callback != null && now - lastNotify > 500) {
callback.onProgress(downloaded, total);
lastNotify = now;
}
}
out.flush();
out.close();
in.close();
conn.disconnect();
}
private List<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
List<String[]> symlinks = new ArrayList<>();
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry entry;
int extracted = 0;
int total = 0;
java.util.Enumeration<java.util.zip.ZipEntry> entries = java.util.Collections.emptyEnumeration();
java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile);
total = zf.size();
zf.close();
zis = new ZipInputStream(new FileInputStream(zipFile));
while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
if (name.equals("SYMLINKS.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(zis));
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\u2190");
if (parts.length == 2) {
String target = parts[0].replace(TERMUX_PREFIX, destDir);
String linkPath = destDir + "/" + parts[1];
symlinks.add(new String[]{target, linkPath});
}
}
} else {
File outFile = new File(destDir, name);
if (entry.isDirectory()) {
outFile.mkdirs();
} else {
File parent = outFile.getParentFile();
if (parent != null) parent.mkdirs();
FileOutputStream fos = new FileOutputStream(outFile);
byte[] buf = new byte[BUFFER_SIZE];
int len;
while ((len = zis.read(buf)) > 0) {
fos.write(buf, 0, len);
}
fos.close();
}
}
extracted++;
if (callback != null && extracted % 50 == 0) {
callback.onProgress(extracted, total);
}
zis.closeEntry();
}
zis.close();
return symlinks;
}
private void createSymlinks(List<String[]> symlinks, String stagingDir) {
for (String[] link : symlinks) {
try {
String target = link[0];
String linkPath = link[1];
File linkFile = new File(linkPath);
File parent = linkFile.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
if (linkFile.exists()) linkFile.delete();
android.system.Os.symlink(target, linkPath);
} catch (Exception e) {
Log.w(TAG, "Symlink failed: " + link[1] + " -> " + link[0] + ": " + e.getMessage());
}
}
}
private void patchPaths(String dir) {
try {
ProcessBuilder pb = new ProcessBuilder("find", dir, "-type", "f",
"(", "-name", "*.sh", "-o", "-name", "*.conf", "-o", "-name", "*.cfg",
"-o", "-name", "*.txt", "-o", "-name", "*.env", "-o", "-name", "properties.sh",
"-o", "-name", "profile", "-o", "-name", "bashrc", "-o", "-name", "*.profile", ")");
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
List<String> files = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
files.add(line);
}
p.waitFor();
String ourPrefix = filesDir + "/usr";
for (String filePath : files) {
try {
ProcessBuilder sedPb = new ProcessBuilder("sed", "-i",
"s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" +
"s|/data/data/com.termux/files/home|" + homeDir + "|g;" +
"s|/data/data/com.termux|" + filesDir + "|g",
filePath);
sedPb.start().waitFor();
} catch (Exception e) {
Log.w(TAG, "Patch failed for " + filePath + ": " + e.getMessage());
}
}
ProcessBuilder ldSo = new ProcessBuilder("sed", "-i",
"s|/data/data/com.termux/files/usr|" + ourPrefix + "|g",
dir + "/etc/ld.so.conf");
ldSo.start().waitFor();
} catch (Exception e) {
Log.w(TAG, "Path patching error: " + e.getMessage());
}
}
private void setPermissions(File dir) {
if (!dir.exists()) return;
File[] children = dir.listFiles();
if (children == null) return;
for (File f : children) {
if (f.isFile()) {
f.setExecutable(true, false);
f.setReadable(true, false);
}
}
}
private void writeEnvFile() {
try {
File envFile = new File(prefixDir + "/etc/termux.env");
envFile.getParentFile().mkdirs();
java.io.FileWriter writer = new java.io.FileWriter(envFile);
writer.write("HOME=" + homeDir + "\n");
writer.write("PREFIX=" + prefixDir + "\n");
writer.write("PATH=" + binDir + "\n");
writer.write("TMPDIR=" + prefixDir + "/tmp\n");
writer.write("TERM=xterm-256color\n");
writer.write("LANG=en_US.UTF-8\n");
writer.write("BOOTSTRAP=zaichat\n");
writer.close();
} catch (Exception e) {
Log.w(TAG, "Failed to write env file: " + e.getMessage());
}
}
private void writeProfileFile() {
try {
File profileDir = new File(prefixDir + "/etc/profile.d");
profileDir.mkdirs();
File profile = new File(profileDir, "zai-chat.sh");
java.io.FileWriter writer = new java.io.FileWriter(profile);
writer.write("export HOME=" + homeDir + "\n");
writer.write("export PREFIX=" + prefixDir + "\n");
writer.write("export PATH=" + binDir + "\n");
writer.write("export TMPDIR=" + prefixDir + "/tmp\n");
writer.write("export TERM=xterm-256color\n");
writer.write("export LANG=en_US.UTF-8\n");
writer.write("export ANDROID_HOME=" + filesDir + "/tools\n");
writer.write("export PROJECTS=" + filesDir + "/projects\n");
writer.write("export PS1='\\$ '\n");
writer.close();
profile.setExecutable(true, false);
} catch (Exception e) {
Log.w(TAG, "Failed to write profile: " + e.getMessage());
}
}
private void sendProgress(PluginCall call, String message, int percent) {
JSObject event = new JSObject();
event.put("message", message);
event.put("percent", percent);
notifyListeners("bootstrap-progress", event);
}
private String getArch() {
String abi = android.os.Build.SUPPORTED_ABIS[0];
switch (abi) {
case "arm64-v8a": return "aarch64";
case "armeabi-v7a": return "arm";
case "x86_64": return "x86_64";
case "x86": return "i686";
default: return "aarch64";
}
}
private String getBootstrapUrl(String arch) {
switch (arch) {
case "aarch64": return BOOTSTRAP_URL_AARCH64;
case "arm": return BOOTSTRAP_URL_ARM;
case "x86_64": return BOOTSTRAP_URL_X86_64;
case "i686": return BOOTSTRAP_URL_X86;
default: return BOOTSTRAP_URL_AARCH64;
}
}
private void deleteRecursive(File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursive(child);
}
}
}
file.delete();
}
interface ProgressCallback {
void onProgress(long downloaded, long total);
}
interface ExtractCallback {
void onProgress(int extracted, int total);
}
}

View File

@@ -10,6 +10,7 @@ public class MainActivity extends BridgeActivity {
registerPlugin(ShellPlugin.class);
registerPlugin(InstallerPlugin.class);
registerPlugin(WakePlugin.class);
registerPlugin(BootstrapPlugin.class);
super.onCreate(savedInstanceState);
}
}

View File

@@ -29,16 +29,30 @@ public class ShellPlugin extends Plugin {
private String homeDir = null;
private String toolsDir = null;
private String projectsDir = null;
private String prefixDir = null;
private String shellPath = null;
@Override
public void load() {
homeDir = getContext().getFilesDir().getAbsolutePath();
toolsDir = homeDir + "/tools";
projectsDir = homeDir + "/projects";
prefixDir = homeDir + "/usr";
currentCwd = homeDir;
new File(toolsDir).mkdirs();
new File(projectsDir).mkdirs();
new File(homeDir + "/bin").mkdirs();
new File(homeDir + "/tmp").mkdirs();
File bash = new File(prefixDir + "/bin/bash");
File sh = new File(prefixDir + "/bin/sh");
if (bash.exists()) {
shellPath = bash.getAbsolutePath();
} else if (sh.exists()) {
shellPath = sh.getAbsolutePath();
} else {
shellPath = "/system/bin/sh";
}
}
@PluginMethod
@@ -57,7 +71,8 @@ public class ShellPlugin extends Plugin {
try {
String[] env = buildEnv();
ProcessBuilder pb = new ProcessBuilder("sh", "-c", command);
String shell = shellPath != null ? shellPath : "sh";
ProcessBuilder pb = new ProcessBuilder(shell, "-c", command);
pb.directory(new File(cwd));
pb.environment().putAll(toEnvMap(env));
pb.redirectErrorStream(true);
@@ -214,9 +229,11 @@ public class ShellPlugin extends Plugin {
}
private String[] buildEnv() {
String ourBin = prefixDir + "/bin";
String termuxBin = "/data/data/com.termux/files/usr/bin";
String termuxPrefix = "/data/data/com.termux/files/usr";
boolean hasTermux = new File(termuxBin).isDirectory();
boolean hasOurPrefix = new File(ourBin + "/bash").exists() || new File(ourBin + "/sh").exists();
String toolsBin = toolsDir + "/bin";
String toolsUsrBin = toolsDir + "/usr/bin";
@@ -224,23 +241,31 @@ public class ShellPlugin extends Plugin {
String systemPath = System.getenv("PATH");
StringBuilder pathBuilder = new StringBuilder();
if (hasOurPrefix) pathBuilder.append(ourBin).append(":");
pathBuilder.append(appBin).append(":");
pathBuilder.append(toolsBin).append(":");
pathBuilder.append(toolsUsrBin).append(":");
if (hasTermux) pathBuilder.append(termuxBin).append(":");
pathBuilder.append(systemPath);
String prefix = hasOurPrefix ? prefixDir : (hasTermux ? termuxPrefix : toolsDir + "/usr");
String home = hasOurPrefix ? homeDir + "/home" : homeDir;
java.util.List<String> envList = new java.util.ArrayList<>();
envList.add("HOME=" + homeDir);
envList.add("HOME=" + home);
envList.add("PATH=" + pathBuilder.toString());
envList.add("PREFIX=" + (hasTermux ? termuxPrefix : toolsDir + "/usr"));
envList.add("TMPDIR=" + homeDir + "/tmp");
envList.add("PREFIX=" + prefix);
envList.add("TMPDIR=" + (hasOurPrefix ? prefixDir + "/tmp" : homeDir + "/tmp"));
envList.add("TERM=xterm-256color");
envList.add("LANG=en_US.UTF-8");
envList.add("ANDROID_HOME=" + toolsDir);
envList.add("ANDROID_SDK_ROOT=" + toolsDir);
envList.add("JAVA_HOME=" + toolsDir + "/java");
envList.add("PROJECTS=" + projectsDir);
if (hasOurPrefix) {
envList.add("LD_LIBRARY_PATH=" + prefixDir + "/lib");
envList.add("BOOTSTRAP=zaichat");
}
if (hasTermux) {
envList.add("TERMUX_VERSION=" + getTermuxVersion());
envList.add("LD_LIBRARY_PATH=" + termuxPrefix + "/lib");

View File

@@ -1,6 +1,6 @@
{
"name": "zai-chat",
"version": "1.4.0",
"version": "2.0.0",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js",
"scripts": {

View File

@@ -166,8 +166,10 @@
<p class="subtitle">Set up on-device build tools</p>
</div>
<div id="devsetup-status" class="devsetup-status">
<p>Downloads build tools to compile &amp; install APKs directly on your device.</p>
<p>Required: ~50MB download (aapt2, d8, ecj, android.jar, apksigner)</p>
<p>Downloads and sets up a complete <strong>Termux Linux environment</strong> inside the app.</p>
<p>No external apps needed — bash, coreutils, package manager all included.</p>
<p>Download size: ~30MB (one-time). Architecture auto-detected.</p>
<p>After install, use <code>pkg install aapt2 ecj dx apksigner</code> to add build tools.</p>
</div>
<div id="devsetup-progress" style="display:none">
<div class="devsetup-progress-bar">
@@ -272,13 +274,27 @@
</div>
<div class="settings-section">
<h3>About</h3>
<p class="about-text">Z.AI Chat v1.4.0</p>
<p class="about-text">Z.AI Chat v2.0.0</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p>
</div>
<div class="settings-section">
<h3>Changelog</h3>
<ul class="changelog-list">
<li>
<span class="changelog-version">v2.0.0</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li><strong>Built-in Termux</strong> — full Linux environment inside the app, no external Termux needed</li>
<li>One-time download (~30MB) of Termux bootstrap (bash, coreutils, apt package manager)</li>
<li>Auto-detects CPU architecture (ARM64, ARM, x86, x86_64)</li>
<li>Path patching — fixes all Termux paths to work from our app prefix</li>
<li>BootstrapPlugin — native download, extract, symlink, patch pipeline</li>
<li>ShellPlugin upgraded — uses bundled bash instead of limited /system/bin/sh</li>
<li>Install build tools via: <code>pkg install aapt2 ecj dx apksigner</code></li>
<li>APK stays tiny (~1MB) — bootstrap downloaded on first use, not embedded</li>
</ul>
</li>
<li>
<span class="changelog-version">v1.4.0</span>
<span class="changelog-date">2026-05-19</span>

View File

@@ -1043,6 +1043,7 @@
var Shell = null;
var Installer = null;
var Wake = null;
var Bootstrap = null;
var termState = {
history: [],
historyIndex: -1,
@@ -1062,10 +1063,12 @@
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
} catch(e) {}
if (!Shell) console.warn('Shell plugin not available');
if (!Installer) console.warn('Installer plugin not available');
if (!Wake) console.warn('Wake plugin not available');
if (!Bootstrap) console.warn('Bootstrap plugin not available');
}
async function setWakeLock(on) {
@@ -1710,17 +1713,16 @@
];
async function checkDevTools() {
if (!Shell) return false;
if (!Bootstrap) return false;
try {
var result = await shellExec('which aapt2 2>/dev/null && echo "OK" || echo "MISSING"', termState.homeDir, false);
termState.devToolsInstalled = result.output && result.output.indexOf('OK') >= 0;
return termState.devToolsInstalled;
var status = await Bootstrap.getStatus();
return status.installed === true;
} catch(e) { return false; }
}
async function setupDevTools() {
if (!Shell) {
alert('Shell plugin not available');
if (!Bootstrap) {
alert('Bootstrap plugin not available');
return;
}
@@ -1734,94 +1736,38 @@
btn.querySelector('.btn-text').textContent = 'Installing...';
btn.querySelector('.btn-loader').style.display = 'inline-block';
progress.style.display = 'block';
progressText.textContent = 'Starting...';
var toolsDir = termState.toolsDir || (termState.homeDir + '/tools');
var binDir = toolsDir + '/bin';
var steps = [
{ label: 'Creating directories...', cmd: 'mkdir -p ' + binDir + ' ' + toolsDir + '/lib ' + toolsDir + '/java' },
{ label: 'Setting up shell...', cmd: 'cp /system/bin/sh ' + binDir + '/sh 2>/dev/null; chmod +x ' + binDir + '/* 2>/dev/null; echo OK' },
{ label: 'Checking environment...', cmd: 'ls -la ' + binDir + '/ && echo "Environment ready"' }
];
Bootstrap.addListener('bootstrap-progress', function(event) {
if (progressFill) progressFill.style.width = event.percent + '%';
if (progressText) progressText.textContent = event.message;
});
for (var i = 0; i < steps.length; i++) {
progressText.textContent = steps[i].label;
progressFill.style.width = ((i + 1) / (steps.length + 1) * 100) + '%';
var result = await shellExec(steps[i].cmd, termState.homeDir, false);
if (result.exitCode !== 0 && result.exitCode !== undefined) {
progressText.textContent = 'Warning: ' + steps[i].label + ' had issues';
try {
var result = await Bootstrap.install();
statusEl.innerHTML = '<p style="color:var(--success);font-size:16px;font-weight:700">&#10004; Termux environment installed!</p>' +
'<p>Full Linux shell with bash, coreutils, and package manager ready.</p>' +
'<p>Install build tools: open Terminal → type <code>pkg install aapt2 ecj dx apksigner</code></p>';
btn.querySelector('.btn-text').textContent = 'Installed';
btn.querySelector('.btn-loader').style.display = 'none';
termState.homeDir = result.prefixDir ? result.prefixDir.replace('/usr', '') : termState.homeDir;
termState.cwd = termState.homeDir + '/home';
if (Shell) {
var env = await Shell.getEnv();
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME;
}
updateCwdDisplay();
} catch(e) {
statusEl.innerHTML = '<p style="color:var(--danger)">Install failed: ' + e.message + '</p>' +
'<p>Check your internet connection and try again.</p>';
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Retry Install';
btn.querySelector('.btn-loader').style.display = 'none';
}
progressText.textContent = 'Writing setup scripts...';
progressFill.style.width = '80%';
var setupScript = '#!/system/bin/sh\n' +
'TOOLS_DIR="' + toolsDir + '"\n' +
'BIN_DIR="' + binDir + '"\n' +
'echo "[*] Z.AI Dev Tools Setup"\n' +
'echo "[*] Tools directory: $TOOLS_DIR"\n' +
'echo "[*] For full build support, install these via Termux:"\n' +
'echo " pkg install aapt2 openjdk-17 dx ecj"\n' +
'echo ""\n' +
'echo "[*] Checking available tools..."\n' +
'for tool in aapt2 d8 ecj java apksigner zipalign; do\n' +
' if which $tool 2>/dev/null; then\n' +
' echo " [+] $tool: $(which $tool)"\n' +
' else\n' +
' echo " [-] $tool: not found"\n' +
' fi\n' +
'done\n' +
'echo ""\n' +
'echo "[*] For on-device APK building, you need:"\n' +
'echo " 1. Install Termux from F-Droid or GitHub"\n' +
'echo " 2. In Termux: pkg install aapt2 openjdk-17 dx ecj apksigner"\n' +
'echo " 3. Set TOOLS_PATH in terminal to point to Termux binaries"\n' +
'echo ""\n' +
'echo "[*] Device info:"\n' +
'uname -a\n' +
'echo "Arch: $(uname -m)"\n' +
'echo "[*] Done"\n';
await shellWriteFile(toolsDir + '/setup.sh', setupScript);
await shellExec('chmod +x ' + toolsDir + '/setup.sh', termState.homeDir, false);
var projectTemplate = '#!/system/bin/sh\n' +
'# Z.AI Quick Project Creator\n' +
'PROJECT_NAME="${1:-myapp}"\n' +
'PROJECT_DIR="' + (termState.projectsDir || termState.homeDir + '/projects') + '/$PROJECT_NAME"\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/java/ai/z/app\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/values\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/layout\n' +
'mkdir -p "$PROJECT_DIR"/app/src/main/res/mipmap-hdpi\n' +
'mkdir -p "$PROJECT_DIR"/build\n' +
'# AndroidManifest.xml\n' +
'cat > "$PROJECT_DIR"/app/src/main/AndroidManifest.xml << \'MANIFEST\'\n' +
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n' +
' package="ai.z.app">\n' +
' <application android:label="$PROJECT_NAME" android:theme="@android:style/Theme.Material.Light">\n' +
' <activity android:name=".MainActivity" android:exported="true">\n' +
' <intent-filter>\n' +
' <action android:name="android.intent.action.MAIN"/>\n' +
' <category android:name="android.intent.category.LAUNCHER"/>\n' +
' </intent-filter>\n' +
' </activity>\n' +
' </application>\n' +
'</manifest>\n' +
'MANIFEST\n' +
'echo "[OK] Project created: $PROJECT_DIR"\n' +
'echo "[*] Next: Ask AI to generate the Java code, then build with Deploy"\n';
await shellWriteFile(toolsDir + '/create-project.sh', projectTemplate);
await shellExec('chmod +x ' + toolsDir + '/create-project.sh', termState.homeDir, false);
progressFill.style.width = '100%';
progressText.textContent = 'Setup complete!';
statusEl.innerHTML = '<p style="color:var(--success)">Dev environment ready!</p>' +
'<p>Use the terminal to build apps. Install Termux for full tool support (aapt2, d8, ecj).</p>';
btn.querySelector('.btn-text').textContent = 'Installed';
btn.querySelector('.btn-loader').style.display = 'none';
}
// ---- Init Terminal ----