stackedge 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/README.md ADDED
@@ -0,0 +1,215 @@
1
+ ### Stackedge
2
+
3
+ <div align="center">
4
+ <img src="/download.jfif" alt="Stackedge Logo" width="200"/>
5
+ </div>
6
+
7
+ ## Decentralized App Hosting on Android using Termux + Tor
8
+
9
+ Stackedge is a Termux-friendly app hosting system that allows you to run any web app (Node.js, PHP, Go, etc.) on your Android device.
10
+ Inspired by the desire to build something decentralized, privacy-focused, and open-source, Stackedge integrates Tor onion services to make your apps accessible anywhere safely.
11
+
12
+ Your apps start immediately, Tor bootstraps in the background, and you can manage them via a simple CLI (start, stop, restart, list, resurrect).
13
+
14
+ **Features**
15
+
16
+ - Run any app (Node.js, PHP, Go, Python…) from the folder you are in.
17
+
18
+ - Tor integration for onion services.
19
+
20
+ - Apps start immediately; Tor bootstraps in the background.
21
+
22
+ - Resilient to Wi-Fi drops or Termux restarts.
23
+
24
+ ## CLI commands similar to pm2:
25
+
26
+ - stackedge start <name> -- <command>
27
+
28
+ - stackedge stop <name>
29
+
30
+ - stackedge restart <name>
31
+
32
+ - stackedge list
33
+
34
+ - stackedge resurrect
35
+
36
+ - Fallback command shows status and help.
37
+
38
+ **Open-source and focused on privacy & decentralization.**
39
+ ---
40
+
41
+ ### Table of Contents
42
+
43
+ **Requirements**
44
+
45
+ 1.Installation
46
+
47
+ 2.Setup
48
+
49
+ **Usage**
50
+
51
+ 1.Termux Auto-Resurrect
52
+
53
+ **Project Philosophy**
54
+
55
+ ## Support
56
+
57
+ ---
58
+
59
+ ### Requirements
60
+
61
+ Android device
62
+
63
+ Termux
64
+ installed
65
+
66
+ **Installed Termux packages:**
67
+ ```
68
+ pkg update && pkg upgrade -y
69
+ pkg install nodejs git tor -y
70
+ ```
71
+
72
+ **Optional for PHP apps:**
73
+ ```
74
+ pkg install php -y
75
+ ```
76
+
77
+ **Optional for Go apps:**
78
+ ```
79
+ pkg install golang -y
80
+
81
+ ```
82
+ **Optional for Python apps:**
83
+ ```
84
+ pkg install python -y
85
+ ```
86
+ ### Installation
87
+
88
+ **Clone the Stackedge repository:**
89
+ ```
90
+ cd $HOME
91
+ git clone https://github.com/Frost-bit-star/stackedge.git
92
+ cd stackedge
93
+ ```
94
+
95
+
96
+ **Install globally via npm:**
97
+ ```
98
+ npm install -g stackedge
99
+ ```
100
+
101
+ **Make sure the CLI is executable:**
102
+ ```
103
+ chmod +x $HOME/.npm-global/bin/stackedge
104
+ ```
105
+
106
+ **Verify installation:**
107
+ ```
108
+ stackedge
109
+ ```
110
+
111
+ **You should see a status summary and available commands.**
112
+
113
+ ### Setup
114
+
115
+ Stackedge uses the following directory structure in Termux:
116
+ ```
117
+ $HOME/.stackedge/
118
+ ├── apps.json # Stores all app info
119
+ ├── tor/
120
+ │ ├── torrc # Tor config
121
+ │ └── services/ # Onion services storage
122
+ └── logs/ # App logs
123
+ ```
124
+
125
+ **Stackedge automatically creates these directories on first run.**
126
+
127
+ ## Usage
128
+ Start an app
129
+
130
+ Navigate to the app folder and run:
131
+ ```
132
+ cd ~/my-react-app
133
+ stackedge start blog -- npm start
134
+ ```
135
+
136
+ - blog is the app name.
137
+
138
+ - Everything after -- is the command to start your app.
139
+
140
+ **Tor starts in the background; your app starts immediately.**
141
+
142
+ ### The onion address will appear once Tor is fully bootstrapped.
143
+
144
+ ## Stop an app
145
+ - stackedge stop blog
146
+
147
+ ## Restart an app
148
+ - stackedge restart blog
149
+
150
+ ## List all apps
151
+ - stackedge list
152
+
153
+ ---
154
+
155
+ **Shows:**
156
+
157
+ - App name
158
+
159
+ - App state (running/stopped)
160
+
161
+ - Port
162
+
163
+ - Tor state (pending/online)
164
+
165
+ - Onion address
166
+
167
+ - Resurrect all apps
168
+
169
+ **Use this to restore apps after Termux restart or Wi-Fi loss:**
170
+
171
+ - stackedge resurrect
172
+
173
+ - Termux Auto-Resurrect
174
+
175
+ **To automatically restore apps when Termux opens:**
176
+ ```bash
177
+ echo 'if command -v stackedge >/dev/null; then stackedge resurrect >/dev/null 2>&1 & fi' >> ~/.bashrc
178
+ ```
179
+
180
+ - Apps will start immediately.
181
+
182
+ - Tor will bootstrap in the background.
183
+
184
+ - No manual intervention needed.
185
+
186
+ - Project Philosophy
187
+
188
+ ## Stackedge is:
189
+
190
+ ### Open-source: Learn, modify, and contribute.
191
+
192
+ - Privacy-focused: Tor integration keeps your apps secure and accessible anonymously.
193
+
194
+ - Decentralized hosting on Android: Your device becomes your own server.
195
+
196
+ - Inspired by a love for Termux and building something great, this project is for developers, hackers, and privacy enthusiasts.
197
+
198
+ Check out my other projects and tutorials:
199
+ [here](https://www.youtube.com/@Mr_termux-r2l)
200
+
201
+
202
+ ---
203
+
204
+ ### ☕ Support This Project
205
+
206
+ If you value open-source and anonymity, support me so I can keep building decentralized hosting tools on Android:
207
+
208
+ <a href="https://buymeacoffee.com/morganmilsn" target="_blank"> <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" height="50" alt="Buy Me A Coffee"> </a>
209
+
210
+
211
+ License
212
+
213
+ ---
214
+
215
+ MIT License – Open source for everyone.
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require("commander");
4
+ const { loadApps, saveApps } = require("../lib/registry");
5
+ const { startProcess } = require("../lib/process");
6
+ const { listApps } = require("../lib/tui/list");
7
+ const { resurrect } = require("../lib/resurrect");
8
+ const { stopApp } = require("../lib/commands/stop");
9
+ const { restartApp } = require("../lib/commands/restart");
10
+
11
+ const { spawn } = require("child_process");
12
+ const net = require("net");
13
+ const fs = require("fs-extra");
14
+ const path = require("path");
15
+
16
+ /* =========================
17
+ TOR CONSTANTS
18
+ ========================= */
19
+
20
+ const TOR_BIN = "/data/data/com.termux/files/usr/bin/tor";
21
+ const TOR_BASE = path.join(process.env.HOME, ".tor");
22
+ const TORRC = path.join(TOR_BASE, "torrc");
23
+ const TOR_HS_DIR = path.join(TOR_BASE, "hidden");
24
+ const CONTROL_PORT = 9051;
25
+
26
+ /* =========================
27
+ UTILS
28
+ ========================= */
29
+
30
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
31
+
32
+ async function getFreePort(start = 3000, end = 9000) {
33
+ for (let p = start; p <= end; p++) {
34
+ try {
35
+ await new Promise((res, rej) => {
36
+ const s = net.createServer()
37
+ .once("error", rej)
38
+ .once("listening", () => s.close(res))
39
+ .listen(p, "127.0.0.1");
40
+ });
41
+ return p;
42
+ } catch {}
43
+ }
44
+ throw new Error("No free ports");
45
+ }
46
+
47
+ /* =========================
48
+ TOR MANAGEMENT
49
+ ========================= */
50
+
51
+ async function ensureTorFilesystem() {
52
+ await fs.ensureDir(TOR_BASE);
53
+ await fs.ensureDir(TOR_HS_DIR);
54
+
55
+ await fs.chmod(TOR_BASE, 0o700);
56
+ await fs.chmod(TOR_HS_DIR, 0o700);
57
+
58
+ if (!await fs.pathExists(TORRC)) {
59
+ await fs.writeFile(TORRC, `
60
+ DataDirectory ${TOR_BASE}
61
+ ControlPort ${CONTROL_PORT}
62
+ CookieAuthentication 1
63
+ AvoidDiskWrites 1
64
+ Log notice stdout
65
+ `.trim() + "\n");
66
+ }
67
+ }
68
+
69
+ async function isTorRunning() {
70
+ return new Promise((resolve) => {
71
+ const socket = net.createConnection(CONTROL_PORT, "127.0.0.1");
72
+ socket.once("connect", () => {
73
+ socket.end();
74
+ resolve(true);
75
+ });
76
+ socket.once("error", () => resolve(false));
77
+ });
78
+ }
79
+
80
+ async function startTorOnce() {
81
+ await ensureTorFilesystem();
82
+
83
+ if (await isTorRunning()) return;
84
+
85
+ spawn(TOR_BIN, ["-f", TORRC], {
86
+ detached: true,
87
+ stdio: "ignore",
88
+ }).unref();
89
+
90
+ // wait for control port
91
+ for (let i = 0; i < 15; i++) {
92
+ if (await isTorRunning()) return;
93
+ await sleep(1000);
94
+ }
95
+
96
+ throw new Error("Tor failed to start");
97
+ }
98
+
99
+ /* =========================
100
+ TOR CONTROL PORT
101
+ ========================= */
102
+
103
+ async function torControl(cmd) {
104
+ const cookie = await fs.readFile(
105
+ path.join(TOR_BASE, "control_auth_cookie")
106
+ );
107
+
108
+ return new Promise((resolve, reject) => {
109
+ const socket = net.createConnection(CONTROL_PORT, "127.0.0.1");
110
+
111
+ socket.on("error", reject);
112
+
113
+ socket.on("data", (data) => {
114
+ if (data.toString().startsWith("250")) {
115
+ resolve(data.toString());
116
+ socket.end();
117
+ }
118
+ });
119
+
120
+ socket.once("connect", () => {
121
+ socket.write(`AUTHENTICATE ${cookie.toString("hex")}\r\n`);
122
+ socket.write(cmd + "\r\n");
123
+ socket.write("QUIT\r\n");
124
+ });
125
+ });
126
+ }
127
+
128
+ async function addHiddenService(name, port) {
129
+ const dir = path.join(TOR_HS_DIR, name);
130
+ await fs.ensureDir(dir);
131
+ await fs.chmod(dir, 0o700);
132
+
133
+ const res = await torControl(
134
+ `ADD_ONION NEW:ED25519-V3 Port=${port},127.0.0.1:${port}`
135
+ );
136
+
137
+ return res.match(/ServiceID=(\S+)/)[1] + ".onion";
138
+ }
139
+
140
+ /* =========================
141
+ START COMMAND
142
+ ========================= */
143
+
144
+ program.command("start <name>")
145
+ .allowUnknownOption(true)
146
+ .action(async (name) => {
147
+
148
+ const idx = process.argv.indexOf("--");
149
+ if (idx === -1) {
150
+ console.log("Usage: stackedge start <name> -- <cmd>");
151
+ process.exit(1);
152
+ }
153
+
154
+ const command = process.argv.slice(idx + 1).join(" ");
155
+ const apps = await loadApps();
156
+
157
+ const port = process.env.PORT || await getFreePort();
158
+
159
+ await startTorOnce();
160
+
161
+ const onion = await addHiddenService(name, port);
162
+
163
+ startProcess({
164
+ name,
165
+ command,
166
+ cwd: process.cwd(),
167
+ env: { ...process.env, PORT: String(port) }
168
+ });
169
+
170
+ apps.push({
171
+ name,
172
+ port,
173
+ onion,
174
+ command,
175
+ appState: "online",
176
+ torState: "online",
177
+ autorestart: true
178
+ });
179
+
180
+ await saveApps(apps);
181
+
182
+ console.log(`✔ ${name} running`);
183
+ console.log(`🌐 ${onion}`);
184
+ });
185
+
186
+ /* =========================
187
+ OTHER COMMANDS
188
+ ========================= */
189
+
190
+ program.command("stop <name>").action(stopApp);
191
+ program.command("restart <name>").action(restartApp);
192
+ program.command("list").action(listApps);
193
+ program.command("resurrect").action(resurrect);
194
+
195
+ program.parse(process.argv);
package/download.jfif ADDED
Binary file
@@ -0,0 +1,44 @@
1
+ const { loadApps, saveApps } = require("../registry");
2
+ const { stopProcess } = require("../process");
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+
6
+ /**
7
+ * Delete an app from the registry and stop it if running.
8
+ * @param {string} name - App name to delete
9
+ */
10
+ async function deleteApp(name) {
11
+ const apps = await loadApps();
12
+ const index = apps.findIndex(a => a.name === name);
13
+
14
+ if (index === -1) {
15
+ console.log(`App '${name}' not found`);
16
+ return;
17
+ }
18
+
19
+ const app = apps[index];
20
+
21
+ // Stop the app if running
22
+ if (app.pid) {
23
+ stopProcess(app);
24
+ console.log(`Stopped '${name}'`);
25
+ }
26
+
27
+ // Remove app from registry
28
+ apps.splice(index, 1);
29
+ await saveApps(apps);
30
+ console.log(`Deleted '${name}' from registry`);
31
+
32
+ // Optional: delete app folder if you want
33
+ // await fs.remove(app.cwd);
34
+ // console.log(`Deleted folder: ${app.cwd}`);
35
+
36
+ // Optional: delete Tor hidden service if exists
37
+ // const hiddenServiceDir = path.join(process.env.HOME, ".tor", "hidden_service_" + name);
38
+ // if (await fs.pathExists(hiddenServiceDir)) {
39
+ // await fs.remove(hiddenServiceDir);
40
+ // console.log(`Deleted Tor hidden service: ${hiddenServiceDir}`);
41
+ // }
42
+ }
43
+
44
+ module.exports = { deleteApp };
@@ -0,0 +1,38 @@
1
+ const { loadApps, saveApps } = require("../registry");
2
+ const { startProcess, stopProcess } = require("../process");
3
+
4
+ /**
5
+ * Restart an app by stopping it and starting it again.
6
+ * @param {string} name - App name
7
+ */
8
+ async function restartApp(name) {
9
+ const apps = await loadApps();
10
+ const app = apps.find(a => a.name === name);
11
+
12
+ if (!app) {
13
+ console.log(`App '${name}' not found`);
14
+ return;
15
+ }
16
+
17
+ // Stop the app
18
+ stopProcess(app);
19
+
20
+ // Reset port and state
21
+ app.port = null;
22
+ app.appState = "starting";
23
+ app.torState = "pending";
24
+
25
+ // Start it again
26
+ startProcess(app);
27
+
28
+ // Save updated apps registry
29
+ const index = apps.findIndex(a => a.name === name);
30
+ if (index !== -1) {
31
+ apps[index] = app;
32
+ await saveApps(apps);
33
+ }
34
+
35
+ console.log(`Restarted '${name}' successfully`);
36
+ }
37
+
38
+ module.exports = { restartApp };
@@ -0,0 +1,13 @@
1
+ const { loadApps, saveApps } = require("../registry");
2
+ const { stopProcess } = require("../process");
3
+
4
+ async function stopApp(name) {
5
+ const apps = await loadApps();
6
+ const app = apps.find(a => a.name === name);
7
+ if (!app) return console.log("App not found");
8
+ stopProcess(app);
9
+ await saveApps(apps);
10
+ console.log(`Stopped ${name}`);
11
+ }
12
+
13
+ module.exports = { stopApp };
package/lib/config.js ADDED
@@ -0,0 +1,11 @@
1
+ const path = require("path");
2
+ const HOME = process.env.HOME;
3
+
4
+ module.exports = {
5
+ HOME,
6
+ BASE_DIR: path.join(HOME, ".stackedge"),
7
+ APPS_FILE: path.join(HOME, ".stackedge", "apps.json"),
8
+ LOG_DIR: path.join(HOME, ".stackedge", "logs"),
9
+ TOR_DIR: path.join(HOME, ".stackedge", "tor"),
10
+ TORRC: path.join(HOME, ".stackedge", "tor", "torrc")
11
+ };
package/lib/process.js ADDED
@@ -0,0 +1,88 @@
1
+ const { spawn } = require("child_process");
2
+ const { loadApps, saveApps } = require("./registry");
3
+
4
+ /**
5
+ * Start an application process
6
+ * @param {Object} app
7
+ */
8
+ async function startProcess(app) {
9
+ if (!app || !app.command) {
10
+ throw new Error("App command is required to start a process.");
11
+ }
12
+
13
+ const child = spawn("sh", ["-c", app.command], {
14
+ cwd: app.cwd || process.cwd(),
15
+ env: { ...process.env, ...app.env },
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ detached: true
18
+ });
19
+
20
+ app.pid = child.pid;
21
+ app.appState = "running";
22
+
23
+ // Persist state immediately
24
+ const apps = await loadApps();
25
+ const idx = apps.findIndex(a => a.name === app.name);
26
+ if (idx !== -1) {
27
+ apps[idx] = app;
28
+ } else {
29
+ apps.push(app);
30
+ }
31
+ await saveApps(apps);
32
+
33
+ // Forward logs
34
+ child.stdout.on("data", data => {
35
+ process.stdout.write(`[${app.name}] ${data}`);
36
+ });
37
+
38
+ child.stderr.on("data", data => {
39
+ process.stderr.write(`[${app.name} ERROR] ${data}`);
40
+ });
41
+
42
+ child.on("exit", async (code, signal) => {
43
+ const apps = await loadApps();
44
+ const idx = apps.findIndex(a => a.name === app.name);
45
+ if (idx !== -1) {
46
+ apps[idx].appState = "stopped";
47
+ apps[idx].pid = null;
48
+ await saveApps(apps);
49
+ }
50
+ console.log(`${app.name} exited (${signal || code})`);
51
+ });
52
+
53
+ child.unref();
54
+ }
55
+
56
+ /**
57
+ * Stop a running application
58
+ * @param {Object} app
59
+ */
60
+ async function stopProcess(app) {
61
+ if (!app || !app.pid) return;
62
+
63
+ try {
64
+ process.kill(app.pid, "SIGTERM");
65
+
66
+ // fallback kill after 3s
67
+ setTimeout(() => {
68
+ try {
69
+ process.kill(app.pid, "SIGKILL");
70
+ } catch {}
71
+ }, 3000);
72
+ } catch (err) {
73
+ console.error(`Failed to stop ${app.name}: ${err.message}`);
74
+ }
75
+
76
+ const apps = await loadApps();
77
+ const idx = apps.findIndex(a => a.name === app.name);
78
+ if (idx !== -1) {
79
+ apps[idx].appState = "stopped";
80
+ apps[idx].pid = null;
81
+ await saveApps(apps);
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ startProcess,
87
+ stopProcess
88
+ };
@@ -0,0 +1,58 @@
1
+ // lib/registry.js
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+ const { APPS_FILE } = require("./config");
5
+
6
+ // Default fields for each app
7
+ const DEFAULT_APP = {
8
+ name: "",
9
+ cwd: "",
10
+ command: "",
11
+ port: null,
12
+ pid: null,
13
+ appState: "starting",
14
+ torState: "pending",
15
+ onion: null,
16
+ autorestart: true
17
+ };
18
+
19
+ /**
20
+ * Load apps from registry
21
+ * @returns {Promise<Array>}
22
+ */
23
+ async function loadApps() {
24
+ try {
25
+ if (!(await fs.pathExists(APPS_FILE))) return [];
26
+ const apps = await fs.readJson(APPS_FILE);
27
+ // Ensure all apps have default fields
28
+ return apps.map(a => ({ ...DEFAULT_APP, ...a }));
29
+ } catch (err) {
30
+ console.error("Failed to load apps registry:", err);
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Save or update apps in registry
37
+ * Merges by app name to avoid overwriting other apps
38
+ * @param {Array} appsToSave
39
+ */
40
+ async function saveApps(appsToSave) {
41
+ try {
42
+ // Ensure directory exists
43
+ await fs.ensureDir(path.dirname(APPS_FILE));
44
+
45
+ // Load existing apps
46
+ const existingApps = await loadApps();
47
+
48
+ // Merge apps by name
49
+ const merged = [...existingApps.filter(e => !appsToSave.some(a => a.name === e.name)), ...appsToSave];
50
+
51
+ // Write JSON to file
52
+ await fs.writeJson(APPS_FILE, merged, { spaces: 2 });
53
+ } catch (err) {
54
+ console.error("Failed to save apps registry:", err);
55
+ }
56
+ }
57
+
58
+ module.exports = { loadApps, saveApps };
@@ -0,0 +1,30 @@
1
+ const { loadApps, saveApps } = require("./registry");
2
+ const { startProcess } = require("./process");
3
+ const { ensureTorConfig, startTor } = require("./tor/tor");
4
+ const { waitForBootstrap } = require("./tor/bootstrap");
5
+ const { createOnion } = require("./tor/control");
6
+
7
+ async function resurrect() {
8
+ const apps = await loadApps();
9
+ if (!apps.length) return;
10
+
11
+ for (const app of apps) {
12
+ startProcess(app);
13
+ app.appState = "running";
14
+ app.torState = "pending";
15
+ }
16
+
17
+ await ensureTorConfig();
18
+ startTor();
19
+ await waitForBootstrap();
20
+
21
+ for (const app of apps) {
22
+ if (!app.onion) app.onion = await createOnion(app.port || 8080);
23
+ app.torState = "online";
24
+ }
25
+
26
+ await saveApps(apps);
27
+ console.log("All apps resurrected");
28
+ }
29
+
30
+ module.exports = { resurrect };
@@ -0,0 +1,54 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const net = require("net");
4
+
5
+ async function waitForBootstrap(torDir, timeout = 30000) {
6
+ return new Promise((resolve, reject) => {
7
+ const cookieFile = path.join(torDir, "control_auth_cookie");
8
+
9
+ let cookieHex;
10
+ try {
11
+ cookieHex = fs.readFileSync(cookieFile).toString("hex");
12
+ } catch (err) {
13
+ return reject(new Error("Cannot read Tor cookie: " + err.message));
14
+ }
15
+
16
+ const socket = net.connect(9051, "127.0.0.1");
17
+ const start = Date.now();
18
+
19
+ socket.on("connect", () => {
20
+ socket.write(`AUTHENTICATE ${cookieHex}\r\n`);
21
+ socket.write("SETEVENTS STATUS_GENERAL\r\n");
22
+ });
23
+
24
+ socket.on("data", (data) => {
25
+ const lines = data.toString().split("\r\n");
26
+
27
+ for (const line of lines) {
28
+ // Example:
29
+ // 650 STATUS_GENERAL BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
30
+ if (line.includes("BOOTSTRAP") && line.includes("PROGRESS=100")) {
31
+ socket.end();
32
+ return resolve();
33
+ }
34
+
35
+ if (line.startsWith("515") || line.startsWith("550")) {
36
+ socket.end();
37
+ return reject(new Error("Tor control error: " + line));
38
+ }
39
+ }
40
+ });
41
+
42
+ socket.on("error", reject);
43
+
44
+ const timer = setInterval(() => {
45
+ if (Date.now() - start > timeout) {
46
+ clearInterval(timer);
47
+ socket.end();
48
+ reject(new Error("Tor bootstrap timed out"));
49
+ }
50
+ }, 1000);
51
+ });
52
+ }
53
+
54
+ module.exports = { waitForBootstrap };
@@ -0,0 +1,37 @@
1
+ const net = require("net");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ async function createOnion(port, torDir) {
6
+ return new Promise((resolve, reject) => {
7
+ const socket = net.connect(9051, "127.0.0.1", () => {
8
+ // Read cookie for authentication
9
+ const cookieFile = path.join(torDir, "control_auth_cookie");
10
+ let cookieHex = "";
11
+ try {
12
+ cookieHex = fs.readFileSync(cookieFile).toString("hex");
13
+ } catch (err) {
14
+ return reject("Cannot read Tor cookie: " + err.message);
15
+ }
16
+ socket.write(`AUTHENTICATE ${cookieHex}\r\n`);
17
+ });
18
+
19
+ socket.on("data", (data) => {
20
+ const lines = data.toString().split("\r\n");
21
+ for (const line of lines) {
22
+ if (line.startsWith("250-ServiceID=")) {
23
+ const id = line.split("=")[1].trim();
24
+ socket.end();
25
+ return resolve(`${id}.onion`);
26
+ } else if (line.startsWith("515") || line.startsWith("550")) {
27
+ socket.end();
28
+ return reject("Tor error: " + line);
29
+ }
30
+ }
31
+ });
32
+
33
+ socket.on("error", (err) => reject(err));
34
+ });
35
+ }
36
+
37
+ module.exports = { createOnion };
package/lib/tor/tor.js ADDED
@@ -0,0 +1,58 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { spawn } = require("child_process");
4
+
5
+ const TOR_BIN = "/data/data/com.termux/files/usr/bin/tor"; // Termux Tor binary
6
+ const TOR_DIR = path.join(process.env.HOME, ".tor");
7
+ const TORRC = path.join(TOR_DIR, "torrc");
8
+
9
+ async function ensureTorConfig() {
10
+ await fs.ensureDir(TOR_DIR);
11
+ const torrc = `
12
+ DataDirectory ${TOR_DIR}
13
+ ControlPort 9051
14
+ CookieAuthentication 1
15
+ AvoidDiskWrites 1
16
+ `.trim();
17
+ await fs.writeFile(TORRC, torrc);
18
+ }
19
+
20
+ async function startTor(appName, appPort) {
21
+ await ensureTorConfig();
22
+
23
+ // Hidden service for this app
24
+ const hiddenServiceDir = path.join(TOR_DIR, `hidden_service_${appName}`);
25
+ await fs.ensureDir(hiddenServiceDir);
26
+
27
+ // Append hidden service config to torrc
28
+ const hiddenServiceConfig = `
29
+ HiddenServiceDir ${hiddenServiceDir}
30
+ HiddenServicePort ${appPort} 127.0.0.1:${appPort}
31
+ `.trim();
32
+ await fs.appendFile(TORRC, "\n" + hiddenServiceConfig);
33
+
34
+ return new Promise((resolve, reject) => {
35
+ const tor = spawn(TOR_BIN, ["-f", TORRC], {
36
+ cwd: TOR_DIR,
37
+ detached: true,
38
+ stdio: ["pipe", "pipe", "pipe"]
39
+ });
40
+
41
+ tor.stdout.on("data", (data) => {
42
+ const text = data.toString();
43
+ process.stdout.write(text);
44
+
45
+ if (text.includes("Bootstrapped 100%")) {
46
+ // Tor fully online
47
+ resolve(hiddenServiceDir);
48
+ }
49
+ });
50
+
51
+ tor.stderr.on("data", (data) => process.stderr.write(data.toString()));
52
+ tor.on("error", (err) => reject(err));
53
+
54
+ tor.unref();
55
+ });
56
+ }
57
+
58
+ module.exports = { ensureTorConfig, startTor, TOR_DIR, TORRC };
@@ -0,0 +1,50 @@
1
+ // lib/tui/list.js
2
+
3
+ // For Chalk v5+, handle default export
4
+ const chalkModule = require("chalk");
5
+ const chalk = chalkModule.default ? chalkModule.default : chalkModule;
6
+
7
+ const { loadApps, saveApps } = require("../registry");
8
+
9
+ // Check if a PID is alive (works on Termux / Linux / Node)
10
+ function isProcessAlive(pid) {
11
+ try {
12
+ process.kill(pid, 0); // signal 0 just tests for existence
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ async function listApps() {
20
+ const apps = await loadApps();
21
+
22
+ console.log(chalk.bold("NAME APP STATE PORT TOR STATE ONION"));
23
+
24
+ for (const a of apps) {
25
+ // Real-time check if the app is actually running
26
+ const alive = a.pid && isProcessAlive(a.pid);
27
+ const appState = alive ? (a.appState || "running") : "stopped";
28
+
29
+ console.log(
30
+ `${a.name.padEnd(8)} ` +
31
+ `${(appState || "").padEnd(11)} ` +
32
+ `${String(a.port || "-").padEnd(6)} ` +
33
+ `${(a.torState || "").padEnd(10)} ` +
34
+ `${a.onion || "setting up"}`
35
+ );
36
+
37
+ // Update registry if state has changed
38
+ if (appState !== a.appState) {
39
+ a.appState = appState;
40
+ const allApps = await loadApps();
41
+ const index = allApps.findIndex(x => x.name === a.name);
42
+ if (index !== -1) {
43
+ allApps[index] = a;
44
+ await saveApps(allApps);
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ module.exports = { listApps };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "stackedge",
3
+ "version": "1.0.0",
4
+ "description": "Termux-friendly decentralized app hosting with Tor",
5
+ "bin": {
6
+ "stackedge": "bin/stackedge.js"
7
+ },
8
+ "type": "commonjs",
9
+ "keywords": [
10
+ "termux",
11
+ "tor",
12
+ "cli",
13
+ "hosting",
14
+ "node",
15
+ "decentralized",
16
+ "android"
17
+ ],
18
+ "author": "Mr_termux-r2l <hr@stackverify.site>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/Frost-bit-star/stackedge.git"
23
+ },
24
+ "homepage": "https://www.youtube.com/@Mr_termux-r2l",
25
+ "dependencies": {
26
+ "commander": "^11.0.0",
27
+ "chalk": "^5.3.0",
28
+ "fs-extra": "^11.2.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "scripts": {
34
+ "start": "node bin/stackedge.js"
35
+ }
36
+ }