adb-sqlite-viewer 1.0.5
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/LICENSE +21 -0
- package/README.md +151 -0
- package/bridge/server.js +251 -0
- package/cli/bin.cjs +25 -0
- package/cli/server.cjs +121 -0
- package/dist/assets/index-COoGrD16.js +61 -0
- package/dist/index.html +821 -0
- package/electron/main.cjs +105 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Siddharth Bisht
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SQLite DevTools for Mobile (React Native)
|
|
2
|
+
|
|
3
|
+
A browser-based tool for inspecting SQLite databases on Android devices. Browse tables, view schemas, and execute SQL queries directly on your device.
|
|
4
|
+
|
|
5
|
+
## Four Ways to Use
|
|
6
|
+
|
|
7
|
+
### Option 1: Desktop App (Easiest)
|
|
8
|
+
|
|
9
|
+
Download the installer from [Releases](https://github.com/amitwinit/SQLite-DevTools-Mobile-ReactNative/releases) and run it. The app bundles the ADB bridge server — it starts automatically when you launch the app. No separate downloads, no terminal commands.
|
|
10
|
+
|
|
11
|
+
**Requirements:**
|
|
12
|
+
- Windows (NSIS installer)
|
|
13
|
+
- `adb` on your PATH (Android SDK Platform-Tools)
|
|
14
|
+
- Android device with USB debugging enabled
|
|
15
|
+
|
|
16
|
+
### Option 2: Hosted Version + ADB Bridge (Best for React Native Developers)
|
|
17
|
+
|
|
18
|
+
Use the deployed version at **[amitwinit.github.io/SQLite-DevTools-Mobile-ReactNative](https://amitwinit.github.io/SQLite-DevTools-Mobile-ReactNative/)** together with the **ADB Bridge** — a small localhost server that wraps `adb shell` commands. This lets you inspect databases while ADB stays running for React Native development.
|
|
19
|
+
|
|
20
|
+
**Setup:**
|
|
21
|
+
|
|
22
|
+
1. Download `adb-bridge.exe` from [Releases](https://github.com/amitwinit/SQLite-DevTools-Mobile-ReactNative/releases), or build it yourself:
|
|
23
|
+
```bash
|
|
24
|
+
cd bridge
|
|
25
|
+
npm install
|
|
26
|
+
npm run build # produces adb-bridge.exe
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. Run the bridge:
|
|
30
|
+
```bash
|
|
31
|
+
# Either run the exe directly:
|
|
32
|
+
adb-bridge.exe
|
|
33
|
+
|
|
34
|
+
# Or with Node.js:
|
|
35
|
+
cd bridge && node server.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Open the hosted website — it auto-detects the bridge and connects through it.
|
|
39
|
+
|
|
40
|
+
**How it works:**
|
|
41
|
+
```
|
|
42
|
+
Hosted website (HTTPS) ──HTTP──> localhost:15555 (bridge) ──> adb shell ──> Device
|
|
43
|
+
```
|
|
44
|
+
The website detects the bridge on startup and routes all commands through HTTP instead of WebUSB. No need to kill ADB.
|
|
45
|
+
|
|
46
|
+
### Option 3: Hosted Version with WebUSB (No Setup Required)
|
|
47
|
+
|
|
48
|
+
Use the deployed version at **[amitwinit.github.io/SQLite-DevTools-Mobile-ReactNative](https://amitwinit.github.io/SQLite-DevTools-Mobile-ReactNative/)**
|
|
49
|
+
|
|
50
|
+
This version uses **WebUSB** to communicate with your Android device directly from the browser. No backend server needed.
|
|
51
|
+
|
|
52
|
+
**Requirements:**
|
|
53
|
+
- Chrome or Edge (WebUSB is not supported in Firefox/Safari)
|
|
54
|
+
- Android device with USB debugging enabled
|
|
55
|
+
- You must **stop the local ADB server** first: `adb kill-server`
|
|
56
|
+
|
|
57
|
+
**Important:** WebUSB and the local ADB server cannot use the USB interface at the same time. If you are actively developing a React Native app and need ADB running, use **Option 1** or **Option 2** instead.
|
|
58
|
+
|
|
59
|
+
**Steps:**
|
|
60
|
+
1. Run `adb kill-server` in your terminal
|
|
61
|
+
2. Open the hosted URL in Chrome/Edge
|
|
62
|
+
3. Click **Connect Device** and select your phone from the USB picker
|
|
63
|
+
4. Approve the USB debugging prompt on your phone (first time only)
|
|
64
|
+
5. Select a package and database, then start querying
|
|
65
|
+
|
|
66
|
+
### Option 4: Local Flask Server (Legacy)
|
|
67
|
+
|
|
68
|
+
If you are developing a React Native app and need ADB running alongside, use the local Flask backend. Both tools share the same ADB server so there is no conflict.
|
|
69
|
+
|
|
70
|
+
**Setup:**
|
|
71
|
+
|
|
72
|
+
1. Install Python dependencies:
|
|
73
|
+
```bash
|
|
74
|
+
pip install -r requirements.txt
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. Copy and configure environment:
|
|
78
|
+
```bash
|
|
79
|
+
cp .env.example .env
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Update `.env` with your configuration:
|
|
83
|
+
- `DEVICE_SERIAL` — run `adb devices` to find it
|
|
84
|
+
- `PACKAGE_NAME` — your app's package name
|
|
85
|
+
- `DB_NAME` — the SQLite database filename
|
|
86
|
+
- `PYTHON_TOOLS_PATH` — path to the python_tools directory
|
|
87
|
+
|
|
88
|
+
3. Run the server:
|
|
89
|
+
```bash
|
|
90
|
+
python app.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
4. Open http://localhost:5001 in any browser
|
|
94
|
+
|
|
95
|
+
## When to Use Which
|
|
96
|
+
|
|
97
|
+
| Scenario | Use |
|
|
98
|
+
|----------|-----|
|
|
99
|
+
| Just want it to work, one click | Desktop App (Option 1) |
|
|
100
|
+
| Active React Native development | Desktop App (Option 1) or ADB Bridge (Option 2) |
|
|
101
|
+
| Quick DB inspection, no local setup | WebUSB (Option 3) |
|
|
102
|
+
| Sharing with teammates who don't have Python | WebUSB (Option 3) |
|
|
103
|
+
| Need ADB for other tools simultaneously | Desktop App (Option 1) or ADB Bridge (Option 2) |
|
|
104
|
+
|
|
105
|
+
## Environment Variables (Option 4)
|
|
106
|
+
|
|
107
|
+
### Application Configuration
|
|
108
|
+
- `PACKAGE_NAME`: Android app package name
|
|
109
|
+
- `DB_NAME`: Database name on the device
|
|
110
|
+
- `DEVICE_SERIAL`: ADB device serial number
|
|
111
|
+
- `PYTHON_TOOLS_PATH`: Path to python_tools directory
|
|
112
|
+
|
|
113
|
+
### Flask Server Configuration
|
|
114
|
+
- `FLASK_HOST`: Flask server host (default: 0.0.0.0)
|
|
115
|
+
- `FLASK_PORT`: Flask server port (default: 5001)
|
|
116
|
+
- `FLASK_DEBUG`: Enable debug mode (default: True)
|
|
117
|
+
|
|
118
|
+
### Cache Configuration
|
|
119
|
+
- `USE_CACHE`: Enable database caching (default: True)
|
|
120
|
+
- `FORCE_LOCAL`: Force local database operations (default: False)
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
To work on the WebUSB frontend:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install
|
|
128
|
+
npm run dev
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To build for production (GitHub Pages):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm run build
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The built files go to `dist/` and are deployed to GitHub Pages automatically on push to `main`.
|
|
138
|
+
|
|
139
|
+
To run the Electron desktop app in development:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run electron:dev
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
To build the Electron installer:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run electron:build
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The installer is output to `electron-dist/`.
|
package/bridge/server.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const { execFile, spawn } = require("child_process");
|
|
3
|
+
|
|
4
|
+
const TIMEOUT = 30_000;
|
|
5
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
// ── Helpers ────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function cors(res) {
|
|
10
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
11
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
12
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function json(res, status, data) {
|
|
16
|
+
cors(res);
|
|
17
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
18
|
+
res.end(JSON.stringify(data));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readBody(req) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
req.on("data", (chunk) => {
|
|
26
|
+
size += chunk.length;
|
|
27
|
+
if (size > 1_000_000) {
|
|
28
|
+
reject(new Error("Request body too large"));
|
|
29
|
+
req.destroy();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
});
|
|
34
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
35
|
+
req.on("error", reject);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run adb with simple args (devices, version). Rejects on non-zero exit. */
|
|
40
|
+
function adb(args) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
execFile("adb", args, { timeout: TIMEOUT, maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
43
|
+
if (err) {
|
|
44
|
+
reject(new Error(stderr?.trim() || err.message));
|
|
45
|
+
} else {
|
|
46
|
+
resolve(stdout);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run a shell command on device via stdin piping.
|
|
54
|
+
* Avoids Windows argument quoting issues with complex shell commands.
|
|
55
|
+
* Returns stdout even on non-zero exit codes (common for probe scripts).
|
|
56
|
+
*/
|
|
57
|
+
function adbShell(command, serial) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const args = [];
|
|
60
|
+
if (serial) args.push("-s", serial);
|
|
61
|
+
args.push("shell");
|
|
62
|
+
|
|
63
|
+
const proc = spawn("adb", args, { windowsHide: true });
|
|
64
|
+
let stdout = "";
|
|
65
|
+
let stderr = "";
|
|
66
|
+
let settled = false;
|
|
67
|
+
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
if (!settled) {
|
|
70
|
+
settled = true;
|
|
71
|
+
proc.kill();
|
|
72
|
+
reject(new Error("Command timed out"));
|
|
73
|
+
}
|
|
74
|
+
}, TIMEOUT);
|
|
75
|
+
|
|
76
|
+
proc.stdout.on("data", (data) => { stdout += data; });
|
|
77
|
+
proc.stderr.on("data", (data) => { stderr += data; });
|
|
78
|
+
|
|
79
|
+
proc.on("close", () => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
// Always resolve with stdout — device commands often exit non-zero
|
|
84
|
+
// (e.g. probe scripts where some iterations fail) but still produce
|
|
85
|
+
// valid output. Only reject if we got nothing and stderr has content.
|
|
86
|
+
if (!stdout && stderr.trim()) {
|
|
87
|
+
reject(new Error(stderr.trim()));
|
|
88
|
+
} else {
|
|
89
|
+
resolve(stdout);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
proc.on("error", (err) => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
reject(err);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Send command through stdin — bypasses Windows command-line quoting entirely
|
|
101
|
+
proc.stdin.write(command + "\n");
|
|
102
|
+
proc.stdin.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Routes ─────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function handlePing(_req, res) {
|
|
109
|
+
json(res, 200, { ok: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleDevices(_req, res) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = await adb(["devices", "-l"]);
|
|
115
|
+
const lines = raw.split("\n").slice(1); // skip header
|
|
116
|
+
const devices = [];
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed || trimmed.startsWith("*")) continue;
|
|
120
|
+
const parts = trimmed.split(/\s+/);
|
|
121
|
+
const serial = parts[0];
|
|
122
|
+
const state = parts[1];
|
|
123
|
+
if (state !== "device") continue;
|
|
124
|
+
|
|
125
|
+
// Extract model from "model:<value>" token
|
|
126
|
+
let model = "";
|
|
127
|
+
for (const p of parts.slice(2)) {
|
|
128
|
+
if (p.startsWith("model:")) {
|
|
129
|
+
model = p.slice(6);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
devices.push({
|
|
134
|
+
serial,
|
|
135
|
+
display_name: model ? `${model} (${serial})` : serial,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
json(res, 200, { devices });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
json(res, 500, { error: err.message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function handleShell(req, res) {
|
|
145
|
+
let body;
|
|
146
|
+
try {
|
|
147
|
+
body = JSON.parse(await readBody(req));
|
|
148
|
+
} catch {
|
|
149
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { command, serial } = body;
|
|
154
|
+
if (!command || typeof command !== "string") {
|
|
155
|
+
json(res, 400, { error: "Missing 'command' string in body" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const output = await adbShell(command, serial);
|
|
161
|
+
json(res, 200, { output });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
json(res, 500, { error: err.message });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Exports (for use by cli/server.cjs and electron) ───
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Dispatch an incoming request to the appropriate API handler.
|
|
171
|
+
* Returns true if a route was matched, false otherwise.
|
|
172
|
+
*/
|
|
173
|
+
async function handleRequest(req, res) {
|
|
174
|
+
const url = new URL(req.url, "http://localhost");
|
|
175
|
+
const p = url.pathname;
|
|
176
|
+
|
|
177
|
+
if (req.method === "OPTIONS") {
|
|
178
|
+
cors(res);
|
|
179
|
+
res.writeHead(204);
|
|
180
|
+
res.end();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
if (p === "/api/ping" && req.method === "GET") {
|
|
186
|
+
await handlePing(req, res);
|
|
187
|
+
return true;
|
|
188
|
+
} else if (p === "/api/devices" && req.method === "GET") {
|
|
189
|
+
await handleDevices(req, res);
|
|
190
|
+
return true;
|
|
191
|
+
} else if (p === "/api/shell" && req.method === "POST") {
|
|
192
|
+
await handleShell(req, res);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
json(res, 500, { error: err.message });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Verify adb is in PATH. Returns the version string, or rejects.
|
|
205
|
+
*/
|
|
206
|
+
function checkAdb() {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
execFile("adb", ["version"], { timeout: 5000 }, (err, stdout) => {
|
|
209
|
+
if (err) {
|
|
210
|
+
reject(new Error("'adb' not found in PATH. Install Android SDK Platform-Tools."));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
resolve(stdout.split("\n")[0].trim());
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { handleRequest, checkAdb };
|
|
219
|
+
|
|
220
|
+
// ── Standalone startup (node bridge/server.js) ─────────
|
|
221
|
+
|
|
222
|
+
if (require.main === module) {
|
|
223
|
+
const PORT = 15555;
|
|
224
|
+
const HOST = "127.0.0.1";
|
|
225
|
+
|
|
226
|
+
const server = http.createServer(async (req, res) => {
|
|
227
|
+
const handled = await handleRequest(req, res);
|
|
228
|
+
if (!handled) {
|
|
229
|
+
json(res, 404, { error: "Not found" });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
checkAdb()
|
|
234
|
+
.then((versionLine) => {
|
|
235
|
+
console.log(`Found: ${versionLine}`);
|
|
236
|
+
server.listen(PORT, HOST, () => {
|
|
237
|
+
console.log(`ADB Bridge listening on http://${HOST}:${PORT}`);
|
|
238
|
+
console.log("Endpoints:");
|
|
239
|
+
console.log(" GET /api/ping — health check");
|
|
240
|
+
console.log(" GET /api/devices — list connected devices");
|
|
241
|
+
console.log(" POST /api/shell — run adb shell command");
|
|
242
|
+
console.log("\nPress Ctrl+C to stop.");
|
|
243
|
+
if (process.send) process.send({ type: "ready" });
|
|
244
|
+
});
|
|
245
|
+
})
|
|
246
|
+
.catch((err) => {
|
|
247
|
+
console.error("ERROR:", err.message);
|
|
248
|
+
if (process.send) process.send({ type: "error", message: err.message });
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
251
|
+
}
|
package/cli/bin.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
let port = 8085;
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
9
|
+
port = parseInt(args[i + 1], 10);
|
|
10
|
+
i++;
|
|
11
|
+
} else if (args[i].startsWith("--port=")) {
|
|
12
|
+
port = parseInt(args[i].split("=")[1], 10);
|
|
13
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
14
|
+
console.log("Usage: sqlite-viewer [--port <number>]");
|
|
15
|
+
console.log(" --port, -p Port to listen on (default: 8085)");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
21
|
+
console.error("Invalid port number.");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
require("./server.cjs").start(port);
|
package/cli/server.cjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { exec } = require("child_process");
|
|
7
|
+
const { handleRequest: bridgeHandler, checkAdb } = require("../bridge/server.js");
|
|
8
|
+
|
|
9
|
+
const DIST_DIR = path.join(__dirname, "..", "dist");
|
|
10
|
+
|
|
11
|
+
const MIME = {
|
|
12
|
+
".html": "text/html; charset=utf-8",
|
|
13
|
+
".js": "application/javascript; charset=utf-8",
|
|
14
|
+
".css": "text/css; charset=utf-8",
|
|
15
|
+
".json": "application/json",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".ico": "image/x-icon",
|
|
19
|
+
".woff": "font/woff",
|
|
20
|
+
".woff2": "font/woff2",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function serveStatic(req, res) {
|
|
24
|
+
let urlPath = new URL(req.url, "http://localhost").pathname;
|
|
25
|
+
if (urlPath === "/") urlPath = "/index.html";
|
|
26
|
+
|
|
27
|
+
const filePath = path.join(DIST_DIR, urlPath);
|
|
28
|
+
const resolved = path.resolve(filePath);
|
|
29
|
+
|
|
30
|
+
// Prevent path traversal
|
|
31
|
+
if (!resolved.startsWith(path.resolve(DIST_DIR))) {
|
|
32
|
+
res.writeHead(403);
|
|
33
|
+
res.end("Forbidden");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fs.readFile(resolved, (err, data) => {
|
|
38
|
+
if (err) {
|
|
39
|
+
// SPA fallback — serve index.html for unknown paths
|
|
40
|
+
fs.readFile(path.join(DIST_DIR, "index.html"), (err2, html) => {
|
|
41
|
+
if (err2) {
|
|
42
|
+
res.writeHead(500);
|
|
43
|
+
res.end("dist/index.html not found. Was the package built?");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
47
|
+
res.end(html);
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const ext = path.extname(resolved);
|
|
52
|
+
const mime = MIME[ext] || "application/octet-stream";
|
|
53
|
+
const isHashed = /assets[\\/].*\.[a-f0-9A-Z_-]{5,}\.(js|css)$/.test(resolved);
|
|
54
|
+
res.writeHead(200, {
|
|
55
|
+
"Content-Type": mime,
|
|
56
|
+
"Cache-Control": isHashed ? "public, max-age=31536000, immutable" : "no-cache",
|
|
57
|
+
});
|
|
58
|
+
res.end(data);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function requestHandler(req, res) {
|
|
63
|
+
const urlPath = new URL(req.url, "http://localhost").pathname;
|
|
64
|
+
|
|
65
|
+
if (urlPath.startsWith("/api/") || req.method === "OPTIONS") {
|
|
66
|
+
const handled = await bridgeHandler(req, res);
|
|
67
|
+
if (handled) return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
serveStatic(req, res);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function openBrowser(url) {
|
|
74
|
+
const cmd =
|
|
75
|
+
process.platform === "win32" ? `start "" "${url}"`
|
|
76
|
+
: process.platform === "darwin" ? `open "${url}"`
|
|
77
|
+
: `xdg-open "${url}"`;
|
|
78
|
+
exec(cmd, () => {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function start(port) {
|
|
82
|
+
// Verify dist/ exists
|
|
83
|
+
if (!fs.existsSync(path.join(DIST_DIR, "index.html"))) {
|
|
84
|
+
console.error("ERROR: dist/index.html not found.");
|
|
85
|
+
console.error("Run: npm run build:npm");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check adb (warn only — WebUSB still works without it)
|
|
90
|
+
try {
|
|
91
|
+
const version = await checkAdb();
|
|
92
|
+
console.log(`Found: ${version}`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.warn(`WARNING: ${err.message}`);
|
|
95
|
+
console.warn("ADB bridge features will not work.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const server = http.createServer(requestHandler);
|
|
99
|
+
|
|
100
|
+
server.listen(port, "127.0.0.1", () => {
|
|
101
|
+
const url = `http://127.0.0.1:${port}`;
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log(" ADB SQLite DevTools");
|
|
104
|
+
console.log(` Running at ${url}`);
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log(" Press Ctrl+C to stop.");
|
|
107
|
+
console.log("");
|
|
108
|
+
setTimeout(() => openBrowser(url), 300);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
server.on("error", (err) => {
|
|
112
|
+
if (err.code === "EADDRINUSE") {
|
|
113
|
+
console.error(`Port ${port} is already in use. Try: sqlite-viewer --port ${port + 1}`);
|
|
114
|
+
} else {
|
|
115
|
+
console.error("Server error:", err.message);
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { start };
|