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 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.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/voidconnect-cli"
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
- // 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
- }
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
- // Logging middleware (only when --log flag is present)
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 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
-
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 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',
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); // Signal 0 checks if process exists
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
- }