qwenproxy-cli 1.0.28 → 1.0.30

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.
@@ -0,0 +1,237 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
+ import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
+
6
+ function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
7
+ const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
8
+ const match = content.match(regex);
9
+ if (!match || match.index === undefined) return null;
10
+
11
+ const startIndex = match.index;
12
+ const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
13
+ if (braceIndex === -1) return null;
14
+
15
+ let depth = 0;
16
+ let inString = false;
17
+ let inLineComment = false;
18
+ let inBlockComment = false;
19
+ let escape = false;
20
+
21
+ for (let i = braceIndex; i < content.length; i++) {
22
+ const ch = content[i];
23
+ const nextCh = content[i + 1] || "";
24
+
25
+ if (inLineComment) {
26
+ if (ch === "\n") inLineComment = false;
27
+ continue;
28
+ }
29
+ if (inBlockComment) {
30
+ if (ch === "*" && nextCh === "/") {
31
+ inBlockComment = false;
32
+ i++;
33
+ }
34
+ continue;
35
+ }
36
+ if (inString) {
37
+ if (escape) {
38
+ escape = false;
39
+ } else if (ch === "\\") {
40
+ escape = true;
41
+ } else if (ch === '"') {
42
+ inString = false;
43
+ }
44
+ continue;
45
+ }
46
+
47
+ if (ch === "/" && nextCh === "/") {
48
+ inLineComment = true;
49
+ i++;
50
+ continue;
51
+ }
52
+ if (ch === "/" && nextCh === "*") {
53
+ inBlockComment = true;
54
+ i++;
55
+ continue;
56
+ }
57
+
58
+ if (ch === '"') {
59
+ inString = true;
60
+ continue;
61
+ }
62
+
63
+ if (ch === "{") {
64
+ depth++;
65
+ } else if (ch === "}") {
66
+ depth--;
67
+ if (depth === 0) {
68
+ let endIndex = i + 1;
69
+ let hasTrailingComma = false;
70
+ while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
71
+ if (content[endIndex] === ",") {
72
+ hasTrailingComma = true;
73
+ endIndex++;
74
+ break;
75
+ }
76
+ if (content[endIndex] === "\n") {
77
+ break;
78
+ }
79
+ endIndex++;
80
+ }
81
+ return { start: startIndex, end: endIndex, hasTrailingComma };
82
+ }
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function buildOpenClawProviderObject(
90
+ baseUrl: string,
91
+ apiKey: string,
92
+ model: string = "qwen3.8-max",
93
+ ): Record<string, any> {
94
+ const models = [
95
+ {
96
+ id: model,
97
+ name: model === "qwen3.8-max" ? "Qwen 3.8 Max" : model,
98
+ reasoning: true,
99
+ supportsReasoningEffort: true,
100
+ supportedReasoningEfforts: ["low", "medium", "high"],
101
+ contextWindow: 1000000,
102
+ maxTokens: 65536,
103
+ },
104
+ ];
105
+
106
+ if (model !== "qwen3.7-plus") {
107
+ models.push({
108
+ id: "qwen3.7-plus",
109
+ name: "Qwen 3.7 Plus",
110
+ reasoning: true,
111
+ supportsReasoningEffort: true,
112
+ supportedReasoningEfforts: ["low", "medium", "high"],
113
+ contextWindow: 1000000,
114
+ maxTokens: 65536,
115
+ });
116
+ }
117
+
118
+ return {
119
+ baseUrl,
120
+ apiKey,
121
+ api: "openai-completions",
122
+ models,
123
+ };
124
+ }
125
+
126
+ export function syncOpenClaw(options: SyncOptions): ClientSyncResult {
127
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
128
+ try {
129
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
130
+
131
+ let backupPath: string | undefined;
132
+ let content = "";
133
+
134
+ if (fs.existsSync(filePath)) {
135
+ backupPath = createTimestampBackup(filePath);
136
+ content = fs.readFileSync(filePath, "utf-8");
137
+ }
138
+
139
+ const providerObj = buildOpenClawProviderObject(baseUrl, apiKey, model);
140
+ const providerJson = JSON.stringify(providerObj, null, 6)
141
+ .split("\n")
142
+ .map((line, idx) => (idx === 0 ? line : ` ${line}`))
143
+ .join("\n");
144
+
145
+ const qwenEntry = ` "qwenproxy": ${providerJson}`;
146
+
147
+ if (!content.trim()) {
148
+ const initial = {
149
+ models: {
150
+ mode: "merge",
151
+ providers: {
152
+ qwenproxy: providerObj,
153
+ },
154
+ },
155
+ agents: {
156
+ defaults: {
157
+ model: {
158
+ primary: `qwenproxy/${model}`,
159
+ },
160
+ thinking: {
161
+ effort: reasoningEffort,
162
+ },
163
+ },
164
+ },
165
+ };
166
+ fs.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
167
+ } else {
168
+ // Check if "qwenproxy" already exists under providers
169
+ const existingSpan = findKeyObjectSpan(content, "qwenproxy");
170
+ if (existingSpan) {
171
+ const comma = existingSpan.hasTrailingComma ? "," : "";
172
+ content =
173
+ content.slice(0, existingSpan.start) +
174
+ `"qwenproxy": ${providerJson}${comma}` +
175
+ content.slice(existingSpan.end);
176
+ } else {
177
+ const providersMatch = content.match(/"providers"\s*:\s*\{/);
178
+ if (providersMatch && providersMatch.index !== undefined) {
179
+ const insertIdx = providersMatch.index + providersMatch[0].length;
180
+ content =
181
+ content.slice(0, insertIdx) +
182
+ "\n" +
183
+ qwenEntry +
184
+ "," +
185
+ content.slice(insertIdx);
186
+ } else {
187
+ // If "providers" does not exist, check if "models" exists
188
+ const modelsMatch = content.match(/"models"\s*:\s*\{/);
189
+ if (modelsMatch && modelsMatch.index !== undefined) {
190
+ const insertIdx = modelsMatch.index + modelsMatch[0].length;
191
+ const providersBlock = `\n "providers": {\n${qwenEntry}\n },`;
192
+ content = content.slice(0, insertIdx) + providersBlock + content.slice(insertIdx);
193
+ } else {
194
+ // Append models before the last closing brace
195
+ const lastBraceIdx = content.lastIndexOf("}");
196
+ if (lastBraceIdx !== -1) {
197
+ const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
198
+ const modelsBlock = `${comma}\n "models": {\n "mode": "merge",\n "providers": {\n${qwenEntry}\n }\n }\n`;
199
+ content = content.slice(0, lastBraceIdx).trimEnd() + modelsBlock + "}\n";
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ fs.writeFileSync(filePath, content, "utf-8");
206
+ }
207
+
208
+ return {
209
+ client: "openclaw",
210
+ filePath,
211
+ backupPath,
212
+ success: true,
213
+ action: backupPath ? "updated" : "created",
214
+ message: `Configured OpenClaw with provider qwenproxy (${baseUrl}) and reasoning effort ${reasoningEffort}`,
215
+ };
216
+ } catch (err: any) {
217
+ return {
218
+ client: "openclaw",
219
+ filePath,
220
+ success: false,
221
+ action: "failed",
222
+ error: err?.message || String(err),
223
+ };
224
+ }
225
+ }
226
+
227
+ export function restoreOpenClaw(filePath: string, backupPath?: string): ClientSyncResult {
228
+ const restored = restoreFromBackup(filePath, backupPath);
229
+ return {
230
+ client: "openclaw",
231
+ filePath,
232
+ backupPath,
233
+ success: restored,
234
+ action: restored ? "restored" : "failed",
235
+ message: restored ? "Restored OpenClaw config from backup" : "Backup file not found",
236
+ };
237
+ }
package/src/sync/types.ts CHANGED
@@ -1,7 +1,20 @@
1
+ export type SyncClientName =
2
+ | "claude-code"
3
+ | "codex"
4
+ | "opencode"
5
+ | "omp"
6
+ | "hermes"
7
+ | "openclaw"
8
+ | "kilo"
9
+ | "cline"
10
+ | "zed"
11
+ | "aider";
12
+
1
13
  export interface ClientSyncResult {
2
- client: "claude-code" | "codex" | "opencode" | "omp";
14
+ client: SyncClientName;
3
15
  filePath: string;
4
16
  backupPath?: string;
17
+ extraBackupPath?: string;
5
18
  success: boolean;
6
19
  action: "updated" | "created" | "skipped" | "restored" | "failed";
7
20
  message?: string;
@@ -14,6 +27,8 @@ export interface SyncOptions {
14
27
  baseUrl: string;
15
28
  model?: string;
16
29
  setActive?: boolean;
30
+ reasoningEffort?: "low" | "medium" | "high" | "none";
31
+ modelSettingsPath?: string;
17
32
  }
18
33
 
19
34
  export interface SyncAllOptions {
@@ -22,15 +37,20 @@ export interface SyncAllOptions {
22
37
  host?: string;
23
38
  setActive?: boolean;
24
39
  stateFilePath?: string;
25
- targets?: ("claude-code" | "codex" | "opencode" | "omp")[];
40
+ targets?: SyncClientName[];
26
41
  customPaths?: {
27
42
  claudeCode?: string;
28
43
  codex?: string;
29
44
  openCode?: string;
30
45
  omp?: string;
46
+ hermes?: string;
47
+ openClaw?: string;
48
+ kilo?: string;
49
+ cline?: string;
50
+ zed?: string;
51
+ aider?: string;
31
52
  };
32
53
  }
33
-
34
54
  export interface SyncRecord {
35
55
  filePath: string;
36
56
  backupPath: string;
@@ -49,5 +69,11 @@ export interface SyncStateFile {
49
69
  codex?: SyncRecord;
50
70
  openCode?: SyncRecord;
51
71
  omp?: SyncRecord;
72
+ hermes?: SyncRecord;
73
+ openClaw?: SyncRecord;
74
+ kilo?: SyncRecord;
75
+ cline?: SyncRecord;
76
+ zed?: SyncRecord;
77
+ aider?: SyncRecord;
52
78
  };
53
79
  }
@@ -0,0 +1,210 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
+ import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
+
6
+ function parseJsonWithComments(raw: string): Record<string, any> {
7
+ let result = "";
8
+ let inString = false;
9
+ let inLineComment = false;
10
+ let inBlockComment = false;
11
+ let escape = false;
12
+
13
+ for (let i = 0; i < raw.length; i++) {
14
+ const ch = raw[i];
15
+ const nextCh = raw[i + 1] || "";
16
+
17
+ if (inLineComment) {
18
+ if (ch === "\n") {
19
+ inLineComment = false;
20
+ result += ch;
21
+ }
22
+ continue;
23
+ }
24
+ if (inBlockComment) {
25
+ if (ch === "*" && nextCh === "/") {
26
+ inBlockComment = false;
27
+ i++;
28
+ }
29
+ continue;
30
+ }
31
+ if (inString) {
32
+ result += ch;
33
+ if (escape) {
34
+ escape = false;
35
+ } else if (ch === "\\") {
36
+ escape = true;
37
+ } else if (ch === '"') {
38
+ inString = false;
39
+ }
40
+ continue;
41
+ }
42
+
43
+ if (ch === "/" && nextCh === "/") {
44
+ inLineComment = true;
45
+ i++;
46
+ continue;
47
+ }
48
+ if (ch === "/" && nextCh === "*") {
49
+ inBlockComment = true;
50
+ i++;
51
+ continue;
52
+ }
53
+
54
+ if (ch === '"') {
55
+ inString = true;
56
+ result += ch;
57
+ continue;
58
+ }
59
+
60
+ result += ch;
61
+ }
62
+
63
+ const cleaned = result.replace(/,\s*([\}\]])/g, "$1");
64
+ return JSON.parse(cleaned);
65
+ }
66
+
67
+ function buildZedAvailableModels(primaryModel: string = "qwen3.8-max"): any[] {
68
+ const models = [
69
+ {
70
+ name: primaryModel,
71
+ max_tokens: 1000000,
72
+ max_output_tokens: 131072,
73
+ max_completion_tokens: 131072,
74
+ capabilities: {
75
+ tools: true,
76
+ images: true,
77
+ parallel_tool_calls: true,
78
+ prompt_cache_key: true,
79
+ chat_completions: true,
80
+ interleaved_reasoning: true,
81
+ },
82
+ },
83
+ {
84
+ name: `${primaryModel}-thinking`,
85
+ max_tokens: 1000000,
86
+ max_output_tokens: 131072,
87
+ max_completion_tokens: 131072,
88
+ capabilities: {
89
+ tools: true,
90
+ images: true,
91
+ parallel_tool_calls: true,
92
+ prompt_cache_key: true,
93
+ chat_completions: true,
94
+ interleaved_reasoning: true,
95
+ },
96
+ },
97
+ {
98
+ name: `${primaryModel}-fast`,
99
+ max_tokens: 1000000,
100
+ max_output_tokens: 131072,
101
+ max_completion_tokens: 131072,
102
+ capabilities: {
103
+ tools: true,
104
+ images: true,
105
+ parallel_tool_calls: true,
106
+ prompt_cache_key: true,
107
+ chat_completions: true,
108
+ interleaved_reasoning: true,
109
+ },
110
+ },
111
+ ];
112
+
113
+ if (primaryModel !== "qwen3.7-plus") {
114
+ models.push({
115
+ name: "qwen3.7-plus",
116
+ max_tokens: 1000000,
117
+ max_output_tokens: 65536,
118
+ max_completion_tokens: 65536,
119
+ capabilities: {
120
+ tools: true,
121
+ images: true,
122
+ parallel_tool_calls: true,
123
+ prompt_cache_key: true,
124
+ chat_completions: true,
125
+ interleaved_reasoning: true,
126
+ },
127
+ });
128
+ }
129
+
130
+ return models;
131
+ }
132
+
133
+ export function syncZed(options: SyncOptions): ClientSyncResult {
134
+ const { filePath, baseUrl, model = "qwen3.8-max", setActive = true } = options;
135
+ try {
136
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
137
+
138
+ let backupPath: string | undefined;
139
+ let existingSettings: Record<string, any> = {};
140
+
141
+ if (fs.existsSync(filePath)) {
142
+ backupPath = createTimestampBackup(filePath);
143
+ try {
144
+ const content = fs.readFileSync(filePath, "utf-8");
145
+ existingSettings = parseJsonWithComments(content);
146
+ } catch {
147
+ existingSettings = {};
148
+ }
149
+ }
150
+
151
+ const languageModels = existingSettings.language_models || {};
152
+ const openaiCompatible = languageModels.openai_compatible || {};
153
+
154
+ openaiCompatible.QwenProxy = {
155
+ api_url: baseUrl,
156
+ available_models: buildZedAvailableModels(model),
157
+ };
158
+
159
+ const updatedSettings: Record<string, any> = {
160
+ ...existingSettings,
161
+ language_models: {
162
+ ...languageModels,
163
+ openai_compatible: openaiCompatible,
164
+ },
165
+ };
166
+
167
+ if (setActive) {
168
+ const existingAgent = existingSettings.agent || {};
169
+ updatedSettings.agent = {
170
+ ...existingAgent,
171
+ default_model: {
172
+ provider: "QwenProxy",
173
+ model,
174
+ enable_thinking: true,
175
+ },
176
+ };
177
+ }
178
+
179
+ fs.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
180
+
181
+ return {
182
+ client: "zed",
183
+ filePath,
184
+ backupPath,
185
+ success: true,
186
+ action: backupPath ? "updated" : "created",
187
+ message: `Configured Zed Editor with QwenProxy (${baseUrl}) and model ${model}`,
188
+ };
189
+ } catch (err: any) {
190
+ return {
191
+ client: "zed",
192
+ filePath,
193
+ success: false,
194
+ action: "failed",
195
+ error: err?.message || String(err),
196
+ };
197
+ }
198
+ }
199
+
200
+ export function restoreZed(filePath: string, backupPath?: string): ClientSyncResult {
201
+ const restored = restoreFromBackup(filePath, backupPath);
202
+ return {
203
+ client: "zed",
204
+ filePath,
205
+ backupPath,
206
+ success: restored,
207
+ action: restored ? "restored" : "failed",
208
+ message: restored ? "Restored Zed settings from backup" : "Backup file not found",
209
+ };
210
+ }
@@ -6,7 +6,7 @@ import {
6
6
  getDefaultPaths,
7
7
  inspectClientSyncStatus,
8
8
  } from "./sync/index.ts";
9
- import fs from "node:fs";
9
+ import type { SyncClientName } from "./sync/types.ts";
10
10
 
11
11
  function parseArgs() {
12
12
  const args = process.argv.slice(2);
@@ -18,7 +18,7 @@ function parseArgs() {
18
18
  port?: number;
19
19
  host?: string;
20
20
  setActive: boolean;
21
- targets: ("claude-code" | "codex" | "opencode" | "omp")[];
21
+ targets: SyncClientName[];
22
22
  } = {
23
23
  restore: false,
24
24
  list: false,
@@ -61,20 +61,27 @@ Uso:
61
61
  npm run sync [clientes...] [opções]
62
62
 
63
63
  Exemplos:
64
- npm run sync # Sincroniza todos os clientes detectados
65
- npm run sync claude # Sincroniza apenas o Claude Code
66
- npm run sync codex # Sincroniza apenas o Codex CLI
64
+ npm run sync # Sincroniza todos os 10 clientes detectados
65
+ npm run sync hermes # Sincroniza apenas o Hermes Agent
67
66
  npm run sync opencode # Sincroniza apenas o OpenCode
67
+ npm run sync claude # Sincroniza apenas o Claude Code
68
+ npm run sync openclaw # Sincroniza apenas o OpenClaw
69
+ npm run sync kilo # Sincroniza apenas o Kilo Code
70
+ npm run sync cline # Sincroniza Cline & Zoo Code
68
71
  npm run sync omp # Sincroniza apenas o OMP (Oh My Pi)
69
- npm run sync claude codex # Sincroniza Claude Code e Codex
70
- npm run sync -- --list # Lista os arquivos dos clientes detectados
72
+ npm run sync codex # Sincroniza apenas o Codex CLI
73
+ npm run sync zed # Sincroniza apenas o Zed Editor
74
+ npm run sync aider # Sincroniza apenas o Aider
75
+ npm run sync claude codex # Sincroniza múltiplos clientes específicos
76
+ npm run sync -- --list # Lista status de detecção de todos os 10 clientes
71
77
  npm run sync -- --restore # Restaura as configurações originais (rollback)
78
+
72
79
  Opções:
73
- --client <nome> Nome do cliente (claude, codex, opencode, omp)
80
+ --client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
74
81
  --api-key <chave> Sobrescrever chave de API (padrão: lê do .env ou usa sk-qwenproxy-local)
75
82
  --port <porta> Sobrescrever porta do servidor (padrão: lê do .env ou usa 7936)
76
83
  --host <host> Sobrescrever host do servidor (padrão: 127.0.0.1)
77
- --no-active Não definir o modelo ativo no Codex (apenas adiciona o provider)
84
+ --no-active Não definir o modelo ativo como padrão (apenas adiciona o provider)
78
85
  --restore Desfaz alterações restaurando backups
79
86
  --list Mostra status de detecção dos arquivos de configuração
80
87
  `);
@@ -84,7 +91,7 @@ async function main() {
84
91
  const options = parseArgs();
85
92
 
86
93
  console.log("==================================================");
87
- console.log(" 🚀 QwenProxy - Client Configuration Sync");
94
+ console.log(" 🚀 QwenProxy - Top 10 Client Configuration Sync");
88
95
  console.log("==================================================");
89
96
 
90
97
  if (options.help) {
@@ -93,13 +100,19 @@ async function main() {
93
100
  }
94
101
 
95
102
  if (options.list) {
96
- console.log("\n📁 Status de detecção dos clientes no seu computador:\n");
103
+ console.log("\n📁 Status de detecção dos 10 clientes no seu computador:\n");
97
104
  const defaultPaths = getDefaultPaths();
98
- const clients = [
99
- { id: "claude-code" as const, name: "Claude Code", path: defaultPaths.claudeCode },
100
- { id: "codex" as const, name: "Codex CLI", path: defaultPaths.codex },
101
- { id: "opencode" as const, name: "OpenCode", path: defaultPaths.openCode },
102
- { id: "omp" as const, name: "OMP (Oh My Pi)", path: defaultPaths.omp },
105
+ const clients: { id: SyncClientName; name: string; path: string }[] = [
106
+ { id: "hermes", name: "1. Hermes Agent", path: defaultPaths.hermes },
107
+ { id: "opencode", name: "2. OpenCode", path: defaultPaths.openCode },
108
+ { id: "claude-code", name: "3. Claude Code", path: defaultPaths.claudeCode },
109
+ { id: "openclaw", name: "4. OpenClaw", path: defaultPaths.openClaw },
110
+ { id: "kilo", name: "5. Kilo Code", path: defaultPaths.kilo },
111
+ { id: "cline", name: "6. Cline & Zoo", path: defaultPaths.cline },
112
+ { id: "omp", name: "7. OMP (Oh My Pi)", path: defaultPaths.omp },
113
+ { id: "codex", name: "8. Codex CLI", path: defaultPaths.codex },
114
+ { id: "zed", name: "9. Zed Editor", path: defaultPaths.zed },
115
+ { id: "aider", name: "10. Aider", path: defaultPaths.aider },
103
116
  ];
104
117
 
105
118
  for (const c of clients) {
@@ -116,12 +129,13 @@ async function main() {
116
129
  icon = "⚪";
117
130
  badge = "[Não instalado]";
118
131
  }
119
- console.log(` ${icon} ${c.name.padEnd(16)} ${badge}`);
132
+ console.log(` ${icon} ${c.name.padEnd(20)} ${badge}`);
120
133
  console.log(` ${c.path}`);
121
134
  }
122
135
  console.log("\nPara sincronizar um ou todos, execute:");
123
136
  console.log(" npm run sync");
124
- console.log(" npm run sync claude\n");
137
+ console.log(" npm run sync hermes");
138
+ console.log(" npm run sync claude cline zed\n");
125
139
  return;
126
140
  }
127
141
 
package/src/update-cli.ts CHANGED
@@ -68,6 +68,46 @@ export function isNewerVersion(current: string, latest: string): boolean {
68
68
  return false;
69
69
  }
70
70
 
71
+ export async function fetchLatestNpmVersion(packageName: string): Promise<string> {
72
+ // 1. Direct fast HTTP query to registry.npmjs.org (bypasses child process & Windows npm.cmd cold start)
73
+ try {
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), 6000);
76
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
77
+ headers: { Accept: "application/json" },
78
+ signal: controller.signal,
79
+ });
80
+ clearTimeout(timer);
81
+ if (res.ok) {
82
+ const data = (await res.json()) as { version?: string };
83
+ if (data.version && typeof data.version === "string") {
84
+ return data.version.trim();
85
+ }
86
+ }
87
+ } catch {}
88
+
89
+ // 2. Fallback: npm view via CLI with a generous 30s timeout and clean semver extraction
90
+ try {
91
+ const fullCmd = `npm view ${packageName} version`;
92
+ const res = spawnSync(fullCmd, {
93
+ encoding: "utf-8",
94
+ shell: true,
95
+ timeout: 30000,
96
+ });
97
+ if (res.stdout) {
98
+ const lines = res.stdout.trim().split(/\r?\n/);
99
+ for (const line of lines.reverse()) {
100
+ const cleaned = line.trim().replace(/^v/i, "");
101
+ if (/^\d+\.\d+\.\d+/.test(cleaned)) {
102
+ return cleaned;
103
+ }
104
+ }
105
+ }
106
+ } catch {}
107
+
108
+ return "";
109
+ }
110
+
71
111
  export async function runUpdateCommand(): Promise<void> {
72
112
  let pkg: any = { name: "qwenproxy-cli", version: "1.0.0" };
73
113
  try {
@@ -82,16 +122,7 @@ export async function runUpdateCommand(): Promise<void> {
82
122
  console.log(`⚙️ [QwenProxy] Gerenciador de pacotes detectado: ${pm}`);
83
123
  console.log(`🔍 [QwenProxy] Verificando se há novas versões de ${packageName} no npm registry...`);
84
124
 
85
- let latestVersion = "";
86
- try {
87
- const fullCmd = `npm view ${packageName} version`;
88
- const res = spawnSync(fullCmd, {
89
- encoding: "utf-8",
90
- shell: true,
91
- timeout: 10000,
92
- });
93
- latestVersion = res.stdout ? res.stdout.trim() : "";
94
- } catch {}
125
+ const latestVersion = await fetchLatestNpmVersion(packageName);
95
126
 
96
127
  if (!latestVersion) {
97
128
  console.warn("⚠️ [QwenProxy] Não foi possível consultar o registro online.");