voidconnect 0.1.13 → 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 ADDED
@@ -0,0 +1,15 @@
1
+ # hub
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
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!");
@@ -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
+ }
@@ -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
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidconnect",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "NPM wrapper for voidconnect-cli",
5
5
  "bin": {
6
6
  "voidconnect": "bin/voidconnect.js"