adb-webui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "adb-webui",
3
+ "version": "1.0.0",
4
+ "description": "A beautiful, browser-based ADB APK installer and Android device manager",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "adb-web": "bin/adb-web.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "node server.js"
12
+ },
13
+ "keywords": [
14
+ "adb",
15
+ "android",
16
+ "apk",
17
+ "apk-installer",
18
+ "developer-tools",
19
+ "android-debug-bridge",
20
+ "nodejs",
21
+ "web-ui",
22
+ "cli"
23
+ ],
24
+ "author": "shubhamsoni24 <sonyshubham24@gmail.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/shubhamsoni24/adb-web.git"
29
+ },
30
+ "homepage": "https://github.com/shubhamsoni24/adb-web#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/shubhamsoni24/adb-web/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=16"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "server.js",
40
+ "index.html",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "dependencies": {
45
+ "express": "^4.18.2",
46
+ "multer": "^2.0.1",
47
+ "ws": "^8.16.0"
48
+ }
49
+ }
package/server.js ADDED
@@ -0,0 +1,279 @@
1
+ const express = require('express');
2
+ const multer = require('multer');
3
+ const { WebSocketServer } = require('ws');
4
+ const { spawn, exec } = require('child_process');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const http = require('http');
8
+
9
+ const app = express();
10
+ const server = http.createServer(app);
11
+ const wss = new WebSocketServer({ server });
12
+
13
+ app.use(express.json());
14
+ app.use(express.static(__dirname));
15
+
16
+ // --- Config persistence ---
17
+ const CONFIG_FILE = path.join(__dirname, 'config.json');
18
+ let config = { adbPath: 'adb' };
19
+ if (fs.existsSync(CONFIG_FILE)) {
20
+ try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch(e) {}
21
+ }
22
+ function saveConfig() {
23
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+
26
+ // --- Upload setup ---
27
+ const UPLOAD_DIR = path.join(__dirname, 'uploads');
28
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
29
+ const storage = multer.diskStorage({
30
+ destination: (req, file, cb) => cb(null, UPLOAD_DIR),
31
+ filename: (req, file, cb) => {
32
+ const unique = Date.now() + '-' + Math.round(Math.random() * 1e4);
33
+ cb(null, unique + '-' + file.originalname);
34
+ }
35
+ });
36
+ const upload = multer({ storage, fileFilter: (req, file, cb) => {
37
+ if (file.originalname.endsWith('.apk')) cb(null, true);
38
+ else cb(new Error('Only .apk files allowed'));
39
+ }});
40
+
41
+ // --- WebSocket clients ---
42
+ const clients = new Set();
43
+ wss.on('connection', ws => {
44
+ clients.add(ws);
45
+ ws.on('close', () => clients.delete(ws));
46
+ });
47
+
48
+ function broadcast(data) {
49
+ const msg = JSON.stringify(data);
50
+ clients.forEach(ws => { if (ws.readyState === 1) ws.send(msg); });
51
+ }
52
+
53
+ function log(level, message, extra = {}) {
54
+ broadcast({ type: 'log', level, message, timestamp: new Date().toISOString(), ...extra });
55
+ console.log(`[${level.toUpperCase()}] ${message}`);
56
+ }
57
+
58
+ // --- ADB helper ---
59
+ function adbExec(args, onData, onError, onDone) {
60
+ const adb = config.adbPath || 'adb';
61
+ const proc = spawn(adb, args, { shell: true });
62
+ let stdout = '', stderr = '';
63
+ proc.stdout.on('data', d => { const s = d.toString(); stdout += s; if (onData) onData(s); });
64
+ proc.stderr.on('data', d => { const s = d.toString(); stderr += s; if (onError) onError(s); });
65
+ proc.on('close', code => { if (onDone) onDone(code, stdout, stderr); });
66
+ return proc;
67
+ }
68
+
69
+ function adbPromise(args) {
70
+ return new Promise((resolve, reject) => {
71
+ adbExec(args, null, null, (code, stdout, stderr) => {
72
+ if (code === 0) resolve(stdout.trim());
73
+ else reject(new Error(stderr.trim() || `Exit code ${code}`));
74
+ });
75
+ });
76
+ }
77
+
78
+ // --- Routes ---
79
+
80
+ // Config
81
+ app.get('/api/config', (req, res) => res.json(config));
82
+ app.post('/api/config', (req, res) => {
83
+ const { adbPath } = req.body;
84
+ if (adbPath !== undefined) config.adbPath = adbPath;
85
+ saveConfig();
86
+ res.json({ ok: true, config });
87
+ });
88
+
89
+ // ADB version / status
90
+ app.get('/api/adb/status', async (req, res) => {
91
+ try {
92
+ const version = await adbPromise(['version']);
93
+ res.json({ ok: true, version });
94
+ } catch (e) {
95
+ res.json({ ok: false, error: e.message });
96
+ }
97
+ });
98
+
99
+ // List devices
100
+ app.get('/api/devices', async (req, res) => {
101
+ try {
102
+ const raw = await adbPromise(['devices', '-l']);
103
+ const lines = raw.split('\n').slice(1).filter(l => l.trim() && !l.includes('* daemon'));
104
+ const devices = lines.map(line => {
105
+ const parts = line.trim().split(/\s+/);
106
+ const serial = parts[0];
107
+ const state = parts[1];
108
+ const info = {};
109
+ parts.slice(2).forEach(p => {
110
+ const [k, v] = p.split(':');
111
+ if (k && v) info[k] = v;
112
+ });
113
+ return { serial, state, model: info.model || info.device || 'Unknown', product: info.product || '', transport: info.transport_id || '' };
114
+ }).filter(d => d.serial && d.state);
115
+ res.json({ ok: true, devices });
116
+ } catch (e) {
117
+ res.json({ ok: false, error: e.message, devices: [] });
118
+ }
119
+ });
120
+
121
+ // List uploaded APKs
122
+ app.get('/api/apks', (req, res) => {
123
+ try {
124
+ const files = fs.readdirSync(UPLOAD_DIR)
125
+ .filter(f => f.endsWith('.apk'))
126
+ .map(f => {
127
+ const stat = fs.statSync(path.join(UPLOAD_DIR, f));
128
+ return { filename: f, size: stat.size, mtime: stat.mtime };
129
+ })
130
+ .sort((a, b) => b.mtime - a.mtime);
131
+ res.json({ ok: true, files });
132
+ } catch (e) {
133
+ res.json({ ok: false, files: [] });
134
+ }
135
+ });
136
+
137
+ // Upload APK
138
+ app.post('/api/upload', upload.single('apk'), (req, res) => {
139
+ if (!req.file) return res.status(400).json({ ok: false, error: 'No file uploaded' });
140
+ log('info', `šŸ“¦ Uploaded: ${req.file.originalname} (${(req.file.size / 1024 / 1024).toFixed(2)} MB)`);
141
+ res.json({ ok: true, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size });
142
+ });
143
+
144
+ // Delete APK
145
+ app.delete('/api/apks/:filename', (req, res) => {
146
+ const filePath = path.join(UPLOAD_DIR, req.params.filename);
147
+ if (!filePath.startsWith(UPLOAD_DIR)) return res.status(400).json({ ok: false });
148
+ try {
149
+ fs.unlinkSync(filePath);
150
+ res.json({ ok: true });
151
+ } catch (e) {
152
+ res.status(404).json({ ok: false, error: e.message });
153
+ }
154
+ });
155
+
156
+ // Install APK
157
+ app.post('/api/install', (req, res) => {
158
+ const { filename, serial, flags } = req.body;
159
+ if (!filename) return res.status(400).json({ ok: false, error: 'filename required' });
160
+
161
+ const filePath = path.join(UPLOAD_DIR, filename);
162
+ if (!fs.existsSync(filePath)) return res.status(404).json({ ok: false, error: 'File not found' });
163
+
164
+ const args = [];
165
+ if (serial) { args.push('-s'); args.push(serial); }
166
+ args.push('install');
167
+ if (flags && flags.replace) args.push('-r');
168
+ if (flags && flags.grant) args.push('-g');
169
+ if (flags && flags.downgrade) args.push('-d');
170
+ if (flags && flags.test) args.push('-t');
171
+ args.push(filePath);
172
+
173
+ log('info', `šŸš€ Installing ${filename}${serial ? ' on ' + serial : ''}...`, { apk: filename, device: serial });
174
+
175
+ const installId = Date.now().toString();
176
+ res.json({ ok: true, installId });
177
+
178
+ adbExec(args,
179
+ (data) => log('stdout', data.trim(), { installId }),
180
+ (data) => log('stderr', data.trim(), { installId }),
181
+ (code, stdout, stderr) => {
182
+ if (code === 0 && (stdout + stderr).includes('Success')) {
183
+ log('success', `āœ… Installation SUCCESSFUL: ${filename}`, { installId, result: 'success' });
184
+ } else if (code === 0) {
185
+ log('success', `āœ… Done (code 0): ${filename}`, { installId, result: 'success' });
186
+ } else {
187
+ const errMsg = (stdout + stderr).match(/INSTALL_FAILED[^\n]*/)?.[0] || 'Unknown error';
188
+ log('error', `āŒ Installation FAILED: ${errMsg}`, { installId, result: 'error' });
189
+ }
190
+ }
191
+ );
192
+ });
193
+
194
+ // ADB shell command (safe subset)
195
+ app.post('/api/shell', (req, res) => {
196
+ const { serial, command } = req.body;
197
+ if (!command) return res.status(400).json({ ok: false, error: 'command required' });
198
+
199
+ // Whitelist safe commands
200
+ const SAFE = ['getprop', 'pm list packages', 'dumpsys battery', 'wm size', 'settings get', 'cat /proc/meminfo', 'uptime'];
201
+ const isSafe = SAFE.some(s => command.startsWith(s));
202
+ if (!isSafe) return res.status(403).json({ ok: false, error: 'Command not in safe list' });
203
+
204
+ const args = [];
205
+ if (serial) { args.push('-s'); args.push(serial); }
206
+ args.push('shell');
207
+ args.push(command);
208
+
209
+ log('info', `🐚 Shell: adb ${args.join(' ')}`);
210
+ adbExec(args, null, null, (code, stdout, stderr) => {
211
+ res.json({ ok: code === 0, output: stdout, error: stderr });
212
+ });
213
+ });
214
+
215
+ // Generic ADB command (safe subset)
216
+ app.post('/api/adb', (req, res) => {
217
+ const { serial, action, extra } = req.body;
218
+ const ALLOWED_ACTIONS = {
219
+ 'reboot': ['reboot'],
220
+ 'reboot-recovery': ['reboot', 'recovery'],
221
+ 'reboot-bootloader': ['reboot', 'bootloader'],
222
+ 'reboot-fastboot': ['reboot', 'fastboot'],
223
+ 'kill-server': ['kill-server'],
224
+ 'start-server': ['start-server'],
225
+ 'uninstall': extra ? ['uninstall', extra] : null,
226
+ 'logcat': ['logcat', '-d', '-t', '200'],
227
+ };
228
+
229
+ const cmdArgs = ALLOWED_ACTIONS[action];
230
+ if (!cmdArgs) return res.status(400).json({ ok: false, error: 'Unknown action' });
231
+
232
+ const args = [];
233
+ if (serial) { args.push('-s'); args.push(serial); }
234
+ args.push(...cmdArgs);
235
+
236
+ log('info', `⚔ ADB: ${action}${serial ? ' [' + serial + ']' : ''}${extra ? ' ' + extra : ''}`);
237
+
238
+ adbExec(args, null, null, (code, stdout, stderr) => {
239
+ const output = (stdout + stderr).trim();
240
+ log(code === 0 ? 'success' : 'error', output || (code === 0 ? 'Done' : 'Error'));
241
+ res.json({ ok: code === 0, output, code });
242
+ });
243
+ });
244
+
245
+ // Uninstall package
246
+ app.post('/api/uninstall', (req, res) => {
247
+ const { serial, packageName, keepData } = req.body;
248
+ if (!packageName) return res.status(400).json({ ok: false, error: 'packageName required' });
249
+
250
+ const args = [];
251
+ if (serial) { args.push('-s'); args.push(serial); }
252
+ args.push('uninstall');
253
+ if (keepData) args.push('-k');
254
+ args.push(packageName);
255
+
256
+ log('info', `šŸ—‘ļø Uninstalling ${packageName}...`);
257
+ const installId = Date.now().toString();
258
+ res.json({ ok: true, installId });
259
+
260
+ adbExec(args,
261
+ (data) => log('stdout', data.trim(), { installId }),
262
+ (data) => log('stderr', data.trim(), { installId }),
263
+ (code, stdout, stderr) => {
264
+ const success = (stdout + stderr).includes('Success');
265
+ log(success ? 'success' : 'error',
266
+ success ? `āœ… Uninstalled ${packageName}` : `āŒ Failed: ${(stdout + stderr).trim()}`,
267
+ { installId, result: success ? 'success' : 'error' }
268
+ );
269
+ }
270
+ );
271
+ });
272
+
273
+ // Serve main HTML
274
+ app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html')));
275
+
276
+ const PORT = process.env.PORT || 3737;
277
+ server.listen(PORT, () => {
278
+ console.log(`\nšŸš€ ADB APK Installer running at http://localhost:${PORT}\n`);
279
+ });