voidct 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 +99 -0
- package/bun.lock +41 -0
- package/package.json +39 -0
- package/src/config.ts +131 -0
- package/src/handlers.ts +357 -0
- package/src/hub/auth.ts +18 -0
- package/src/hub/config.ts +42 -0
- package/src/hub/index.ts +137 -0
- package/src/hub/logger.ts +53 -0
- package/src/hub/process_manager.ts +62 -0
- package/src/hub/tunnel.ts +46 -0
- package/src/index.ts +312 -0
- package/src/utils.ts +112 -0
- package/tsconfig.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# VoidConnect CLI
|
|
2
|
+
|
|
3
|
+
CLI tool for managing VoidConnect mobile app servers. Built with [OpenTUI](https://github.com/nicholasgriffintn/opentui) for beautiful terminal interfaces.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx voidct
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Serve the current directory
|
|
15
|
+
bunx voidct run
|
|
16
|
+
|
|
17
|
+
# Add a project to your saved list
|
|
18
|
+
bunx voidct add /path/to/project --name "My Project"
|
|
19
|
+
|
|
20
|
+
# Start serving all saved projects
|
|
21
|
+
bunx voidct start
|
|
22
|
+
|
|
23
|
+
# Check status
|
|
24
|
+
bunx voidct status
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | Arguments | Description |
|
|
30
|
+
|---------|-----------|-------------|
|
|
31
|
+
| `run` | `--dir <path>`, `--cf`, `--port <port>` | Run a local project (foreground) |
|
|
32
|
+
| `start` | `--cf`, `--bg`, `--port <port>`, `--log` | Start server with saved projects |
|
|
33
|
+
| `stop` | | Stop the background server |
|
|
34
|
+
| `restart` | | Restart the server |
|
|
35
|
+
| `status` | | Show status dashboard |
|
|
36
|
+
| `add` | `<path>` `[--name <name>]` | Add a project path |
|
|
37
|
+
| `remove` | `<name>` | Remove a project |
|
|
38
|
+
| `config` | `--device-name`, `--password`, `--port` | Configure device settings |
|
|
39
|
+
| `update` | | Check for updates |
|
|
40
|
+
|
|
41
|
+
## Flags
|
|
42
|
+
|
|
43
|
+
### Global Flags
|
|
44
|
+
- `--cf` - Enable Cloudflare tunnel for external access
|
|
45
|
+
- `--port <port>` - Specify a custom port (default: 3838)
|
|
46
|
+
|
|
47
|
+
### Start Command Flags
|
|
48
|
+
- `--bg` - Run server in background mode
|
|
49
|
+
- `--log` - Enable traffic logging to `void_traffic.log` in the current directory
|
|
50
|
+
|
|
51
|
+
### Config Command Flags
|
|
52
|
+
- `--device-name <name>` - Set the device name shown to mobile clients
|
|
53
|
+
- `--password <pwd>` - Set a password for authentication
|
|
54
|
+
- `--no-password` - Remove password protection
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
Configuration is stored in `~/.voidconnect/hub_config.json` and includes:
|
|
59
|
+
- Device name
|
|
60
|
+
- Password (optional)
|
|
61
|
+
- Default port
|
|
62
|
+
- Saved projects list
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
src/
|
|
68
|
+
├── index.tsx # Main CLI entry (OpenTUI/React UI)
|
|
69
|
+
├── config.ts # Configuration management
|
|
70
|
+
├── handlers.ts # Command handlers
|
|
71
|
+
├── utils.ts # Utility functions
|
|
72
|
+
└── hub/ # Hub server
|
|
73
|
+
├── index.ts # Hono web server
|
|
74
|
+
├── auth.ts # Authentication middleware
|
|
75
|
+
├── config.ts # Hub config loader
|
|
76
|
+
├── logger.ts # Traffic logging
|
|
77
|
+
├── process_manager.ts # OpenCode process management
|
|
78
|
+
└── tunnel.ts # Cloudflare tunnel integration
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Install dependencies
|
|
85
|
+
bun install
|
|
86
|
+
|
|
87
|
+
# Run in development mode
|
|
88
|
+
bun run dev
|
|
89
|
+
|
|
90
|
+
# Run the CLI directly
|
|
91
|
+
bun run src/index.tsx [command]
|
|
92
|
+
|
|
93
|
+
# Run the hub server directly
|
|
94
|
+
bun run src/hub/index.ts
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/bun.lock
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "react",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"hono": "^4.0.0",
|
|
9
|
+
"qrcode-terminal": "^0.12.0",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
|
|
22
|
+
|
|
23
|
+
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
|
24
|
+
|
|
25
|
+
"@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
|
|
26
|
+
|
|
27
|
+
"@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="],
|
|
28
|
+
|
|
29
|
+
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
|
|
30
|
+
|
|
31
|
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
32
|
+
|
|
33
|
+
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
|
34
|
+
|
|
35
|
+
"qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
|
|
36
|
+
|
|
37
|
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
|
38
|
+
|
|
39
|
+
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voidct",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for managing VoidConnect mobile app servers",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"voidct": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "bun run --watch src/index.ts",
|
|
12
|
+
"start": "bun run src/index.ts",
|
|
13
|
+
"hub": "bun run src/hub/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "latest",
|
|
17
|
+
"@types/qrcode-terminal": "^0.12.2"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"hono": "^4.0.0",
|
|
24
|
+
"qrcode-terminal": "^0.12.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/xptea/voidconnect-cli"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"cli",
|
|
32
|
+
"voidconnect",
|
|
33
|
+
"mobile",
|
|
34
|
+
"server",
|
|
35
|
+
"tunnel"
|
|
36
|
+
],
|
|
37
|
+
"author": "xptea",
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
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
|
+
port: number | null;
|
|
14
|
+
projects: ProjectConfig[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServerState {
|
|
18
|
+
cf: boolean;
|
|
19
|
+
bg: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getConfigDir(): string {
|
|
23
|
+
const dir = join(homedir(), '.voidconnect');
|
|
24
|
+
if (!existsSync(dir)) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getConfigPath(): string {
|
|
31
|
+
return join(getConfigDir(), 'hub_config.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getPidPath(): string {
|
|
35
|
+
return join(getConfigDir(), 'server.pid');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getStatePath(): string {
|
|
39
|
+
return join(getConfigDir(), 'server.state');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadConfig(): HubConfig {
|
|
43
|
+
const path = getConfigPath();
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
return {
|
|
46
|
+
device_name: 'VoidConnect Hub',
|
|
47
|
+
password_hash: null,
|
|
48
|
+
port: null,
|
|
49
|
+
projects: []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const content = readFileSync(path, 'utf-8');
|
|
54
|
+
return JSON.parse(content) as HubConfig;
|
|
55
|
+
} catch {
|
|
56
|
+
return {
|
|
57
|
+
device_name: 'VoidConnect Hub',
|
|
58
|
+
password_hash: null,
|
|
59
|
+
port: null,
|
|
60
|
+
projects: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveConfig(config: HubConfig): void {
|
|
66
|
+
const path = getConfigPath();
|
|
67
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveState(state: ServerState): void {
|
|
71
|
+
const path = getStatePath();
|
|
72
|
+
writeFileSync(path, JSON.stringify(state));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadState(): ServerState | null {
|
|
76
|
+
const path = getStatePath();
|
|
77
|
+
if (!existsSync(path)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(path, 'utf-8');
|
|
82
|
+
return JSON.parse(content) as ServerState;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function addProject(pathStr: string, name?: string): string {
|
|
89
|
+
const config = loadConfig();
|
|
90
|
+
const fs = require('fs');
|
|
91
|
+
const path = require('path');
|
|
92
|
+
|
|
93
|
+
const absolutePath = path.resolve(pathStr);
|
|
94
|
+
if (!fs.existsSync(absolutePath)) {
|
|
95
|
+
throw new Error(`Invalid path '${pathStr}': Directory does not exist`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clean path (remove \\?\ prefix on Windows)
|
|
99
|
+
let cleanPath = absolutePath;
|
|
100
|
+
if (cleanPath.startsWith('\\\\?\\')) {
|
|
101
|
+
cleanPath = cleanPath.slice(4);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const projectName = name || path.basename(cleanPath);
|
|
105
|
+
|
|
106
|
+
// Check if project with same path exists
|
|
107
|
+
const existing = config.projects.find(p => p.path === cleanPath);
|
|
108
|
+
if (existing) {
|
|
109
|
+
existing.name = projectName;
|
|
110
|
+
} else {
|
|
111
|
+
config.projects.push({
|
|
112
|
+
name: projectName,
|
|
113
|
+
path: cleanPath
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
saveConfig(config);
|
|
118
|
+
return projectName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function removeProject(name: string): boolean {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
const originalLen = config.projects.length;
|
|
124
|
+
config.projects = config.projects.filter(p => p.name !== name);
|
|
125
|
+
|
|
126
|
+
if (config.projects.length < originalLen) {
|
|
127
|
+
saveConfig(config);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
package/src/handlers.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'bun';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { resolve, basename, dirname, join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
saveState,
|
|
8
|
+
loadState,
|
|
9
|
+
addProject,
|
|
10
|
+
removeProject,
|
|
11
|
+
getPidPath,
|
|
12
|
+
getStatePath,
|
|
13
|
+
type HubConfig
|
|
14
|
+
} from './config';
|
|
15
|
+
import { isPortAvailable, findAvailablePort, isServerRunning, killServer, cleanupServerFiles } from './utils';
|
|
16
|
+
|
|
17
|
+
export interface RunOptions {
|
|
18
|
+
dir?: string;
|
|
19
|
+
cf?: boolean;
|
|
20
|
+
port?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StartOptions {
|
|
24
|
+
cf?: boolean;
|
|
25
|
+
bg?: boolean;
|
|
26
|
+
port?: number;
|
|
27
|
+
log?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleRun(options: RunOptions): Promise<{ success: boolean; message: string }> {
|
|
31
|
+
const { running, pid } = isServerRunning();
|
|
32
|
+
if (running && pid) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
message: `A server is already running (PID: ${pid}). Stop it first with 'voidct stop'.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Determine project path
|
|
40
|
+
const projectPath = options.dir ? resolve(options.dir) : process.cwd();
|
|
41
|
+
|
|
42
|
+
if (!existsSync(projectPath)) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Invalid directory '${projectPath}': Directory does not exist`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const projectName = basename(projectPath);
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
const requestedPort = options.port ?? config.port ?? 3838;
|
|
52
|
+
|
|
53
|
+
let port = requestedPort;
|
|
54
|
+
if (!(await isPortAvailable(port))) {
|
|
55
|
+
port = await findAvailablePort(port + 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hubPath = getHubPath();
|
|
59
|
+
if (!hubPath) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: 'Hub directory not found'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await ensureHubDependencies(hubPath);
|
|
67
|
+
|
|
68
|
+
const args = ['run', join(hubPath, 'index.ts')];
|
|
69
|
+
if (options.cf) {
|
|
70
|
+
args.push('--cf');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const env: Record<string, string> = {
|
|
74
|
+
...process.env as Record<string, string>,
|
|
75
|
+
HUB_PROJECT_PATH: projectPath,
|
|
76
|
+
HUB_PROJECT_NAME: projectName,
|
|
77
|
+
HUB_DEVICE_NAME: config.device_name,
|
|
78
|
+
PORT: port.toString()
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (config.password_hash) {
|
|
82
|
+
env.HUB_PASSWORD = config.password_hash;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const child = spawn(['bun', ...args], {
|
|
86
|
+
cwd: hubPath,
|
|
87
|
+
env,
|
|
88
|
+
stdout: 'inherit',
|
|
89
|
+
stderr: 'inherit'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
writeFileSync(getPidPath(), child.pid.toString());
|
|
93
|
+
|
|
94
|
+
// Wait for process to exit
|
|
95
|
+
await child.exited;
|
|
96
|
+
cleanupServerFiles();
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
message: `Running project "${projectName}" on port ${port}`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function handleStart(options: StartOptions): Promise<{ success: boolean; message: string }> {
|
|
105
|
+
// Save state for restart
|
|
106
|
+
saveState({ cf: options.cf ?? false, bg: options.bg ?? false });
|
|
107
|
+
|
|
108
|
+
const { running, pid } = isServerRunning();
|
|
109
|
+
if (running && pid) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
message: `A server is already running (PID: ${pid}). Stop it first with 'voidct stop'.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
if (config.projects.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
message: "No projects configured. Add a project first with 'voidct add <path>'."
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requestedPort = options.port ?? config.port ?? 3838;
|
|
125
|
+
let port = requestedPort;
|
|
126
|
+
if (!(await isPortAvailable(port))) {
|
|
127
|
+
port = await findAvailablePort(port + 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hubPath = getHubPath();
|
|
131
|
+
if (!hubPath) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
message: 'Hub directory not found'
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await ensureHubDependencies(hubPath);
|
|
139
|
+
|
|
140
|
+
const args = ['run', join(hubPath, 'index.ts')];
|
|
141
|
+
if (options.cf) {
|
|
142
|
+
args.push('--cf');
|
|
143
|
+
}
|
|
144
|
+
if (options.log) {
|
|
145
|
+
args.push('--log');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const env: Record<string, string> = {
|
|
149
|
+
...process.env as Record<string, string>,
|
|
150
|
+
HUB_DEVICE_NAME: config.device_name,
|
|
151
|
+
HUB_START_DIR: process.cwd(),
|
|
152
|
+
PORT: port.toString()
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (config.password_hash) {
|
|
156
|
+
env.HUB_PASSWORD = config.password_hash;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (options.bg) {
|
|
160
|
+
// Background mode
|
|
161
|
+
const child = spawn(['bun', ...args], {
|
|
162
|
+
cwd: hubPath,
|
|
163
|
+
env,
|
|
164
|
+
stdout: 'pipe',
|
|
165
|
+
stderr: 'pipe'
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
writeFileSync(getPidPath(), child.pid.toString());
|
|
169
|
+
|
|
170
|
+
// Wait for server to output URL
|
|
171
|
+
const reader = child.stdout.getReader();
|
|
172
|
+
const decoder = new TextDecoder();
|
|
173
|
+
|
|
174
|
+
let output = '';
|
|
175
|
+
while (true) {
|
|
176
|
+
const { done, value } = await reader.read();
|
|
177
|
+
if (done) break;
|
|
178
|
+
|
|
179
|
+
output += decoder.decode(value);
|
|
180
|
+
console.log(decoder.decode(value));
|
|
181
|
+
|
|
182
|
+
if (output.includes('URL: http')) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
message: `Server started in background (PID: ${child.pid}) on port ${port}`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Foreground mode
|
|
194
|
+
const child = spawn(['bun', ...args], {
|
|
195
|
+
cwd: hubPath,
|
|
196
|
+
env,
|
|
197
|
+
stdout: 'inherit',
|
|
198
|
+
stderr: 'inherit'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
writeFileSync(getPidPath(), child.pid.toString());
|
|
202
|
+
await child.exited;
|
|
203
|
+
cleanupServerFiles();
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
message: `Server stopped`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function handleStop(): Promise<{ success: boolean; message: string }> {
|
|
212
|
+
const { running, pid } = isServerRunning();
|
|
213
|
+
|
|
214
|
+
if (!running || !pid) {
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
message: 'Server is not running'
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (killServer(pid)) {
|
|
222
|
+
cleanupServerFiles();
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: `Server (PID: ${pid}) stopped successfully`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: `Failed to stop server (PID: ${pid})`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function handleRestart(): Promise<{ success: boolean; message: string }> {
|
|
236
|
+
const state = loadState();
|
|
237
|
+
const cf = state?.cf ?? false;
|
|
238
|
+
const bg = state?.bg ?? false;
|
|
239
|
+
|
|
240
|
+
await handleStop();
|
|
241
|
+
await new Promise(r => setTimeout(r, 500));
|
|
242
|
+
return handleStart({ cf, bg });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function handleAdd(path: string, name?: string): Promise<{ success: boolean; message: string }> {
|
|
246
|
+
try {
|
|
247
|
+
const savedName = addProject(path, name);
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
message: `Project '${savedName}' has been saved!`
|
|
251
|
+
};
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
message: `Failed to add project: ${e}`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function handleRemove(name: string): Promise<{ success: boolean; message: string }> {
|
|
261
|
+
if (removeProject(name)) {
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
message: `Project '${name}' removed successfully`
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
message: `Project '${name}' not found`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface StatusInfo {
|
|
274
|
+
deviceName: string;
|
|
275
|
+
running: boolean;
|
|
276
|
+
pid: number | null;
|
|
277
|
+
projects: Array<{ name: string; path: string }>;
|
|
278
|
+
port: number | null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function handleStatus(): Promise<StatusInfo> {
|
|
282
|
+
const config = loadConfig();
|
|
283
|
+
const { running, pid } = isServerRunning();
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
deviceName: config.device_name,
|
|
287
|
+
running,
|
|
288
|
+
pid,
|
|
289
|
+
projects: config.projects,
|
|
290
|
+
port: config.port
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface ConfigOptions {
|
|
295
|
+
deviceName?: string;
|
|
296
|
+
password?: string | null;
|
|
297
|
+
port?: number | null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function handleConfig(options: ConfigOptions): Promise<{ success: boolean; message: string }> {
|
|
301
|
+
const config = loadConfig();
|
|
302
|
+
|
|
303
|
+
if (options.deviceName !== undefined) {
|
|
304
|
+
config.device_name = options.deviceName;
|
|
305
|
+
}
|
|
306
|
+
if (options.password !== undefined) {
|
|
307
|
+
config.password_hash = options.password;
|
|
308
|
+
}
|
|
309
|
+
if (options.port !== undefined) {
|
|
310
|
+
config.port = options.port;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
saveConfig(config);
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
message: 'Configuration updated successfully'
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function getConfigInfo(): HubConfig {
|
|
321
|
+
return loadConfig();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getHubPath(): string | null {
|
|
325
|
+
// Check various locations for the hub
|
|
326
|
+
const execPath = process.argv[1] || '';
|
|
327
|
+
const possiblePaths = [
|
|
328
|
+
join(dirname(execPath), 'hub'),
|
|
329
|
+
join(dirname(execPath), '..', 'hub'),
|
|
330
|
+
join(process.cwd(), 'src', 'hub'),
|
|
331
|
+
join(process.cwd(), 'hub'),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
for (const p of possiblePaths) {
|
|
335
|
+
if (existsSync(join(p, 'index.ts'))) {
|
|
336
|
+
return p;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function ensureHubDependencies(hubPath: string): Promise<void> {
|
|
344
|
+
const nodeModules = join(hubPath, '..', 'node_modules');
|
|
345
|
+
if (!existsSync(nodeModules)) {
|
|
346
|
+
console.log('First run detected. Installing dependencies...');
|
|
347
|
+
const result = spawnSync(['bun', 'install'], {
|
|
348
|
+
cwd: join(hubPath, '..'),
|
|
349
|
+
stdout: 'inherit',
|
|
350
|
+
stderr: 'inherit'
|
|
351
|
+
});
|
|
352
|
+
if (result.exitCode !== 0) {
|
|
353
|
+
throw new Error('Failed to install dependencies');
|
|
354
|
+
}
|
|
355
|
+
console.log('Dependencies installed.\n');
|
|
356
|
+
}
|
|
357
|
+
}
|
package/src/hub/auth.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Context, type Next } from 'hono';
|
|
2
|
+
|
|
3
|
+
export async function authMiddleware(c: Context, next: Next) {
|
|
4
|
+
const hubPassword = process.env.HUB_PASSWORD;
|
|
5
|
+
|
|
6
|
+
if (!hubPassword) {
|
|
7
|
+
await next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const authHeader = c.req.header('Authorization');
|
|
12
|
+
|
|
13
|
+
if (!authHeader || authHeader !== `Bearer ${hubPassword}`) {
|
|
14
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await next();
|
|
18
|
+
}
|