voidconnect 0.1.12 → 0.1.14
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/hub/README.md +15 -0
- package/hub/bun.lock +37 -0
- package/hub/index.ts +1 -0
- package/hub/package.json +20 -0
- package/hub/src/auth.ts +19 -0
- package/hub/src/config.ts +43 -0
- package/hub/src/index.ts +103 -0
- package/hub/src/process_manager.ts +62 -0
- package/hub/src/tunnel.ts +49 -0
- package/hub/tsconfig.json +29 -0
- package/install.js +6 -1
- package/package.json +1 -1
package/hub/README.md
ADDED
package/hub/bun.lock
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "hub",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
9
|
+
"hono": "^4.11.4",
|
|
10
|
+
"qrcode-terminal": "^0.12.0",
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/bun": "^1.3.5",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
22
|
+
|
|
23
|
+
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
|
|
24
|
+
|
|
25
|
+
"@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
|
|
26
|
+
|
|
27
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
28
|
+
|
|
29
|
+
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
|
30
|
+
|
|
31
|
+
"qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
|
|
32
|
+
|
|
33
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
34
|
+
|
|
35
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
36
|
+
}
|
|
37
|
+
}
|
package/hub/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
package/hub/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hub",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "^1.3.5"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"typescript": "^5"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "bun run src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
17
|
+
"hono": "^4.11.4",
|
|
18
|
+
"qrcode-terminal": "^0.12.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/hub/src/auth.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import { type Context, type Next } from 'hono';
|
|
3
|
+
|
|
4
|
+
export async function authMiddleware(c: Context, next: Next) {
|
|
5
|
+
const hubPassword = process.env.HUB_PASSWORD;
|
|
6
|
+
|
|
7
|
+
if (!hubPassword) {
|
|
8
|
+
await next();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const authHeader = c.req.header('Authorization');
|
|
13
|
+
|
|
14
|
+
if (!authHeader || authHeader !== `Bearer ${hubPassword}`) {
|
|
15
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await next();
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export interface ProjectConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HubConfig {
|
|
11
|
+
device_name: string;
|
|
12
|
+
password_hash: string | null;
|
|
13
|
+
projects: ProjectConfig[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loadConfig(): Promise<HubConfig> {
|
|
17
|
+
// Check for local run mode (single project via environment variable)
|
|
18
|
+
const projectPath = process.env.HUB_PROJECT_PATH;
|
|
19
|
+
const projectName = process.env.HUB_PROJECT_NAME;
|
|
20
|
+
|
|
21
|
+
if (projectPath && projectName) {
|
|
22
|
+
return {
|
|
23
|
+
device_name: process.env.HUB_DEVICE_NAME || 'VoidConnect Hub',
|
|
24
|
+
password_hash: process.env.HUB_PASSWORD || null,
|
|
25
|
+
projects: [{ name: projectName, path: projectPath }]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Normal mode: load from config file
|
|
30
|
+
const configPath = join(homedir(), '.voidconnect', 'hub_config.json');
|
|
31
|
+
const file = Bun.file(configPath);
|
|
32
|
+
|
|
33
|
+
if (await file.exists()) {
|
|
34
|
+
const content = await file.json();
|
|
35
|
+
return content as HubConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
device_name: 'VoidConnect Hub',
|
|
40
|
+
password_hash: null,
|
|
41
|
+
projects: []
|
|
42
|
+
};
|
|
43
|
+
}
|
package/hub/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { loadConfig } from './config';
|
|
4
|
+
import { authMiddleware } from './auth';
|
|
5
|
+
import { processManager } from './process_manager';
|
|
6
|
+
import qrcode from 'qrcode-terminal';
|
|
7
|
+
import { networkInterfaces } from 'os';
|
|
8
|
+
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
|
|
11
|
+
app.use('/api/*', authMiddleware);
|
|
12
|
+
app.use('/project/*', authMiddleware);
|
|
13
|
+
|
|
14
|
+
app.get('/', (c) => c.text('VoidConnect Hub Running'));
|
|
15
|
+
|
|
16
|
+
app.get('/api/status', (c) => {
|
|
17
|
+
return c.json({ status: 'running', device: process.env.HUB_DEVICE_NAME || 'Unknown' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.get('/api/projects', async (c) => {
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
return c.json(config.projects);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.all('/project/:name/*', async (c) => {
|
|
26
|
+
const name = c.req.param('name');
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const project = config.projects.find(p => p.name === name);
|
|
29
|
+
|
|
30
|
+
if (!project) {
|
|
31
|
+
return c.text('Project not found', 404);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const port = await processManager.startProject(project.name, project.path);
|
|
36
|
+
|
|
37
|
+
const path = c.req.path.replace(`/project/${name}`, '') || '/';
|
|
38
|
+
|
|
39
|
+
const targetUrlObj = new URL(`http://127.0.0.1:${port}${path}`);
|
|
40
|
+
|
|
41
|
+
const originalUrl = new URL(c.req.url);
|
|
42
|
+
originalUrl.searchParams.forEach((value, key) => {
|
|
43
|
+
targetUrlObj.searchParams.set(key, value);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
targetUrlObj.searchParams.set('directory', project.path);
|
|
47
|
+
|
|
48
|
+
const targetUrl = targetUrlObj.toString();
|
|
49
|
+
|
|
50
|
+
const newReq = new Request(targetUrl, {
|
|
51
|
+
method: c.req.method,
|
|
52
|
+
headers: c.req.header(),
|
|
53
|
+
body: c.req.raw.body,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const response = await fetch(newReq);
|
|
57
|
+
return response;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`[Proxy] Error:`, e);
|
|
60
|
+
return c.text(`Global Error: ${e}`, 500);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
65
|
+
|
|
66
|
+
if (process.argv.includes('--tunnel') || process.argv.includes('--cf')) {
|
|
67
|
+
import('./tunnel').then(({ startTunnel }) => {
|
|
68
|
+
startTunnel(port);
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
(async () => {
|
|
72
|
+
const nets = networkInterfaces();
|
|
73
|
+
let localIp = 'localhost';
|
|
74
|
+
|
|
75
|
+
for (const name of Object.keys(nets)) {
|
|
76
|
+
const interfaces = nets[name];
|
|
77
|
+
if (interfaces) {
|
|
78
|
+
for (const net of interfaces) {
|
|
79
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
80
|
+
localIp = net.address;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (localIp !== 'localhost') break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const config = await loadConfig();
|
|
89
|
+
const name = process.env.HUB_DEVICE_NAME || config.device_name || 'My PC';
|
|
90
|
+
const url = `http://${localIp}:${port}`;
|
|
91
|
+
|
|
92
|
+
console.log('\n');
|
|
93
|
+
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
94
|
+
console.log(`\nHub Online: ${name}`);
|
|
95
|
+
console.log(`URL: ${url}\n`);
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const server = Bun.serve({
|
|
100
|
+
port,
|
|
101
|
+
fetch: app.fetch,
|
|
102
|
+
idleTimeout: 255,
|
|
103
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
import { spawn } from 'bun';
|
|
3
|
+
|
|
4
|
+
export class ProcessManager {
|
|
5
|
+
private processes: Map<string, any> = new Map();
|
|
6
|
+
private ports: Map<string, number> = new Map();
|
|
7
|
+
private startPort = 4100;
|
|
8
|
+
|
|
9
|
+
constructor() { }
|
|
10
|
+
|
|
11
|
+
async startProject(name: string, path: string): Promise<number> {
|
|
12
|
+
|
|
13
|
+
if (this.ports.has(name)) {
|
|
14
|
+
const existingPort = this.ports.get(name)!;
|
|
15
|
+
return existingPort;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const port = this.startPort + this.processes.size;
|
|
19
|
+
|
|
20
|
+
const p = spawn(["opencode", "serve", "--port", port.toString(), "--hostname", "0.0.0.0"], {
|
|
21
|
+
cwd: path,
|
|
22
|
+
env: { ...process.env, OPENCODE_CLIENT: 'mobile' },
|
|
23
|
+
stdout: 'ignore',
|
|
24
|
+
stderr: 'ignore',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.processes.set(name, p);
|
|
28
|
+
this.ports.set(name, port);
|
|
29
|
+
|
|
30
|
+
let attempts = 0;
|
|
31
|
+
const maxAttempts = 50;
|
|
32
|
+
|
|
33
|
+
while (attempts < maxAttempts) {
|
|
34
|
+
try {
|
|
35
|
+
const healthRes = await fetch(`http://127.0.0.1:${port}/global/health`);
|
|
36
|
+
if (healthRes.ok) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
}
|
|
41
|
+
await new Promise(r => setTimeout(r, 100));
|
|
42
|
+
attempts++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (attempts >= maxAttempts) {
|
|
46
|
+
console.error(`[ProcessManager] WARNING: Project ${name} may not have started correctly on port ${port}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return port;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stopAll() {
|
|
53
|
+
for (const [name, p] of this.processes) {
|
|
54
|
+
console.log(`Stopping ${name}...`);
|
|
55
|
+
p.kill();
|
|
56
|
+
}
|
|
57
|
+
this.processes.clear();
|
|
58
|
+
this.ports.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const processManager = new ProcessManager();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import { spawn } from 'bun';
|
|
3
|
+
import qrcode from 'qrcode-terminal';
|
|
4
|
+
import { loadConfig } from './config';
|
|
5
|
+
|
|
6
|
+
export async function startTunnel(port: number): Promise<string> {
|
|
7
|
+
console.log(`Starting Cloudflare tunnel...`);
|
|
8
|
+
|
|
9
|
+
const proc = spawn(["cloudflared", "tunnel", "--url", `http://localhost:${port}`], {
|
|
10
|
+
stdout: 'pipe',
|
|
11
|
+
stderr: 'pipe',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let found = false;
|
|
16
|
+
|
|
17
|
+
const reader = proc.stderr.getReader();
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
|
|
20
|
+
async function read() {
|
|
21
|
+
while (true) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done) break;
|
|
24
|
+
|
|
25
|
+
const text = decoder.decode(value);
|
|
26
|
+
// Silence output unless debug needed
|
|
27
|
+
// console.log(text);
|
|
28
|
+
|
|
29
|
+
const match = text.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
30
|
+
if (match && !found) {
|
|
31
|
+
found = true;
|
|
32
|
+
const url = match[0];
|
|
33
|
+
const config = await loadConfig();
|
|
34
|
+
const name = config.device_name;
|
|
35
|
+
|
|
36
|
+
console.log('\n');
|
|
37
|
+
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
38
|
+
console.log(`\nHub Online: ${name}`);
|
|
39
|
+
console.log(`URL: ${url}\n`);
|
|
40
|
+
|
|
41
|
+
resolve(url);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!found) reject("Failed to get tunnel URL");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
read();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|
package/install.js
CHANGED
|
@@ -38,6 +38,7 @@ async function install() {
|
|
|
38
38
|
|
|
39
39
|
const destPath = path.join(binDir, binary);
|
|
40
40
|
if (fs.existsSync(destPath)) {
|
|
41
|
+
addToPath(binDir);
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -106,6 +107,10 @@ function extract(file, dest, extension, binary) {
|
|
|
106
107
|
throw error;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
addToPath(dest);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function addToPath(dest) {
|
|
109
114
|
// Add to PATH on Windows if not present
|
|
110
115
|
if (os.platform() === 'win32') {
|
|
111
116
|
try {
|
|
@@ -113,7 +118,7 @@ function extract(file, dest, extension, binary) {
|
|
|
113
118
|
if (!userPath.includes(dest)) {
|
|
114
119
|
const newPath = `${userPath};${dest}`;
|
|
115
120
|
// Powershell command to set the new path
|
|
116
|
-
const setPathCmd = `[Environment]::SetEnvironmentVariable(
|
|
121
|
+
const setPathCmd = `[Environment]::SetEnvironmentVariable('Path', '${newPath.replace(/'/g, "''")}', [EnvironmentVariableTarget]::User)`;
|
|
117
122
|
execSync(`powershell -Command "${setPathCmd}"`);
|
|
118
123
|
console.log(`Added ${dest} to user PATH.`);
|
|
119
124
|
console.log('You may need to restart your terminal for changes to take effect.');
|