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 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/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("Path", "${newPath}", [EnvironmentVariableTarget]::User)`;
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.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidconnect",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "NPM wrapper for voidconnect-cli",
5
5
  "bin": {
6
6
  "voidconnect": "bin/voidconnect.js"