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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
|
|
4
|
+
export interface ProjectConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface HubConfig {
|
|
10
|
+
device_name: string;
|
|
11
|
+
password_hash: string | null;
|
|
12
|
+
projects: ProjectConfig[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function loadConfig(): Promise<HubConfig> {
|
|
16
|
+
// Check for local run mode (single project via environment variable)
|
|
17
|
+
const projectPath = process.env.HUB_PROJECT_PATH;
|
|
18
|
+
const projectName = process.env.HUB_PROJECT_NAME;
|
|
19
|
+
|
|
20
|
+
if (projectPath && projectName) {
|
|
21
|
+
return {
|
|
22
|
+
device_name: process.env.HUB_DEVICE_NAME || 'VoidConnect Hub',
|
|
23
|
+
password_hash: process.env.HUB_PASSWORD || null,
|
|
24
|
+
projects: [{ name: projectName, path: projectPath }]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Normal mode: load from config file
|
|
29
|
+
const configPath = join(homedir(), '.voidconnect', 'hub_config.json');
|
|
30
|
+
const file = Bun.file(configPath);
|
|
31
|
+
|
|
32
|
+
if (await file.exists()) {
|
|
33
|
+
const content = await file.json();
|
|
34
|
+
return content as HubConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
device_name: 'VoidConnect Hub',
|
|
39
|
+
password_hash: null,
|
|
40
|
+
projects: []
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/hub/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { loadConfig } from './config';
|
|
3
|
+
import { authMiddleware } from './auth';
|
|
4
|
+
import { processManager } from './process_manager';
|
|
5
|
+
import qrcode from 'qrcode-terminal';
|
|
6
|
+
import { networkInterfaces } from 'os';
|
|
7
|
+
import { logTraffic, formatRequest, formatResponse } from './logger';
|
|
8
|
+
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
|
|
11
|
+
// Logging middleware (only when --log flag is present)
|
|
12
|
+
app.use('*', async (c, next) => {
|
|
13
|
+
if (process.argv.includes('--log')) {
|
|
14
|
+
try {
|
|
15
|
+
await logTraffic('INCOMING_REQUEST', await formatRequest(c.req.raw));
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error('Error logging request:', e);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await next();
|
|
22
|
+
|
|
23
|
+
if (process.argv.includes('--log')) {
|
|
24
|
+
try {
|
|
25
|
+
await logTraffic('OUTGOING_RESPONSE', await formatResponse(c.res));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('Error logging response:', e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Auth middleware for protected routes
|
|
33
|
+
app.use('/api/*', authMiddleware);
|
|
34
|
+
app.use('/project/*', authMiddleware);
|
|
35
|
+
|
|
36
|
+
// Health check
|
|
37
|
+
app.get('/', (c) => c.text('VoidConnect Hub Running'));
|
|
38
|
+
|
|
39
|
+
// Status endpoint
|
|
40
|
+
app.get('/api/status', (c) => {
|
|
41
|
+
return c.json({ status: 'running', device: process.env.HUB_DEVICE_NAME || 'Unknown' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// List projects
|
|
45
|
+
app.get('/api/projects', async (c) => {
|
|
46
|
+
const config = await loadConfig();
|
|
47
|
+
return c.json(config.projects);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Project proxy
|
|
51
|
+
app.all('/project/:name/*', async (c) => {
|
|
52
|
+
const name = c.req.param('name');
|
|
53
|
+
const config = await loadConfig();
|
|
54
|
+
const project = config.projects.find(p => p.name === name);
|
|
55
|
+
|
|
56
|
+
if (!project) {
|
|
57
|
+
return c.text('Project not found', 404);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const port = await processManager.startProject(project.name, project.path);
|
|
62
|
+
|
|
63
|
+
const path = c.req.path.replace(`/project/${name}`, '') || '/';
|
|
64
|
+
|
|
65
|
+
const targetUrlObj = new URL(`http://127.0.0.1:${port}${path}`);
|
|
66
|
+
|
|
67
|
+
const originalUrl = new URL(c.req.url);
|
|
68
|
+
originalUrl.searchParams.forEach((value, key) => {
|
|
69
|
+
targetUrlObj.searchParams.set(key, value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
targetUrlObj.searchParams.set('directory', project.path);
|
|
73
|
+
|
|
74
|
+
const targetUrl = targetUrlObj.toString();
|
|
75
|
+
|
|
76
|
+
const newReq = new Request(targetUrl, {
|
|
77
|
+
method: c.req.method,
|
|
78
|
+
headers: c.req.header(),
|
|
79
|
+
body: c.req.raw.body,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (process.argv.includes('--log')) {
|
|
83
|
+
await logTraffic('PROXY_REQUEST_OUTGOING', await formatRequest(newReq));
|
|
84
|
+
}
|
|
85
|
+
const response = await fetch(newReq);
|
|
86
|
+
if (process.argv.includes('--log')) {
|
|
87
|
+
await logTraffic('PROXY_RESPONSE_INCOMING', await formatResponse(response));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return response;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`[Proxy] Error:`, e);
|
|
93
|
+
return c.text(`Global Error: ${e}`, 500);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const port = parseInt(process.env.PORT || '3838');
|
|
98
|
+
|
|
99
|
+
// Start with Cloudflare tunnel or local mode
|
|
100
|
+
if (process.argv.includes('--tunnel') || process.argv.includes('--cf')) {
|
|
101
|
+
import('./tunnel').then(({ startTunnel }) => {
|
|
102
|
+
startTunnel(port);
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
(async () => {
|
|
106
|
+
const nets = networkInterfaces();
|
|
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
|
+
|
|
122
|
+
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
|
+
}
|
|
132
|
+
|
|
133
|
+
const server = Bun.serve({
|
|
134
|
+
port,
|
|
135
|
+
fetch: app.fetch,
|
|
136
|
+
idleTimeout: 255,
|
|
137
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { appendFile } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const LOG_FILE = join(process.env.HUB_START_DIR || process.cwd(), 'void_traffic.log');
|
|
6
|
+
|
|
7
|
+
export async function logTraffic(type: string, data: any) {
|
|
8
|
+
const timestamp = new Date().toISOString();
|
|
9
|
+
const logEntry = `[${timestamp}] [${type}]\n${JSON.stringify(data, null, 2)}\n\n--------------------------------------------------\n\n`;
|
|
10
|
+
try {
|
|
11
|
+
await appendFile(LOG_FILE, logEntry);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error('Failed to write to log file:', err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function formatRequest(req: Request) {
|
|
18
|
+
let body = '(empty)';
|
|
19
|
+
try {
|
|
20
|
+
if (req.body) {
|
|
21
|
+
// Clone to not consume the stream needed for processing
|
|
22
|
+
body = await req.clone().text();
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
body = `(stream/error reading body: ${e})`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
url: req.url,
|
|
30
|
+
method: req.method,
|
|
31
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
32
|
+
body
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function formatResponse(res: Response) {
|
|
37
|
+
let body = '(empty)';
|
|
38
|
+
try {
|
|
39
|
+
// Clone response to read body without consuming the original stream
|
|
40
|
+
if (res.body) {
|
|
41
|
+
body = await res.clone().text();
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
body = `(stream/error reading body: ${e})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
status: res.status,
|
|
49
|
+
statusText: res.statusText,
|
|
50
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
51
|
+
body
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawn } from 'bun';
|
|
2
|
+
|
|
3
|
+
export class ProcessManager {
|
|
4
|
+
private processes: Map<string, any> = new Map();
|
|
5
|
+
private ports: Map<string, number> = new Map();
|
|
6
|
+
private startPort = 4100;
|
|
7
|
+
|
|
8
|
+
constructor() { }
|
|
9
|
+
|
|
10
|
+
async startProject(name: string, path: string): Promise<number> {
|
|
11
|
+
if (this.ports.has(name)) {
|
|
12
|
+
const existingPort = this.ports.get(name)!;
|
|
13
|
+
return existingPort;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const port = this.startPort + this.processes.size;
|
|
17
|
+
|
|
18
|
+
const p = spawn(["bun", "x", "opencode", "serve", "--port", port.toString(), "--hostname", "0.0.0.0"], {
|
|
19
|
+
cwd: path,
|
|
20
|
+
env: { ...process.env, OPENCODE_CLIENT: 'mobile' },
|
|
21
|
+
stdout: 'ignore',
|
|
22
|
+
stderr: 'ignore',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.processes.set(name, p);
|
|
26
|
+
this.ports.set(name, port);
|
|
27
|
+
|
|
28
|
+
// Wait for server to be ready
|
|
29
|
+
let attempts = 0;
|
|
30
|
+
const maxAttempts = 50;
|
|
31
|
+
|
|
32
|
+
while (attempts < maxAttempts) {
|
|
33
|
+
try {
|
|
34
|
+
const healthRes = await fetch(`http://127.0.0.1:${port}/global/health`);
|
|
35
|
+
if (healthRes.ok) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Server not ready yet
|
|
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,46 @@
|
|
|
1
|
+
import { spawn } from 'bun';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import { loadConfig } from './config';
|
|
4
|
+
|
|
5
|
+
export async function startTunnel(port: number): Promise<string> {
|
|
6
|
+
console.log(`Starting Cloudflare tunnel...`);
|
|
7
|
+
|
|
8
|
+
const proc = spawn(["cloudflared", "tunnel", "--url", `http://localhost:${port}`], {
|
|
9
|
+
stdout: 'pipe',
|
|
10
|
+
stderr: 'pipe',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let found = false;
|
|
15
|
+
|
|
16
|
+
const reader = proc.stderr.getReader();
|
|
17
|
+
const decoder = new TextDecoder();
|
|
18
|
+
|
|
19
|
+
async function read() {
|
|
20
|
+
while (true) {
|
|
21
|
+
const { done, value } = await reader.read();
|
|
22
|
+
if (done) break;
|
|
23
|
+
|
|
24
|
+
const text = decoder.decode(value);
|
|
25
|
+
|
|
26
|
+
const match = text.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
27
|
+
if (match && !found) {
|
|
28
|
+
found = true;
|
|
29
|
+
const url = match[0];
|
|
30
|
+
const config = await loadConfig();
|
|
31
|
+
const name = config.device_name;
|
|
32
|
+
|
|
33
|
+
console.log('\n');
|
|
34
|
+
qrcode.generate(JSON.stringify({ name, url }), { small: true });
|
|
35
|
+
console.log(`\nHub Online: ${name}`);
|
|
36
|
+
console.log(`URL: ${url}\n`);
|
|
37
|
+
|
|
38
|
+
resolve(url);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!found) reject("Failed to get tunnel URL");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
read();
|
|
45
|
+
});
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
handleRun,
|
|
4
|
+
handleStart,
|
|
5
|
+
handleStop,
|
|
6
|
+
handleRestart,
|
|
7
|
+
handleAdd,
|
|
8
|
+
handleRemove,
|
|
9
|
+
handleStatus,
|
|
10
|
+
handleConfig,
|
|
11
|
+
getConfigInfo,
|
|
12
|
+
type StatusInfo
|
|
13
|
+
} from "./handlers";
|
|
14
|
+
import { checkForUpdates } from "./utils";
|
|
15
|
+
|
|
16
|
+
const VERSION = "1.0.0";
|
|
17
|
+
|
|
18
|
+
// ANSI color codes
|
|
19
|
+
const Colors = {
|
|
20
|
+
reset: "\x1b[0m",
|
|
21
|
+
bold: "\x1b[1m",
|
|
22
|
+
dim: "\x1b[2m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
green: "\x1b[32m",
|
|
25
|
+
yellow: "\x1b[33m",
|
|
26
|
+
red: "\x1b[31m",
|
|
27
|
+
white: "\x1b[37m"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Parse command line arguments
|
|
31
|
+
function parseArgs() {
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const command = args[0];
|
|
34
|
+
const flags: Record<string, string | boolean> = {};
|
|
35
|
+
const positional: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (!arg) continue;
|
|
40
|
+
if (arg.startsWith('--')) {
|
|
41
|
+
const key = arg.slice(2);
|
|
42
|
+
const nextArg = args[i + 1];
|
|
43
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
44
|
+
flags[key] = nextArg;
|
|
45
|
+
i++;
|
|
46
|
+
} else {
|
|
47
|
+
flags[key] = true;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
positional.push(arg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { command, flags, positional };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Print helpers
|
|
58
|
+
function printBox(lines: string[]) {
|
|
59
|
+
const width = 63;
|
|
60
|
+
console.log(`${Colors.cyan}┌${'─'.repeat(width)}┐${Colors.reset}`);
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
// Strip ANSI codes to calculate visible length
|
|
63
|
+
const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
64
|
+
const padding = width - visibleLen - 2;
|
|
65
|
+
console.log(`${Colors.cyan}│${Colors.reset} ${line}${' '.repeat(Math.max(0, padding))}${Colors.cyan}│${Colors.reset}`);
|
|
66
|
+
}
|
|
67
|
+
console.log(`${Colors.cyan}└${'─'.repeat(width)}┘${Colors.reset}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printDivider() {
|
|
71
|
+
console.log(`${Colors.cyan}├${'─'.repeat(63)}┤${Colors.reset}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function printSuccess(msg: string) {
|
|
75
|
+
console.log(`\n${Colors.green}✓${Colors.reset} ${msg}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printError(msg: string) {
|
|
79
|
+
console.log(`\n${Colors.red}✗${Colors.reset} ${msg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printWarning(msg: string) {
|
|
83
|
+
console.log(`\n${Colors.yellow}!${Colors.reset} ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Display functions
|
|
87
|
+
function showHelp() {
|
|
88
|
+
console.log(`\n${Colors.cyan}${Colors.bold}VoidConnect CLI v${VERSION}${Colors.reset}`);
|
|
89
|
+
console.log(`${Colors.dim}CLI tool for managing VoidConnect mobile app servers${Colors.reset}`);
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(`${Colors.white}${Colors.bold}Usage:${Colors.reset}`);
|
|
92
|
+
console.log(`${Colors.dim} bunx voidct [command] [options]${Colors.reset}`);
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(`${Colors.white}${Colors.bold}Commands:${Colors.reset}`);
|
|
95
|
+
console.log(`${Colors.dim} run Run a local project (current directory or --dir)${Colors.reset}`);
|
|
96
|
+
console.log(`${Colors.dim} start Start the server using saved configuration${Colors.reset}`);
|
|
97
|
+
console.log(`${Colors.dim} stop Stop the background server${Colors.reset}`);
|
|
98
|
+
console.log(`${Colors.dim} status Show status dashboard${Colors.reset}`);
|
|
99
|
+
console.log(`${Colors.dim} add Add a project to saved configuration${Colors.reset}`);
|
|
100
|
+
console.log(`${Colors.dim} remove Remove a project from configuration${Colors.reset}`);
|
|
101
|
+
console.log(`${Colors.dim} restart Restart the background server${Colors.reset}`);
|
|
102
|
+
console.log(`${Colors.dim} config Configure device settings${Colors.reset}`);
|
|
103
|
+
console.log(`${Colors.dim} update Check for updates${Colors.reset}`);
|
|
104
|
+
console.log();
|
|
105
|
+
console.log(`${Colors.white}${Colors.bold}Quick Start:${Colors.reset}`);
|
|
106
|
+
console.log(`${Colors.dim} • Run 'voidct run' to serve the current directory${Colors.reset}`);
|
|
107
|
+
console.log(`${Colors.dim} • Use 'voidct add <path>' to save projects${Colors.reset}`);
|
|
108
|
+
console.log(`${Colors.dim} • Use 'voidct start' to serve saved projects${Colors.reset}`);
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(`${Colors.dim}GitHub: https://github.com/xptea/voidconnect-cli${Colors.reset}`);
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function showStatus(status: StatusInfo) {
|
|
115
|
+
console.log();
|
|
116
|
+
printBox([
|
|
117
|
+
`${Colors.bold}${Colors.white}VoidConnect Status Dashboard${Colors.reset}`,
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
console.log(`${Colors.cyan}├${'─'.repeat(63)}┤${Colors.reset}`);
|
|
121
|
+
console.log(`${Colors.cyan}│${Colors.reset} Device: ${Colors.white}${status.deviceName}${Colors.reset}`);
|
|
122
|
+
|
|
123
|
+
const statusText = status.running
|
|
124
|
+
? `${Colors.green}Running${Colors.reset} (PID: ${status.pid})`
|
|
125
|
+
: `${Colors.red}Offline${Colors.reset}`;
|
|
126
|
+
console.log(`${Colors.cyan}│${Colors.reset} Status: ${statusText}`);
|
|
127
|
+
console.log(`${Colors.cyan}│${Colors.reset} Port: ${Colors.white}${status.port ?? 3838}${Colors.reset}`);
|
|
128
|
+
console.log(`${Colors.cyan}└${'─'.repeat(63)}┘${Colors.reset}`);
|
|
129
|
+
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(`${Colors.white}${Colors.bold}Configured Projects:${Colors.reset}`);
|
|
132
|
+
if (status.projects.length === 0) {
|
|
133
|
+
console.log(`${Colors.dim} No projects configured.${Colors.reset}`);
|
|
134
|
+
} else {
|
|
135
|
+
for (const p of status.projects) {
|
|
136
|
+
console.log(` • ${Colors.cyan}${p.name}${Colors.reset}: ${p.path}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(`${Colors.white}${Colors.bold}Available Commands:${Colors.reset}`);
|
|
142
|
+
console.log(`${Colors.dim} run --dir, --cf, --port Run local project${Colors.reset}`);
|
|
143
|
+
console.log(`${Colors.dim} start --cf, --bg, --port Start server${Colors.reset}`);
|
|
144
|
+
console.log(`${Colors.dim} stop Stop server${Colors.reset}`);
|
|
145
|
+
console.log(`${Colors.dim} restart Restart server${Colors.reset}`);
|
|
146
|
+
console.log(`${Colors.dim} add <path> [--name] Add a project${Colors.reset}`);
|
|
147
|
+
console.log(`${Colors.dim} remove <name> Remove a project${Colors.reset}`);
|
|
148
|
+
console.log(`${Colors.dim} config Configure settings${Colors.reset}`);
|
|
149
|
+
console.log(`${Colors.dim} update Check for updates${Colors.reset}`);
|
|
150
|
+
console.log();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function showConfig(config: ReturnType<typeof getConfigInfo>) {
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(`${Colors.cyan}${Colors.bold}Current Configuration:${Colors.reset}`);
|
|
156
|
+
console.log(`${Colors.white} Device name: ${config.device_name}${Colors.reset}`);
|
|
157
|
+
console.log(`${Colors.white} Password: ${config.password_hash ? 'Set' : 'Not set'}${Colors.reset}`);
|
|
158
|
+
console.log(`${Colors.white} Port: ${config.port ?? 'Default (3838)'}${Colors.reset}`);
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(`${Colors.dim}To change settings, use:${Colors.reset}`);
|
|
161
|
+
console.log(`${Colors.dim} --device-name <name> Change device name${Colors.reset}`);
|
|
162
|
+
console.log(`${Colors.dim} --password <pwd> Set password${Colors.reset}`);
|
|
163
|
+
console.log(`${Colors.dim} --no-password Remove password${Colors.reset}`);
|
|
164
|
+
console.log(`${Colors.dim} --port <port> Set default port${Colors.reset}`);
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function showAddSuccess(projectName: string) {
|
|
169
|
+
printSuccess(`Project '${projectName}' has been saved!`);
|
|
170
|
+
console.log();
|
|
171
|
+
printBox([
|
|
172
|
+
`${Colors.yellow}Next Steps:${Colors.reset}`,
|
|
173
|
+
`• Run ${Colors.bold}bunx voidct start${Colors.reset} to start all projects`,
|
|
174
|
+
`• Run ${Colors.bold}bunx voidct status${Colors.reset} to see projects`,
|
|
175
|
+
]);
|
|
176
|
+
console.log();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Main function
|
|
180
|
+
async function main() {
|
|
181
|
+
const { command, flags, positional } = parseArgs();
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
switch (command) {
|
|
185
|
+
case 'run': {
|
|
186
|
+
const result = await handleRun({
|
|
187
|
+
dir: flags.dir as string | undefined,
|
|
188
|
+
cf: !!flags.cf,
|
|
189
|
+
port: flags.port ? parseInt(flags.port as string) : undefined
|
|
190
|
+
});
|
|
191
|
+
if (result.success) {
|
|
192
|
+
printSuccess(result.message);
|
|
193
|
+
} else {
|
|
194
|
+
printError(result.message);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'start': {
|
|
200
|
+
const result = await handleStart({
|
|
201
|
+
cf: !!flags.cf,
|
|
202
|
+
bg: !!flags.bg,
|
|
203
|
+
port: flags.port ? parseInt(flags.port as string) : undefined,
|
|
204
|
+
log: !!flags.log
|
|
205
|
+
});
|
|
206
|
+
if (result.success) {
|
|
207
|
+
printSuccess(result.message);
|
|
208
|
+
} else {
|
|
209
|
+
printError(result.message);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'stop': {
|
|
215
|
+
const result = await handleStop();
|
|
216
|
+
if (result.success) {
|
|
217
|
+
printSuccess(result.message);
|
|
218
|
+
} else {
|
|
219
|
+
printError(result.message);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'restart': {
|
|
225
|
+
const result = await handleRestart();
|
|
226
|
+
if (result.success) {
|
|
227
|
+
printSuccess(result.message);
|
|
228
|
+
} else {
|
|
229
|
+
printError(result.message);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'add': {
|
|
235
|
+
const path = positional[0];
|
|
236
|
+
if (!path) {
|
|
237
|
+
printError("Please provide a path: voidct add <path>");
|
|
238
|
+
} else {
|
|
239
|
+
const result = await handleAdd(path, flags.name as string | undefined);
|
|
240
|
+
if (result.success) {
|
|
241
|
+
const projectName = result.message.match(/Project '(.+)' has been saved/)?.[1] || path;
|
|
242
|
+
showAddSuccess(projectName);
|
|
243
|
+
} else {
|
|
244
|
+
printError(result.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'remove': {
|
|
251
|
+
const name = positional[0];
|
|
252
|
+
if (!name) {
|
|
253
|
+
printError("Please provide a project name: voidct remove <name>");
|
|
254
|
+
} else {
|
|
255
|
+
const result = await handleRemove(name);
|
|
256
|
+
if (result.success) {
|
|
257
|
+
printSuccess(result.message);
|
|
258
|
+
} else {
|
|
259
|
+
printError(result.message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'status': {
|
|
266
|
+
const status = await handleStatus();
|
|
267
|
+
showStatus(status);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case 'config': {
|
|
272
|
+
if (Object.keys(flags).length > 0) {
|
|
273
|
+
const result = await handleConfig({
|
|
274
|
+
deviceName: flags['device-name'] as string | undefined,
|
|
275
|
+
password: flags['no-password'] ? null : flags.password as string | undefined,
|
|
276
|
+
port: flags.port ? parseInt(flags.port as string) : undefined
|
|
277
|
+
});
|
|
278
|
+
if (result.success) {
|
|
279
|
+
printSuccess(result.message);
|
|
280
|
+
} else {
|
|
281
|
+
printError(result.message);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
const config = getConfigInfo();
|
|
285
|
+
showConfig(config);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
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
|
+
default:
|
|
304
|
+
showHelp();
|
|
305
|
+
}
|
|
306
|
+
} catch (err: any) {
|
|
307
|
+
printError(`Error: ${err.message}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
main();
|