qwenproxy-cli 1.0.32 → 1.2.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.
@@ -4,9 +4,11 @@
4
4
 
5
5
  import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
6
6
  import type { KeyEvent } from "../screen.ts";
7
- import { theme, glyphs, drawBox, pad, truncate } from "../theme.ts";
7
+ import { theme, glyphs, drawBox, pad, truncate, setClipboardText } from "../theme.ts";
8
8
  import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
+ import { getRuntimeChatMode, cycleNextChatMode } from "../../core/config.ts";
11
+ import { saveTuiSettings } from "../settings.ts";
10
12
 
11
13
  export function renderProgressBar(
12
14
  pct: number,
@@ -59,13 +61,21 @@ export class StatusView implements TuiView {
59
61
  private actionMessage = "";
60
62
  private actionMessageTimeout: NodeJS.Timeout | null = null;
61
63
  private hoveredActionRow: number | null = null;
64
+ private isBaseUrlHovered = false;
65
+ private copiedRecently = false;
66
+ private copiedTimeout: NodeJS.Timeout | null = null;
67
+ private lastBaseUrl = "http://127.0.0.1:7936/v1";
68
+ private lastBaseUrlRow = 6;
69
+ private lastModoApiRow = 7;
62
70
  private lastLeftW = 38;
63
71
  private lastActionRecarregarRow = 20;
64
72
  private lastActionZerarRow = 21;
73
+ private lastActionModoRow = 22;
74
+ private lastActionCopiarRow = 23;
75
+
65
76
  constructor() {
66
77
  this.refresh();
67
78
  }
68
-
69
79
  public async refresh(): Promise<void> {
70
80
  try {
71
81
  if (process.stdout.isTTY && !process.env.NODE_TEST_CONTEXT) {
@@ -86,6 +96,8 @@ export class StatusView implements TuiView {
86
96
  return [
87
97
  { key: "r", label: "Recarregar" },
88
98
  { key: "z", label: "Zerar Cooldowns" },
99
+ { key: "m", label: "Alternar Modo" },
100
+ { key: "c", label: "Copiar URL" },
89
101
  ];
90
102
  }
91
103
 
@@ -98,23 +110,34 @@ export class StatusView implements TuiView {
98
110
  }
99
111
 
100
112
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
101
- // Mouse hover over quick actions
113
+ // Mouse hover over quick actions or Base URL
102
114
  if (key.name === "hover" && key.mouse) {
103
115
  const { row, col } = key.mouse;
104
116
  const leftW = this.lastLeftW || 38;
105
- if (
106
- col >= 2 &&
107
- col <= leftW - 1 &&
108
- (row === this.lastActionRecarregarRow || row === this.lastActionZerarRow)
109
- ) {
117
+ const isOverLeft = col >= 2 && col <= leftW - 1;
118
+ const isOverBaseUrl = isOverLeft && row === this.lastBaseUrlRow;
119
+ const isOverAction =
120
+ isOverLeft &&
121
+ (row === this.lastActionRecarregarRow ||
122
+ row === this.lastActionZerarRow ||
123
+ row === this.lastActionModoRow ||
124
+ row === this.lastActionCopiarRow);
125
+
126
+ let changed = false;
127
+ if (isOverBaseUrl !== this.isBaseUrlHovered) {
128
+ this.isBaseUrlHovered = isOverBaseUrl;
129
+ changed = true;
130
+ }
131
+ if (isOverAction) {
110
132
  if (this.hoveredActionRow !== row) {
111
133
  this.hoveredActionRow = row;
112
- return true;
134
+ changed = true;
113
135
  }
114
136
  } else if (this.hoveredActionRow !== null) {
115
137
  this.hoveredActionRow = null;
116
- return true;
138
+ changed = true;
117
139
  }
140
+ if (changed) return true;
118
141
  }
119
142
 
120
143
  // Mouse click interactions
@@ -122,6 +145,17 @@ export class StatusView implements TuiView {
122
145
  const { row, col } = key.mouse;
123
146
  const leftW = this.lastLeftW || 38;
124
147
  if (col >= 2 && col <= leftW - 1) {
148
+ if (row === this.lastBaseUrlRow || row === this.lastActionCopiarRow) {
149
+ setClipboardText(this.lastBaseUrl);
150
+ this.copiedRecently = true;
151
+ if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
152
+ this.copiedTimeout = setTimeout(() => {
153
+ this.copiedRecently = false;
154
+ this.copiedTimeout = null;
155
+ }, 2500);
156
+ this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
157
+ return true;
158
+ }
125
159
  if (row === this.lastActionRecarregarRow) {
126
160
  await this.refresh();
127
161
  this.setMessage(theme.green("✓ Status atualizado"));
@@ -133,9 +167,15 @@ export class StatusView implements TuiView {
133
167
  this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
134
168
  return true;
135
169
  }
170
+ if (row === this.lastActionModoRow || row === this.lastModoApiRow) {
171
+ const nextMode = cycleNextChatMode();
172
+ saveTuiSettings({ chat: { mode: nextMode } });
173
+ await this.refresh();
174
+ this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
175
+ return true;
176
+ }
136
177
  }
137
178
  }
138
-
139
179
  if ((key.name === "r" || key.name === "R") && !key.ctrl) {
140
180
  await this.refresh();
141
181
  this.setMessage(theme.green("✓ Status atualizado"));
@@ -148,8 +188,27 @@ export class StatusView implements TuiView {
148
188
  this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
149
189
  return true;
150
190
  }
151
- }
152
191
 
192
+ if ((key.name === "m" || key.name === "M") && !key.ctrl) {
193
+ const nextMode = cycleNextChatMode();
194
+ saveTuiSettings({ chat: { mode: nextMode } });
195
+ await this.refresh();
196
+ this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
197
+ return true;
198
+ }
199
+
200
+ if ((key.name === "c" || key.name === "C") && !key.ctrl && !key.meta) {
201
+ setClipboardText(this.lastBaseUrl);
202
+ this.copiedRecently = true;
203
+ if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
204
+ this.copiedTimeout = setTimeout(() => {
205
+ this.copiedRecently = false;
206
+ this.copiedTimeout = null;
207
+ }, 2500);
208
+ this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
209
+ return true;
210
+ }
211
+ }
153
212
  public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
154
213
  const data = snapshot || this.statusData;
155
214
  const isOnline = data?.online ?? false;
@@ -177,7 +236,9 @@ export class StatusView implements TuiView {
177
236
  const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
178
237
  const uptimeStr = formatUptime(uptimeSecs);
179
238
  const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
180
-
239
+ this.lastBaseUrl = baseUrl;
240
+ this.lastBaseUrlRow = 6;
241
+ this.lastModoApiRow = 7;
181
242
  const m = data?.metrics;
182
243
  const reqsTotal = m?.requestsTotal ?? 0;
183
244
  const reqsErrors = m?.requestsErrors ?? 0;
@@ -210,12 +271,28 @@ export class StatusView implements TuiView {
210
271
  if (data?.waitingStreams && data.waitingStreams > 0) {
211
272
  connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
212
273
  }
274
+ let urlDisplay: string;
275
+ if (this.copiedRecently) {
276
+ urlDisplay = theme.bold(theme.green(baseUrl));
277
+ } else if (this.isBaseUrlHovered) {
278
+ urlDisplay = theme.bold(theme.underline(theme.cyan(baseUrl)));
279
+ } else {
280
+ urlDisplay = theme.cyan(baseUrl);
281
+ }
213
282
 
214
283
  const leftContent: string[] = [
215
284
  ` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
216
- ` ${theme.bold(lbl("Base URL:"))} ${theme.cyan(baseUrl)}`,
285
+ ` ${theme.bold(lbl("Base URL:"))} ${urlDisplay}`,
286
+ ` ${theme.bold(lbl("Modo API:"))} ${
287
+ getRuntimeChatMode() === "thread"
288
+ ? theme.cyan("[thread]")
289
+ : getRuntimeChatMode() === "thread-temp"
290
+ ? theme.green("[thread-temp]")
291
+ : getRuntimeChatMode() === "stateless-temp"
292
+ ? theme.yellow("[stateless-temp]")
293
+ : theme.lavender("[stateless]")
294
+ } ${theme.dim("('M' alternar)")}`,
217
295
  ` ${theme.bold(lbl("Uptime:"))} ${theme.cyan(uptimeStr)}`,
218
- ` ${theme.bold(lbl("Memória:"))} ${theme.cyan(ramStr)}`,
219
296
  ` ${theme.bold(lbl("Conexões:"))} ${connsStr}`,
220
297
  ` ${theme.dim("───────────────────────────────────────")}`,
221
298
  ` ${theme.bold("Tráfego & Performance:")}`,
@@ -233,12 +310,19 @@ export class StatusView implements TuiView {
233
310
 
234
311
  const recarregarIdx = leftContent.length;
235
312
  const zerarIdx = leftContent.length + 1;
313
+ const modoIdx = leftContent.length + 2;
314
+ const copiarIdx = leftContent.length + 3;
315
+
236
316
  this.lastActionRecarregarRow = 5 + recarregarIdx;
237
317
  this.lastActionZerarRow = 5 + zerarIdx;
318
+ this.lastActionModoRow = 5 + modoIdx;
319
+ this.lastActionCopiarRow = 5 + copiarIdx;
238
320
 
239
321
  leftContent.push(
240
322
  ` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
241
323
  ` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
324
+ ` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
325
+ ` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
242
326
  );
243
327
 
244
328
  const boxHeight = Math.max(contentH, leftContent.length + 2);
@@ -12,7 +12,7 @@ import {
12
12
  inspectClientSyncStatus,
13
13
  } from "../../sync/index.ts";
14
14
  import type { SyncClientName } from "../../sync/types.ts";
15
- import { fetchLiveModels } from "../proxy-client.ts";
15
+ import { fetchLiveModels, DEFAULT_FALLBACK_MODELS } from "../proxy-client.ts";
16
16
 
17
17
  interface ClientOption {
18
18
  id: SyncClientName;
@@ -30,18 +30,17 @@ export class SyncView implements TuiView {
30
30
  private clients: ClientOption[] = [];
31
31
  private selectedRowIndex = 0; // 0..9 for clients, 10 for model, 11 for scope, 12 for sync, 13 for restore
32
32
  private hoveredActionRow: number | null = null;
33
- private availableModels = [
34
- "qwen3.8-max",
35
- "qwen3.7-plus",
36
- "qwen3.7-max",
37
- "z-image-turbo",
38
- "qwen-image-3.0-pro",
39
- "wan3.0-video",
40
- ];
33
+ private availableModels = [...DEFAULT_FALLBACK_MODELS];
41
34
  private modelIndex = 0;
42
35
  private syncAllModels = true;
43
36
  private actionLog: string[] = [];
44
37
  private lastLeftW = 46;
38
+ private lastClientStartRow = 8;
39
+ private lastClientEndRow = 17;
40
+ private lastModelRow = 20;
41
+ private lastScopeRow = 21;
42
+ private lastSyncRow = 24;
43
+ private lastRestoreRow = 25;
45
44
  constructor() {
46
45
  this.detectClients();
47
46
  }
@@ -50,11 +49,14 @@ export class SyncView implements TuiView {
50
49
  this.detectClients();
51
50
  void this.refreshModels();
52
51
  }
53
- private async refreshModels(): Promise<void> {
52
+ public async refreshModels(): Promise<void> {
54
53
  try {
55
54
  const live = await fetchLiveModels();
56
55
  if (live && live.length > 0) {
56
+ const current = this.availableModels[this.modelIndex];
57
57
  this.availableModels = live;
58
+ const foundIdx = this.availableModels.indexOf(current);
59
+ this.modelIndex = foundIdx !== -1 ? foundIdx : 0;
58
60
  }
59
61
  } catch {}
60
62
  }
@@ -103,27 +105,62 @@ export class SyncView implements TuiView {
103
105
  const { row, col } = key.mouse;
104
106
  const leftW = this.lastLeftW || 46;
105
107
  if (col >= 2 && col <= leftW - 1) {
106
- if (row >= 8 && row <= 17) {
107
- const targetRow = row - 8;
108
+ if (row >= this.lastClientStartRow && row <= this.lastClientEndRow) {
109
+ const targetRow = row - this.lastClientStartRow;
110
+ let changed = false;
108
111
  if (this.selectedRowIndex !== targetRow) {
109
112
  this.selectedRowIndex = targetRow;
110
- return true;
113
+ changed = true;
114
+ }
115
+ if (this.hoveredActionRow !== null) {
116
+ this.hoveredActionRow = null;
117
+ changed = true;
111
118
  }
112
- } else if (row === 20 || row === 14) {
119
+ if (changed) return true;
120
+ } else if (row === this.lastModelRow) {
121
+ let changed = false;
113
122
  if (this.selectedRowIndex !== 10) {
114
123
  this.selectedRowIndex = 10;
115
- return true;
124
+ changed = true;
116
125
  }
117
- } else if (row === 21 || row === 15) {
126
+ if (this.hoveredActionRow !== null) {
127
+ this.hoveredActionRow = null;
128
+ changed = true;
129
+ }
130
+ if (changed) return true;
131
+ } else if (row === this.lastScopeRow) {
132
+ let changed = false;
118
133
  if (this.selectedRowIndex !== 11) {
119
134
  this.selectedRowIndex = 11;
120
- return true;
135
+ changed = true;
121
136
  }
122
- } else if (row === 24 || row === 18 || row === 25 || row === 19) {
123
- if (this.hoveredActionRow !== row) {
124
- this.hoveredActionRow = row;
125
- return true;
137
+ if (this.hoveredActionRow !== null) {
138
+ this.hoveredActionRow = null;
139
+ changed = true;
140
+ }
141
+ if (changed) return true;
142
+ } else if (row === this.lastSyncRow) {
143
+ let changed = false;
144
+ if (this.selectedRowIndex !== 12) {
145
+ this.selectedRowIndex = 12;
146
+ changed = true;
147
+ }
148
+ if (this.hoveredActionRow !== this.lastSyncRow) {
149
+ this.hoveredActionRow = this.lastSyncRow;
150
+ changed = true;
151
+ }
152
+ if (changed) return true;
153
+ } else if (row === this.lastRestoreRow) {
154
+ let changed = false;
155
+ if (this.selectedRowIndex !== 13) {
156
+ this.selectedRowIndex = 13;
157
+ changed = true;
126
158
  }
159
+ if (this.hoveredActionRow !== this.lastRestoreRow) {
160
+ this.hoveredActionRow = this.lastRestoreRow;
161
+ changed = true;
162
+ }
163
+ if (changed) return true;
127
164
  } else if (this.hoveredActionRow !== null) {
128
165
  this.hoveredActionRow = null;
129
166
  return true;
@@ -140,35 +177,40 @@ export class SyncView implements TuiView {
140
177
  const leftW = this.lastLeftW || 46;
141
178
  if (col >= 2 && col <= leftW - 1) {
142
179
  // Rows 8..17: Toggle client
143
- if (row >= 8 && row <= 17) {
144
- const client = this.clients[row - 8];
180
+ if (row >= this.lastClientStartRow && row <= this.lastClientEndRow) {
181
+ const client = this.clients[row - this.lastClientStartRow];
145
182
  if (client) {
146
183
  client.selected = !client.selected;
147
- this.selectedRowIndex = row - 8;
184
+ this.selectedRowIndex = row - this.lastClientStartRow;
185
+ this.hoveredActionRow = null;
148
186
  return true;
149
187
  }
150
188
  }
151
189
  // Model selector
152
- if (row === 20 || row === 14) {
190
+ if (row === this.lastModelRow) {
153
191
  this.modelIndex = (this.modelIndex + 1) % this.availableModels.length;
154
192
  this.selectedRowIndex = 10;
193
+ this.hoveredActionRow = null;
155
194
  return true;
156
195
  }
157
196
  // Scope selector
158
- if (row === 21 || row === 15) {
197
+ if (row === this.lastScopeRow) {
159
198
  this.syncAllModels = !this.syncAllModels;
160
199
  this.selectedRowIndex = 11;
200
+ this.hoveredActionRow = null;
161
201
  return true;
162
202
  }
163
203
  // Sincronizar button
164
- if (row === 24 || row === 18) {
204
+ if (row === this.lastSyncRow) {
165
205
  this.selectedRowIndex = 12;
206
+ this.hoveredActionRow = this.lastSyncRow;
166
207
  this.executeSync();
167
208
  return true;
168
209
  }
169
210
  // Restaurar button
170
- if (row === 25 || row === 19) {
211
+ if (row === this.lastRestoreRow) {
171
212
  this.selectedRowIndex = 13;
213
+ this.hoveredActionRow = this.lastRestoreRow;
172
214
  this.executeRollback();
173
215
  return true;
174
216
  }
@@ -282,9 +324,16 @@ export class SyncView implements TuiView {
282
324
  }
283
325
 
284
326
  private executeRollback(): void {
285
- this.actionLog.unshift(theme.yellow("⏳ Restaurando backups anteriores de configuração..."));
327
+ const selectedTargets = this.clients
328
+ .filter((c) => c.selected)
329
+ .map((c) => c.id);
330
+
331
+ const targetDesc = selectedTargets.length > 0 ? `[${selectedTargets.join(", ")}]` : "todos os clientes";
332
+ this.actionLog.unshift(theme.yellow(`⏳ Restaurando backups anteriores de ${targetDesc}...`));
286
333
  try {
287
- const res = restoreAllClients();
334
+ const res = restoreAllClients({
335
+ targets: selectedTargets.length > 0 ? selectedTargets : undefined,
336
+ });
288
337
  this.actionLog.unshift(
289
338
  theme.green(`✓ Rollback concluído: ${res.restoredCount} arquivo(s) restaurados com sucesso.`),
290
339
  );
@@ -298,6 +347,12 @@ export class SyncView implements TuiView {
298
347
  const contentH = Math.max(22, height);
299
348
  const leftW = Math.max(48, Math.floor(width * 0.52));
300
349
  this.lastLeftW = leftW;
350
+ this.lastClientStartRow = 8;
351
+ this.lastClientEndRow = 8 + this.clients.length - 1;
352
+ this.lastModelRow = this.lastClientEndRow + 3;
353
+ this.lastScopeRow = this.lastModelRow + 1;
354
+ this.lastSyncRow = this.lastScopeRow + 3;
355
+ this.lastRestoreRow = this.lastSyncRow + 1;
301
356
  const rightW = Math.max(30, width - leftW - 1);
302
357
 
303
358
  // Left Panel: Options and Selectors
@@ -351,13 +406,13 @@ export class SyncView implements TuiView {
351
406
  leftContent.push(` ${theme.bold("Ações:")}`);
352
407
  // Row index 12: Sincronizar
353
408
  const isSyncFocused = this.selectedRowIndex === 12;
354
- const isSyncHovered = this.hoveredActionRow === 24 || this.hoveredActionRow === 18;
409
+ const isSyncHovered = this.hoveredActionRow === this.lastSyncRow;
355
410
  const syncLine = ` ${isSyncHovered || isSyncFocused ? theme.bgHover(` ${theme.cyan("[ Enter ] Sincronizar")} `) : `${theme.cyan("[ Enter ]")} Sincronizar`}`;
356
411
  leftContent.push(syncLine);
357
412
 
358
413
  // Row index 13: Restaurar
359
414
  const isRestoreFocused = this.selectedRowIndex === 13;
360
- const isRestoreHovered = this.hoveredActionRow === 25 || this.hoveredActionRow === 19;
415
+ const isRestoreHovered = this.hoveredActionRow === this.lastRestoreRow;
361
416
  const restoreLine = ` ${isRestoreHovered || isRestoreFocused ? theme.bgHover(` ${theme.yellow("[ R ] Restaurar")} `) : `${theme.yellow("[ R ]")} Restaurar`}`;
362
417
  leftContent.push(restoreLine);
363
418
 
package/src/update-cli.ts CHANGED
@@ -1,161 +1,17 @@
1
- import { spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const packageRoot = path.resolve(__dirname, "..");
8
- const packageJsonPath = path.join(packageRoot, "package.json");
9
-
10
1
  export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
11
2
 
12
- export function detectPackageManager(): PackageManager {
13
- const userAgent = process.env.npm_config_user_agent || "";
14
- if (userAgent.startsWith("bun")) return "bun";
15
- if (userAgent.startsWith("pnpm")) return "pnpm";
16
- if (userAgent.startsWith("yarn")) return "yarn";
17
-
18
- // Check which executable is actually running the script or installed in PATH
19
- const execPath = process.execPath.toLowerCase();
20
- if (execPath.includes("bun")) return "bun";
21
-
22
- // Check if installation path contains pnpm / bun / yarn markers
23
- const currentPath = packageRoot.toLowerCase();
24
- if (currentPath.includes(".pnpm") || currentPath.includes("pnpm")) return "pnpm";
25
- if (currentPath.includes(".bun") || currentPath.includes("bun")) return "bun";
26
- if (currentPath.includes("yarn")) return "yarn";
27
-
28
- return "npm";
29
- }
30
- export function getUpdateArgs(
31
- pm: PackageManager,
32
- packageName: string,
33
- targetVersion?: string,
34
- ): { cmd: string; args: string[] } {
35
- const versionTag = targetVersion ? `@${targetVersion}` : "@latest";
36
- switch (pm) {
37
- case "bun":
38
- return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
39
- case "pnpm":
40
- return {
41
- cmd: "pnpm",
42
- args: targetVersion
43
- ? ["add", "-g", `${packageName}@${targetVersion}`]
44
- : ["update", "-g", packageName],
45
- };
46
- case "yarn":
47
- return {
48
- cmd: "yarn",
49
- args: targetVersion
50
- ? ["global", "add", `${packageName}@${targetVersion}`]
51
- : ["global", "upgrade", packageName],
52
- };
53
- case "npm":
54
- default:
55
- return { cmd: "npm", args: ["install", "-g", `${packageName}${versionTag}`] };
56
- }
57
- }
58
-
59
- export function isNewerVersion(current: string, latest: string): boolean {
60
- const c = current.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
61
- const l = latest.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
62
- for (let i = 0; i < 3; i++) {
63
- const cv = c[i] || 0;
64
- const lv = l[i] || 0;
65
- if (lv > cv) return true;
66
- if (lv < cv) return false;
67
- }
68
- return false;
69
- }
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 {}
3
+ export {
4
+ detectPackageManager,
5
+ getUpdateArgs,
6
+ isNewerVersion,
7
+ fetchLatestNpmVersion,
8
+ runUpdateCommand,
9
+ } from "../bin/update.js";
107
10
 
108
- return "";
109
- }
110
-
111
- export async function runUpdateCommand(): Promise<void> {
112
- let pkg: any = { name: "qwenproxy-cli", version: "1.0.0" };
113
- try {
114
- pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
115
- } catch {}
116
-
117
- const currentVersion = pkg.version || "1.0.0";
118
- const packageName = pkg.name || "qwenproxy-cli";
119
- const pm = detectPackageManager();
120
-
121
- console.log(`\n📦 [QwenProxy] Versão local instalada: v${currentVersion}`);
122
- console.log(`⚙️ [QwenProxy] Gerenciador de pacotes detectado: ${pm}`);
123
- console.log(`🔍 [QwenProxy] Verificando se há novas versões de ${packageName} no npm registry...`);
124
-
125
- const latestVersion = await fetchLatestNpmVersion(packageName);
126
-
127
- if (!latestVersion) {
128
- console.warn("⚠️ [QwenProxy] Não foi possível consultar o registro online.");
129
- const manual = getUpdateArgs(pm, packageName);
130
- console.log(`👉 Você pode forçar a atualização manualmente com:\n ${manual.cmd} ${manual.args.join(" ")}\n`);
131
- return;
132
- }
133
-
134
- console.log(`🌐 [QwenProxy] Versão mais recente disponível: v${latestVersion}`);
135
-
136
- if (!isNewerVersion(currentVersion, latestVersion)) {
137
- console.log(`\n✨ Você já está utilizando a versão mais recente (v${currentVersion})!\n`);
138
- return;
139
- }
140
-
141
- console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
142
- const { cmd, args } = getUpdateArgs(pm, packageName, latestVersion);
143
- console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
144
- const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
145
- const updateProc = spawnSync(fullUpdateCmd, {
146
- stdio: "inherit",
147
- shell: true,
148
- });
149
- if (updateProc.status === 0) {
150
- console.log(`\n✅ [QwenProxy] Atualizado com sucesso para a versão v${latestVersion}!`);
151
- console.log("👉 Digite 'qpx' para iniciar a nova versão.\n");
152
- } else {
153
- console.error(`\n❌ [QwenProxy] Falha ao atualizar automaticamente com ${cmd}.`);
154
- console.log(`👉 Tente executar manualmente:\n ${cmd} ${args.join(" ")}\n`);
155
- }
156
- }
11
+ import { runUpdateCommand } from "../bin/update.js";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
157
14
 
158
- // Execute if run directly via CLI runner, not when imported in unit tests
159
15
  const isDirectRun =
160
16
  Boolean(process.argv[1]) &&
161
17
  (fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) ||