voidct 1.0.0 → 1.0.2
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 +0 -1
- package/package.json +2 -2
- package/src/config.ts +0 -2
- package/src/handlers.ts +89 -42
- package/src/hub/config.ts +0 -2
- package/src/hub/index.ts +26 -33
- package/src/hub/logger.ts +0 -2
- package/src/hub/process_manager.ts +7 -6
- package/src/index.ts +5 -28
- package/src/utils.ts +21 -45
package/README.md
CHANGED
|
@@ -36,7 +36,6 @@ bunx voidct status
|
|
|
36
36
|
| `add` | `<path>` `[--name <name>]` | Add a project path |
|
|
37
37
|
| `remove` | `<name>` | Remove a project |
|
|
38
38
|
| `config` | `--device-name`, `--password`, `--port` | Configure device settings |
|
|
39
|
-
| `update` | | Check for updates |
|
|
40
39
|
|
|
41
40
|
## Flags
|
|
42
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidct",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI tool for managing VoidConnect mobile app servers",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/xptea/
|
|
28
|
+
"url": "https://github.com/xptea/voidct"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
31
31
|
"cli",
|
package/src/config.ts
CHANGED
|
@@ -95,7 +95,6 @@ export function addProject(pathStr: string, name?: string): string {
|
|
|
95
95
|
throw new Error(`Invalid path '${pathStr}': Directory does not exist`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Clean path (remove \\?\ prefix on Windows)
|
|
99
98
|
let cleanPath = absolutePath;
|
|
100
99
|
if (cleanPath.startsWith('\\\\?\\')) {
|
|
101
100
|
cleanPath = cleanPath.slice(4);
|
|
@@ -103,7 +102,6 @@ export function addProject(pathStr: string, name?: string): string {
|
|
|
103
102
|
|
|
104
103
|
const projectName = name || path.basename(cleanPath);
|
|
105
104
|
|
|
106
|
-
// Check if project with same path exists
|
|
107
105
|
const existing = config.projects.find(p => p.path === cleanPath);
|
|
108
106
|
if (existing) {
|
|
109
107
|
existing.name = projectName;
|
package/src/handlers.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
getStatePath,
|
|
13
13
|
type HubConfig
|
|
14
14
|
} from './config';
|
|
15
|
-
import { isPortAvailable, findAvailablePort, isServerRunning, killServer, cleanupServerFiles } from './utils';
|
|
15
|
+
import { isPortAvailable, findAvailablePort, isServerRunning, killServer, cleanupServerFiles, getLocalIp } from './utils';
|
|
16
|
+
import qrcode from 'qrcode-terminal';
|
|
16
17
|
|
|
17
18
|
export interface RunOptions {
|
|
18
19
|
dir?: string;
|
|
@@ -25,6 +26,7 @@ export interface StartOptions {
|
|
|
25
26
|
bg?: boolean;
|
|
26
27
|
port?: number;
|
|
27
28
|
log?: boolean;
|
|
29
|
+
cors?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export async function handleRun(options: RunOptions): Promise<{ success: boolean; message: string }> {
|
|
@@ -36,7 +38,6 @@ export async function handleRun(options: RunOptions): Promise<{ success: boolean
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
// Determine project path
|
|
40
41
|
const projectPath = options.dir ? resolve(options.dir) : process.cwd();
|
|
41
42
|
|
|
42
43
|
if (!existsSync(projectPath)) {
|
|
@@ -91,7 +92,6 @@ export async function handleRun(options: RunOptions): Promise<{ success: boolean
|
|
|
91
92
|
|
|
92
93
|
writeFileSync(getPidPath(), child.pid.toString());
|
|
93
94
|
|
|
94
|
-
// Wait for process to exit
|
|
95
95
|
await child.exited;
|
|
96
96
|
cleanupServerFiles();
|
|
97
97
|
|
|
@@ -101,8 +101,7 @@ export async function handleRun(options: RunOptions): Promise<{ success: boolean
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export async function handleStart(options: StartOptions): Promise<{ success: boolean; message: string }> {
|
|
105
|
-
// Save state for restart
|
|
104
|
+
export async function handleStart(options: StartOptions): Promise<{ success: boolean; message: string }> {
|
|
106
105
|
saveState({ cf: options.cf ?? false, bg: options.bg ?? false });
|
|
107
106
|
|
|
108
107
|
const { running, pid } = isServerRunning();
|
|
@@ -144,6 +143,10 @@ export async function handleStart(options: StartOptions): Promise<{ success: boo
|
|
|
144
143
|
if (options.log) {
|
|
145
144
|
args.push('--log');
|
|
146
145
|
}
|
|
146
|
+
if (options.cors) {
|
|
147
|
+
args.push('--cors');
|
|
148
|
+
args.push(options.cors);
|
|
149
|
+
}
|
|
147
150
|
|
|
148
151
|
const env: Record<string, string> = {
|
|
149
152
|
...process.env as Record<string, string>,
|
|
@@ -156,41 +159,73 @@ export async function handleStart(options: StartOptions): Promise<{ success: boo
|
|
|
156
159
|
env.HUB_PASSWORD = config.password_hash;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
if (options.bg) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
162
|
+
if (options.bg) {
|
|
163
|
+
let pid: number | null = null;
|
|
164
|
+
|
|
165
|
+
if (process.platform === 'win32') {
|
|
166
|
+
const envOverrides: Record<string, string> = {
|
|
167
|
+
HUB_DEVICE_NAME: config.device_name,
|
|
168
|
+
HUB_START_DIR: process.cwd(),
|
|
169
|
+
PORT: port.toString()
|
|
170
|
+
};
|
|
171
|
+
if (config.password_hash) {
|
|
172
|
+
envOverrides.HUB_PASSWORD = config.password_hash;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ps = buildPowerShellStartCommand(envOverrides, process.execPath, args, hubPath);
|
|
176
|
+
const result = spawnSync(['powershell', '-NoProfile', '-NonInteractive', '-Command', ps], {
|
|
177
|
+
stdin: 'ignore',
|
|
178
|
+
stdout: 'pipe',
|
|
179
|
+
stderr: 'pipe'
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (result.exitCode !== 0) {
|
|
183
|
+
const errText = new TextDecoder().decode(result.stderr as Uint8Array).trim();
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
message: `Failed to start background server: ${errText || 'Unknown error'}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const outText = new TextDecoder().decode(result.stdout as Uint8Array).trim();
|
|
191
|
+
pid = parseInt(outText, 10);
|
|
192
|
+
} else {
|
|
193
|
+
const child = spawn([process.execPath, ...args], {
|
|
194
|
+
cwd: hubPath,
|
|
195
|
+
env,
|
|
196
|
+
stdin: 'ignore',
|
|
197
|
+
stdout: 'ignore',
|
|
198
|
+
stderr: 'ignore'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
pid = child.pid;
|
|
202
|
+
child.unref();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!pid) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
message: 'Failed to start background server: Missing PID'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
writeFileSync(getPidPath(), pid.toString());
|
|
213
|
+
|
|
214
|
+
const localIp = getLocalIp();
|
|
215
|
+
const name = config.device_name || 'VoidConnect Hub';
|
|
216
|
+
const url = `http://${localIp}:${port}`;
|
|
217
|
+
|
|
218
|
+
console.log('\n');
|
|
219
|
+
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
220
|
+
console.log(`\nHub Online: ${name}`);
|
|
221
|
+
console.log(`URL: ${url}\n`);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: `Server started in background (PID: ${pid}) on port ${port}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
186
228
|
|
|
187
|
-
return {
|
|
188
|
-
success: true,
|
|
189
|
-
message: `Server started in background (PID: ${child.pid}) on port ${port}`
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Foreground mode
|
|
194
229
|
const child = spawn(['bun', ...args], {
|
|
195
230
|
cwd: hubPath,
|
|
196
231
|
env,
|
|
@@ -322,7 +357,6 @@ export function getConfigInfo(): HubConfig {
|
|
|
322
357
|
}
|
|
323
358
|
|
|
324
359
|
function getHubPath(): string | null {
|
|
325
|
-
// Check various locations for the hub
|
|
326
360
|
const execPath = process.argv[1] || '';
|
|
327
361
|
const possiblePaths = [
|
|
328
362
|
join(dirname(execPath), 'hub'),
|
|
@@ -340,7 +374,7 @@ function getHubPath(): string | null {
|
|
|
340
374
|
return null;
|
|
341
375
|
}
|
|
342
376
|
|
|
343
|
-
async function ensureHubDependencies(hubPath: string): Promise<void> {
|
|
377
|
+
async function ensureHubDependencies(hubPath: string): Promise<void> {
|
|
344
378
|
const nodeModules = join(hubPath, '..', 'node_modules');
|
|
345
379
|
if (!existsSync(nodeModules)) {
|
|
346
380
|
console.log('First run detected. Installing dependencies...');
|
|
@@ -354,4 +388,17 @@ async function ensureHubDependencies(hubPath: string): Promise<void> {
|
|
|
354
388
|
}
|
|
355
389
|
console.log('Dependencies installed.\n');
|
|
356
390
|
}
|
|
357
|
-
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildPowerShellStartCommand(envVars: Record<string, string>, execPath: string, args: string[], cwd: string): string {
|
|
394
|
+
const setEnv = Object.entries(envVars)
|
|
395
|
+
.map(([key, value]) => `$env:${key}='${escapePowerShell(value)}';`)
|
|
396
|
+
.join(' ');
|
|
397
|
+
const argList = args.map(arg => `'${escapePowerShell(arg)}'`).join(', ');
|
|
398
|
+
|
|
399
|
+
return `${setEnv} $p = Start-Process -FilePath '${escapePowerShell(execPath)}' -ArgumentList @(${argList}) -WorkingDirectory '${escapePowerShell(cwd)}' -WindowStyle Hidden -PassThru; $p.Id`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function escapePowerShell(value: string): string {
|
|
403
|
+
return value.replace(/'/g, "''");
|
|
404
|
+
}
|
package/src/hub/config.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface HubConfig {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function loadConfig(): Promise<HubConfig> {
|
|
16
|
-
// Check for local run mode (single project via environment variable)
|
|
17
16
|
const projectPath = process.env.HUB_PROJECT_PATH;
|
|
18
17
|
const projectName = process.env.HUB_PROJECT_NAME;
|
|
19
18
|
|
|
@@ -25,7 +24,6 @@ export async function loadConfig(): Promise<HubConfig> {
|
|
|
25
24
|
};
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Normal mode: load from config file
|
|
29
27
|
const configPath = join(homedir(), '.voidconnect', 'hub_config.json');
|
|
30
28
|
const file = Bun.file(configPath);
|
|
31
29
|
|
package/src/hub/index.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
2
3
|
import { loadConfig } from './config';
|
|
3
4
|
import { authMiddleware } from './auth';
|
|
4
5
|
import { processManager } from './process_manager';
|
|
5
6
|
import qrcode from 'qrcode-terminal';
|
|
6
|
-
import { networkInterfaces } from 'os';
|
|
7
7
|
import { logTraffic, formatRequest, formatResponse } from './logger';
|
|
8
|
+
import { getLocalIp } from '../utils';
|
|
8
9
|
|
|
9
10
|
const app = new Hono();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const corsArgIndex = process.argv.findIndex(arg => arg === '--cors' || arg === '--corse');
|
|
13
|
+
if (corsArgIndex !== -1) {
|
|
14
|
+
const nextArg = process.argv[corsArgIndex + 1];
|
|
15
|
+
const origin = (nextArg && !nextArg.startsWith('-')) ? nextArg : '*';
|
|
16
|
+
console.log(`CORS enabled for origin: ${origin}`);
|
|
17
|
+
|
|
18
|
+
app.use('*', cors({
|
|
19
|
+
origin: origin,
|
|
20
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
|
|
21
|
+
allowHeaders: ['Content-Type', 'Authorization', 'x-requested-with'],
|
|
22
|
+
credentials: true,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
app.use('*', async (c, next) => {
|
|
13
27
|
if (process.argv.includes('--log')) {
|
|
14
28
|
try {
|
|
@@ -29,25 +43,20 @@ app.use('*', async (c, next) => {
|
|
|
29
43
|
}
|
|
30
44
|
});
|
|
31
45
|
|
|
32
|
-
// Auth middleware for protected routes
|
|
33
46
|
app.use('/api/*', authMiddleware);
|
|
34
47
|
app.use('/project/*', authMiddleware);
|
|
35
48
|
|
|
36
|
-
// Health check
|
|
37
49
|
app.get('/', (c) => c.text('VoidConnect Hub Running'));
|
|
38
50
|
|
|
39
|
-
// Status endpoint
|
|
40
51
|
app.get('/api/status', (c) => {
|
|
41
52
|
return c.json({ status: 'running', device: process.env.HUB_DEVICE_NAME || 'Unknown' });
|
|
42
53
|
});
|
|
43
54
|
|
|
44
|
-
// List projects
|
|
45
55
|
app.get('/api/projects', async (c) => {
|
|
46
56
|
const config = await loadConfig();
|
|
47
57
|
return c.json(config.projects);
|
|
48
58
|
});
|
|
49
59
|
|
|
50
|
-
// Project proxy
|
|
51
60
|
app.all('/project/:name/*', async (c) => {
|
|
52
61
|
const name = c.req.param('name');
|
|
53
62
|
const config = await loadConfig();
|
|
@@ -96,39 +105,23 @@ app.all('/project/:name/*', async (c) => {
|
|
|
96
105
|
|
|
97
106
|
const port = parseInt(process.env.PORT || '3838');
|
|
98
107
|
|
|
99
|
-
// Start with Cloudflare tunnel or local mode
|
|
100
108
|
if (process.argv.includes('--tunnel') || process.argv.includes('--cf')) {
|
|
101
109
|
import('./tunnel').then(({ startTunnel }) => {
|
|
102
110
|
startTunnel(port);
|
|
103
111
|
});
|
|
104
112
|
} else {
|
|
105
113
|
(async () => {
|
|
106
|
-
const
|
|
107
|
-
let localIp = 'localhost';
|
|
108
|
-
|
|
109
|
-
for (const name of Object.keys(nets)) {
|
|
110
|
-
const interfaces = nets[name];
|
|
111
|
-
if (interfaces) {
|
|
112
|
-
for (const net of interfaces) {
|
|
113
|
-
if (net.family === 'IPv4' && !net.internal) {
|
|
114
|
-
localIp = net.address;
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (localIp !== 'localhost') break;
|
|
120
|
-
}
|
|
121
|
-
|
|
114
|
+
const localIp = getLocalIp();
|
|
122
115
|
const config = await loadConfig();
|
|
123
|
-
const name = process.env.HUB_DEVICE_NAME || config.device_name || 'My PC';
|
|
124
|
-
const url = `http://${localIp}:${port}`;
|
|
125
|
-
|
|
126
|
-
console.log('\n');
|
|
127
|
-
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
128
|
-
console.log(`\nHub Online: ${name}`);
|
|
129
|
-
console.log(`URL: ${url}\n`);
|
|
130
|
-
})();
|
|
131
|
-
}
|
|
116
|
+
const name = process.env.HUB_DEVICE_NAME || config.device_name || 'My PC';
|
|
117
|
+
const url = `http://${localIp}:${port}`;
|
|
118
|
+
|
|
119
|
+
console.log('\n');
|
|
120
|
+
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
121
|
+
console.log(`\nHub Online: ${name}`);
|
|
122
|
+
console.log(`URL: ${url}\n`);
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
132
125
|
|
|
133
126
|
const server = Bun.serve({
|
|
134
127
|
port,
|
package/src/hub/logger.ts
CHANGED
|
@@ -18,7 +18,6 @@ export async function formatRequest(req: Request) {
|
|
|
18
18
|
let body = '(empty)';
|
|
19
19
|
try {
|
|
20
20
|
if (req.body) {
|
|
21
|
-
// Clone to not consume the stream needed for processing
|
|
22
21
|
body = await req.clone().text();
|
|
23
22
|
}
|
|
24
23
|
} catch (e) {
|
|
@@ -36,7 +35,6 @@ export async function formatRequest(req: Request) {
|
|
|
36
35
|
export async function formatResponse(res: Response) {
|
|
37
36
|
let body = '(empty)';
|
|
38
37
|
try {
|
|
39
|
-
// Clone response to read body without consuming the original stream
|
|
40
38
|
if (res.body) {
|
|
41
39
|
body = await res.clone().text();
|
|
42
40
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'bun';
|
|
2
|
+
import { getLocalIp } from '../utils';
|
|
2
3
|
|
|
3
4
|
export class ProcessManager {
|
|
4
5
|
private processes: Map<string, any> = new Map();
|
|
@@ -15,17 +16,18 @@ export class ProcessManager {
|
|
|
15
16
|
|
|
16
17
|
const port = this.startPort + this.processes.size;
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const opencodeCmd = ["bun", "x", "opencode", "serve", "--port", port.toString(), "--hostname", "0.0.0.0"];
|
|
20
|
+
const localIp = getLocalIp();
|
|
21
|
+
const p = spawn(opencodeCmd, {
|
|
22
|
+
cwd: path,
|
|
23
|
+
env: { ...process.env, OPENCODE_CLIENT: 'mobile' },
|
|
24
|
+
stdout: 'ignore',
|
|
22
25
|
stderr: 'ignore',
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
this.processes.set(name, p);
|
|
26
29
|
this.ports.set(name, port);
|
|
27
30
|
|
|
28
|
-
// Wait for server to be ready
|
|
29
31
|
let attempts = 0;
|
|
30
32
|
const maxAttempts = 50;
|
|
31
33
|
|
|
@@ -36,7 +38,6 @@ export class ProcessManager {
|
|
|
36
38
|
break;
|
|
37
39
|
}
|
|
38
40
|
} catch (e) {
|
|
39
|
-
// Server not ready yet
|
|
40
41
|
}
|
|
41
42
|
await new Promise(r => setTimeout(r, 100));
|
|
42
43
|
attempts++;
|
package/src/index.ts
CHANGED
|
@@ -11,11 +11,9 @@ import {
|
|
|
11
11
|
getConfigInfo,
|
|
12
12
|
type StatusInfo
|
|
13
13
|
} from "./handlers";
|
|
14
|
-
import { checkForUpdates } from "./utils";
|
|
15
14
|
|
|
16
15
|
const VERSION = "1.0.0";
|
|
17
16
|
|
|
18
|
-
// ANSI color codes
|
|
19
17
|
const Colors = {
|
|
20
18
|
reset: "\x1b[0m",
|
|
21
19
|
bold: "\x1b[1m",
|
|
@@ -27,7 +25,6 @@ const Colors = {
|
|
|
27
25
|
white: "\x1b[37m"
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
// Parse command line arguments
|
|
31
28
|
function parseArgs() {
|
|
32
29
|
const args = process.argv.slice(2);
|
|
33
30
|
const command = args[0];
|
|
@@ -54,23 +51,16 @@ function parseArgs() {
|
|
|
54
51
|
return { command, flags, positional };
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
// Print helpers
|
|
58
54
|
function printBox(lines: string[]) {
|
|
59
55
|
const width = 63;
|
|
60
56
|
console.log(`${Colors.cyan}┌${'─'.repeat(width)}┐${Colors.reset}`);
|
|
61
57
|
for (const line of lines) {
|
|
62
|
-
// Strip ANSI codes to calculate visible length
|
|
63
58
|
const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
64
59
|
const padding = width - visibleLen - 2;
|
|
65
60
|
console.log(`${Colors.cyan}│${Colors.reset} ${line}${' '.repeat(Math.max(0, padding))}${Colors.cyan}│${Colors.reset}`);
|
|
66
61
|
}
|
|
67
62
|
console.log(`${Colors.cyan}└${'─'.repeat(width)}┘${Colors.reset}`);
|
|
68
63
|
}
|
|
69
|
-
|
|
70
|
-
function printDivider() {
|
|
71
|
-
console.log(`${Colors.cyan}├${'─'.repeat(63)}┤${Colors.reset}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
function printSuccess(msg: string) {
|
|
75
65
|
console.log(`\n${Colors.green}✓${Colors.reset} ${msg}`);
|
|
76
66
|
}
|
|
@@ -83,7 +73,6 @@ function printWarning(msg: string) {
|
|
|
83
73
|
console.log(`\n${Colors.yellow}!${Colors.reset} ${msg}`);
|
|
84
74
|
}
|
|
85
75
|
|
|
86
|
-
// Display functions
|
|
87
76
|
function showHelp() {
|
|
88
77
|
console.log(`\n${Colors.cyan}${Colors.bold}VoidConnect CLI v${VERSION}${Colors.reset}`);
|
|
89
78
|
console.log(`${Colors.dim}CLI tool for managing VoidConnect mobile app servers${Colors.reset}`);
|
|
@@ -100,7 +89,6 @@ function showHelp() {
|
|
|
100
89
|
console.log(`${Colors.dim} remove Remove a project from configuration${Colors.reset}`);
|
|
101
90
|
console.log(`${Colors.dim} restart Restart the background server${Colors.reset}`);
|
|
102
91
|
console.log(`${Colors.dim} config Configure device settings${Colors.reset}`);
|
|
103
|
-
console.log(`${Colors.dim} update Check for updates${Colors.reset}`);
|
|
104
92
|
console.log();
|
|
105
93
|
console.log(`${Colors.white}${Colors.bold}Quick Start:${Colors.reset}`);
|
|
106
94
|
console.log(`${Colors.dim} • Run 'voidct run' to serve the current directory${Colors.reset}`);
|
|
@@ -146,7 +134,6 @@ function showStatus(status: StatusInfo) {
|
|
|
146
134
|
console.log(`${Colors.dim} add <path> [--name] Add a project${Colors.reset}`);
|
|
147
135
|
console.log(`${Colors.dim} remove <name> Remove a project${Colors.reset}`);
|
|
148
136
|
console.log(`${Colors.dim} config Configure settings${Colors.reset}`);
|
|
149
|
-
console.log(`${Colors.dim} update Check for updates${Colors.reset}`);
|
|
150
137
|
console.log();
|
|
151
138
|
}
|
|
152
139
|
|
|
@@ -176,7 +163,6 @@ function showAddSuccess(projectName: string) {
|
|
|
176
163
|
console.log();
|
|
177
164
|
}
|
|
178
165
|
|
|
179
|
-
// Main function
|
|
180
166
|
async function main() {
|
|
181
167
|
const { command, flags, positional } = parseArgs();
|
|
182
168
|
|
|
@@ -197,11 +183,15 @@ async function main() {
|
|
|
197
183
|
}
|
|
198
184
|
|
|
199
185
|
case 'start': {
|
|
186
|
+
const corsFlag = flags.cors || flags.corse;
|
|
187
|
+
const corsValue = corsFlag === true ? '*' : (corsFlag as string | undefined);
|
|
188
|
+
|
|
200
189
|
const result = await handleStart({
|
|
201
190
|
cf: !!flags.cf,
|
|
202
191
|
bg: !!flags.bg,
|
|
203
192
|
port: flags.port ? parseInt(flags.port as string) : undefined,
|
|
204
|
-
log: !!flags.log
|
|
193
|
+
log: !!flags.log,
|
|
194
|
+
cors: corsValue
|
|
205
195
|
});
|
|
206
196
|
if (result.success) {
|
|
207
197
|
printSuccess(result.message);
|
|
@@ -287,19 +277,6 @@ async function main() {
|
|
|
287
277
|
break;
|
|
288
278
|
}
|
|
289
279
|
|
|
290
|
-
case 'update': {
|
|
291
|
-
console.log(`\n${Colors.dim}Checking for updates...${Colors.reset}`);
|
|
292
|
-
const update = await checkForUpdates(VERSION);
|
|
293
|
-
if (update.hasUpdate && update.version) {
|
|
294
|
-
printWarning(`Update available! v${VERSION} → v${update.version}`);
|
|
295
|
-
console.log(`${Colors.dim}Download: ${update.url}${Colors.reset}`);
|
|
296
|
-
} else {
|
|
297
|
-
printSuccess(`Already up to date (v${VERSION})`);
|
|
298
|
-
}
|
|
299
|
-
console.log();
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
280
|
default:
|
|
304
281
|
showHelp();
|
|
305
282
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { createServer } from 'net';
|
|
3
3
|
import { getPidPath, getStatePath } from './config';
|
|
4
|
+
import { networkInterfaces } from 'os';
|
|
5
|
+
|
|
6
|
+
export function getLocalIp(): string {
|
|
7
|
+
const nets = networkInterfaces();
|
|
8
|
+
let localIp = 'localhost';
|
|
9
|
+
|
|
10
|
+
for (const name of Object.keys(nets)) {
|
|
11
|
+
const interfaces = nets[name];
|
|
12
|
+
if (interfaces) {
|
|
13
|
+
for (const net of interfaces) {
|
|
14
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
15
|
+
localIp = net.address;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (localIp !== 'localhost') break;
|
|
21
|
+
}
|
|
22
|
+
return localIp;
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export function isPortAvailable(port: number): Promise<boolean> {
|
|
6
26
|
return new Promise((resolve) => {
|
|
@@ -37,12 +57,10 @@ export function isServerRunning(): { running: boolean; pid: number | null } {
|
|
|
37
57
|
const content = readFileSync(pidPath, 'utf-8');
|
|
38
58
|
const pid = parseInt(content.trim(), 10);
|
|
39
59
|
|
|
40
|
-
// Check if process is running
|
|
41
60
|
try {
|
|
42
|
-
process.kill(pid, 0);
|
|
61
|
+
process.kill(pid, 0);
|
|
43
62
|
return { running: true, pid };
|
|
44
63
|
} catch {
|
|
45
|
-
// Process not running, clean up stale PID file
|
|
46
64
|
unlinkSync(pidPath);
|
|
47
65
|
return { running: false, pid: null };
|
|
48
66
|
}
|
|
@@ -68,45 +86,3 @@ export function cleanupServerFiles(): void {
|
|
|
68
86
|
try { unlinkSync(statePath); } catch { }
|
|
69
87
|
}
|
|
70
88
|
|
|
71
|
-
export async function checkForUpdates(currentVersion: string): Promise<{ hasUpdate: boolean; version?: string; url?: string }> {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch('https://api.github.com/repos/xptea/voidconnect-cli/releases/latest', {
|
|
74
|
-
headers: {
|
|
75
|
-
'User-Agent': 'voidconnect-cli-update-checker'
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
return { hasUpdate: false };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const data = await response.json() as { tag_name: string; html_url: string };
|
|
84
|
-
const remoteVersion = data.tag_name.replace(/^v/i, '');
|
|
85
|
-
|
|
86
|
-
if (isNewerVersion(remoteVersion, currentVersion)) {
|
|
87
|
-
return {
|
|
88
|
-
hasUpdate: true,
|
|
89
|
-
version: remoteVersion,
|
|
90
|
-
url: data.html_url
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { hasUpdate: false };
|
|
95
|
-
} catch {
|
|
96
|
-
return { hasUpdate: false };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isNewerVersion(remote: string, local: string): boolean {
|
|
101
|
-
const parse = (v: string) => v.split('.').map(s => parseInt(s, 10) || 0);
|
|
102
|
-
const rParts = parse(remote);
|
|
103
|
-
const lParts = parse(local);
|
|
104
|
-
|
|
105
|
-
for (let i = 0; i < Math.max(rParts.length, lParts.length); i++) {
|
|
106
|
-
const r = rParts[i] || 0;
|
|
107
|
-
const l = lParts[i] || 0;
|
|
108
|
-
if (r > l) return true;
|
|
109
|
-
if (r < l) return false;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|