@swimmingliu/autovpn 1.6.6 → 1.6.7

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.
@@ -1,27 +1,7 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
1
  import net from 'node:net';
4
2
  import tls from 'node:tls';
5
- import { mergeProjectEnv } from '../runtime/env.js';
6
3
  import { openMihomoRuntime as defaultOpenMihomoRuntime, probeMihomoProxyDelay as defaultProbeMihomoProxyDelay } from './proxy-runtime.js';
7
- const PYTHON_SPEEDTEST_HELPER = `
8
- import json
9
- import sys
10
- from vpn_automation.config.models import SpeedTestConfig
11
- from vpn_automation.pipeline.speedtest import speedtest_links
12
-
13
- payload = json.load(sys.stdin)
14
- output = [
15
- item.__dict__
16
- for item in speedtest_links(
17
- payload.get("links", []),
18
- SpeedTestConfig(**payload["config"]),
19
- runtime_path=payload.get("runtime_path", ""),
20
- )
21
- ]
22
- json.dump(output, sys.stdout, ensure_ascii=False)
23
- sys.stdout.write("\\n")
24
- `;
4
+ const PROBE_MAX_ATTEMPTS = 2;
25
5
  export function aggregateSpeedMeasurements(values) {
26
6
  if (values.length === 0) {
27
7
  return 0;
@@ -94,9 +74,27 @@ function defaultNow() {
94
74
  }
95
75
  async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
96
76
  const controller = new AbortController();
97
- const timer = setTimeout(() => controller.abort(), timeoutMs);
77
+ let internallyTimedOut = false;
78
+ const timer = setTimeout(() => {
79
+ internallyTimedOut = true;
80
+ controller.abort();
81
+ }, timeoutMs);
98
82
  try {
99
- return await fetchImpl(url, { signal: controller.signal });
83
+ const response = await fetchImpl(url, { signal: controller.signal });
84
+ if (internallyTimedOut) {
85
+ const error = new Error(`request timed out after ${timeoutMs}ms`);
86
+ error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
87
+ throw error;
88
+ }
89
+ return response;
90
+ }
91
+ catch (error) {
92
+ if (internallyTimedOut && !(error instanceof Error && error.code === 'AUTOVPN_INTERNAL_TIMEOUT')) {
93
+ const timeoutError = new Error(`request timed out after ${timeoutMs}ms`, { cause: error });
94
+ timeoutError.code = 'AUTOVPN_INTERNAL_TIMEOUT';
95
+ throw timeoutError;
96
+ }
97
+ throw error;
100
98
  }
101
99
  finally {
102
100
  clearTimeout(timer);
@@ -122,6 +120,7 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
122
120
  if (response.body?.getReader) {
123
121
  const reader = response.body.getReader();
124
122
  let timedOut = false;
123
+ let reachedCap = false;
125
124
  try {
126
125
  return await withBodyTimeout(async () => {
127
126
  let total = 0;
@@ -131,6 +130,9 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
131
130
  break;
132
131
  }
133
132
  total += Math.min(value.byteLength, maxBytes - total);
133
+ if (total >= maxBytes) {
134
+ reachedCap = true;
135
+ }
134
136
  }
135
137
  return total;
136
138
  }, timeoutMs);
@@ -140,16 +142,13 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
140
142
  throw error;
141
143
  }
142
144
  finally {
143
- if (timedOut) {
145
+ if (timedOut || reachedCap) {
144
146
  await reader.cancel().catch(() => { });
145
147
  }
146
148
  reader.releaseLock();
147
149
  }
148
150
  }
149
- if (response.arrayBuffer) {
150
- return await withBodyTimeout(async () => Math.min((await response.arrayBuffer()).byteLength, maxBytes), timeoutMs);
151
- }
152
- return 0;
151
+ throw new Error('streaming response body required for bounded speed test downloads');
153
152
  }
154
153
  function requireOkResponse(response, allowedStatuses) {
155
154
  const status = Number(response.status ?? 200);
@@ -157,6 +156,36 @@ function requireOkResponse(response, allowedStatuses) {
157
156
  throw new Error(`unexpected status ${status}`);
158
157
  }
159
158
  }
159
+ function isTransientProbeError(error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ const code = typeof error === 'object' && error !== null && 'code' in error
162
+ ? String(error.code ?? '')
163
+ : '';
164
+ const statusMatch = /(?:unexpected status|failed with status)\s+(\d{3})\b/i.exec(message);
165
+ if (statusMatch) {
166
+ const status = Number(statusMatch[1]);
167
+ return status >= 500 && status <= 599;
168
+ }
169
+ if (['AUTOVPN_INTERNAL_TIMEOUT', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ECONNREFUSED', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'].includes(code.toUpperCase())) {
170
+ return true;
171
+ }
172
+ return error instanceof TypeError && message.trim().toLowerCase() === 'fetch failed';
173
+ }
174
+ async function retryTransientProbe(operation) {
175
+ let lastError;
176
+ for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt += 1) {
177
+ try {
178
+ return await operation();
179
+ }
180
+ catch (error) {
181
+ lastError = error;
182
+ if (attempt >= PROBE_MAX_ATTEMPTS || !isTransientProbeError(error)) {
183
+ throw error;
184
+ }
185
+ }
186
+ }
187
+ throw lastError;
188
+ }
160
189
  function socketConnect(host, port, timeoutMs) {
161
190
  return new Promise((resolve, reject) => {
162
191
  const socket = net.createConnection({ host, port });
@@ -346,12 +375,14 @@ async function probeLinksDirect(links, config, options) {
346
375
  const now = options.now ?? defaultNow;
347
376
  const timeoutMs = Math.max(1, Number(config.timeout_seconds)) * 1000;
348
377
  return mapWithConcurrency(links, config.concurrency, async (link) => {
349
- const started = now();
350
378
  try {
351
- const response = await fetchWithTimeout(fetchImpl, config.probe_url, timeoutMs);
352
- const elapsed = Math.max(now() - started, 1);
353
- requireOkResponse(response, new Set([200, 204]));
354
- return { link, reachable: true, latency_ms: Math.max(Math.round(elapsed), 1), error: '' };
379
+ return await retryTransientProbe(async () => {
380
+ const started = now();
381
+ const response = await fetchWithTimeout(fetchImpl, config.probe_url, timeoutMs);
382
+ const elapsed = Math.max(now() - started, 1);
383
+ requireOkResponse(response, new Set([200, 204]));
384
+ return { link, reachable: true, latency_ms: Math.max(Math.round(elapsed), 1), error: '' };
385
+ });
355
386
  }
356
387
  catch (error) {
357
388
  return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
@@ -362,22 +393,26 @@ async function probeLinksMihomo(links, config, runtimePath, options) {
362
393
  const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
363
394
  const probeDelay = options.probeMihomoProxyDelay ?? defaultProbeMihomoProxyDelay;
364
395
  return mapWithConcurrency(links, config.concurrency, async (link) => {
365
- let runtime;
366
396
  try {
367
- runtime = await openRuntime(link, {
368
- runtimePath,
369
- startupWaitSeconds: config.startup_wait_seconds,
370
- env: options.env
397
+ return await retryTransientProbe(async () => {
398
+ let runtime;
399
+ try {
400
+ runtime = await openRuntime(link, {
401
+ runtimePath,
402
+ startupWaitSeconds: config.startup_wait_seconds,
403
+ env: options.env
404
+ });
405
+ const latencyMs = await probeDelay(runtime.controllerUrl, runtime.proxyName, config.probe_url, config.timeout_seconds);
406
+ return { link, reachable: true, latency_ms: latencyMs, error: '' };
407
+ }
408
+ finally {
409
+ await runtime?.close();
410
+ }
371
411
  });
372
- const latencyMs = await probeDelay(runtime.controllerUrl, runtime.proxyName, config.probe_url, config.timeout_seconds);
373
- return { link, reachable: true, latency_ms: latencyMs, error: '' };
374
412
  }
375
413
  catch (error) {
376
414
  return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
377
415
  }
378
- finally {
379
- await runtime?.close();
380
- }
381
416
  });
382
417
  }
383
418
  async function testLinkDirect(link, config, options) {
@@ -495,6 +530,17 @@ async function speedtestInNode(input, options) {
495
530
  urls: [...config.urls]
496
531
  });
497
532
  const probes = await probeLinks(input.links, config, { runtime_path: runtimePath });
533
+ for (let index = 0; index < probes.length; index += 1) {
534
+ const probe = probes[index];
535
+ emitEvent(options.eventCallback, 'speedtest_probe_result', {
536
+ completed: index + 1,
537
+ total: input.links.length,
538
+ link: probe.link,
539
+ reachable: probe.reachable,
540
+ latency_ms: probe.latency_ms,
541
+ error: probe.error ?? ''
542
+ });
543
+ }
498
544
  const candidateLinks = selectSpeedtestCandidates(probes, config.max_download_candidates);
499
545
  const probeByLink = new Map(probes.map((probe) => [probe.link, probe]));
500
546
  const candidateSet = new Set(candidateLinks);
@@ -533,64 +579,7 @@ async function speedtestInNode(input, options) {
533
579
  results.push(...testedResults);
534
580
  return results;
535
581
  }
536
- export function selectPipelineStageBackend(stage, env = process.env) {
537
- void stage;
538
- void env;
539
- return 'node';
540
- }
541
- async function defaultResolvePythonCli(env) {
542
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
543
- const runner = await import('../../lib/runner.mjs');
544
- return runner.resolveOrInstallPythonCli({ env });
545
- }
546
- function pythonCommandFor(resolved) {
547
- const command = resolved.command;
548
- const name = path.basename(command).toLowerCase();
549
- if (['autovpn', 'autovpn.exe'].includes(name)) {
550
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
551
- return path.join(path.dirname(command), executable);
552
- }
553
- return process.platform === 'win32' ? 'python.exe' : 'python3';
554
- }
555
- async function speedtestWithPython(input, options) {
556
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
557
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
558
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_SPEEDTEST_HELPER], {
559
- cwd: options.cwd ?? process.cwd(),
560
- env,
561
- stdio: ['pipe', 'pipe', 'pipe']
562
- });
563
- let stdout = '';
564
- let stderr = '';
565
- child.stdout?.on('data', (chunk) => {
566
- stdout += String(chunk);
567
- });
568
- child.stderr?.on('data', (chunk) => {
569
- stderr += String(chunk);
570
- });
571
- const completion = new Promise((resolve, reject) => {
572
- child.on('error', reject);
573
- child.on('close', (code) => {
574
- if (code !== 0) {
575
- reject(new Error(`Python speedtest backend failed with exit code ${code}: ${stderr.trim()}`));
576
- return;
577
- }
578
- try {
579
- resolve(JSON.parse(stdout));
580
- }
581
- catch (error) {
582
- reject(new Error(`Python speedtest backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
583
- }
584
- });
585
- });
586
- child.stdin?.write(JSON.stringify(input));
587
- child.stdin?.end();
588
- return completion;
589
- }
590
582
  export async function speedtestLinksWithBackend(input, options = {}) {
591
- if (selectPipelineStageBackend('speedtest', options.env ?? process.env) === 'python') {
592
- return options.pythonSpeedtest ? options.pythonSpeedtest(input) : speedtestWithPython(input, options);
593
- }
594
583
  return speedtestInNode(input, options);
595
584
  }
596
585
  export async function probeSpeedtestLinksInNode(input, options = {}) {
@@ -5,8 +5,17 @@ export function resolveRuntimeRoot(candidate) {
5
5
  const resolved = fs.existsSync(absolute) ? fs.realpathSync(absolute) : absolute;
6
6
  let current = fs.existsSync(resolved) && fs.statSync(resolved).isFile() ? path.dirname(resolved) : resolved;
7
7
  while (true) {
8
- if (fs.existsSync(path.join(current, 'pyproject.toml'))) {
9
- return current;
8
+ const packagePath = path.join(current, 'package.json');
9
+ if (fs.existsSync(packagePath)) {
10
+ try {
11
+ const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
12
+ if (manifest.name === 'vpn-subscription-automation') {
13
+ return current;
14
+ }
15
+ }
16
+ catch {
17
+ // Continue walking when an unrelated package manifest is invalid.
18
+ }
10
19
  }
11
20
  const parent = path.dirname(current);
12
21
  if (parent === current) {
@@ -5,7 +5,7 @@ const ZH_MESSAGES = {
5
5
  locale: 'zh-CN',
6
6
  appTitle: 'AutoVPN',
7
7
  sidebarTitle: 'AutoVPN',
8
- sidebarVersion: 'v.1.6.6',
8
+ sidebarVersion: 'v.1.6.7',
9
9
  brandSubtitle: '概览、运行、结果、订阅、日志、设置统一管理',
10
10
  languageLabel: '',
11
11
  saveButton: '保存配置',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmingliu/autovpn",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
4
4
  "description": "npm wrapper for the AutoVPN headless CLI",
5
5
  "type": "module",
6
6
  "repository": {
@@ -14,16 +14,15 @@
14
14
  "files": [
15
15
  "bin/",
16
16
  "dist/",
17
- "lib/",
18
17
  "README.md"
19
18
  ],
20
19
  "publishConfig": {
21
20
  "access": "public"
22
21
  },
23
22
  "scripts": {
24
- "build": "tsc -p tsconfig.json && node scripts/copy-web-assets.mjs",
23
+ "build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node scripts/copy-web-assets.mjs",
25
24
  "prepack": "npm run build",
26
- "test": "npm run build && node --test test/*.test.mjs test/doctor/*.test.mjs test/parity/*.test.mjs test/jobs/*.test.mjs test/pipeline/*.test.mjs test/runtime/*.test.mjs test/server/*.test.mjs"
25
+ "test": "npm run build && node --test test/*.test.mjs test/doctor/*.test.mjs test/jobs/*.test.mjs test/pipeline/*.test.mjs test/runtime/*.test.mjs test/server/*.test.mjs"
27
26
  },
28
27
  "engines": {
29
28
  "node": ">=22.5.0"
@@ -1,242 +0,0 @@
1
- import { spawn as defaultSpawn } from 'node:child_process';
2
- import { parseEventLine } from '../events/schema.js';
3
- import { mergeProjectEnv } from '../runtime/env.js';
4
- function pushOption(argv, name, value) {
5
- if (value !== undefined && value !== '') {
6
- argv.push(name, String(value));
7
- }
8
- }
9
- function runArgs(options) {
10
- const argv = ['run', '--project-root', options.projectRoot, '--output', options.output ?? 'jsonl'];
11
- if (options.resumeLatest)
12
- argv.push('--resume-latest');
13
- if (options.skipDeploy)
14
- argv.push('--skip-deploy');
15
- if (options.skipVerify)
16
- argv.push('--skip-verify');
17
- pushOption(argv, '--event-log', options.eventLog);
18
- pushOption(argv, '--human-log', options.humanLog);
19
- return argv;
20
- }
21
- function retryArgs(options) {
22
- const argv = [
23
- 'retry-stage',
24
- '--project-root',
25
- options.projectRoot,
26
- '--artifact-dir',
27
- options.artifactDir,
28
- '--stage',
29
- options.stage,
30
- '--output',
31
- options.output ?? 'jsonl'
32
- ];
33
- pushOption(argv, '--event-log', options.eventLog);
34
- pushOption(argv, '--human-log', options.humanLog);
35
- return argv;
36
- }
37
- function resumeArgs(options) {
38
- const argv = [
39
- 'resume',
40
- options.mode,
41
- '--project-root',
42
- options.projectRoot,
43
- '--session',
44
- options.session,
45
- '--output',
46
- options.output ?? 'jsonl'
47
- ];
48
- pushOption(argv, '--event-log', options.eventLog);
49
- pushOption(argv, '--human-log', options.humanLog);
50
- return argv;
51
- }
52
- async function* splitLines(stream) {
53
- const queue = [];
54
- let ended = false;
55
- let pendingResolve;
56
- let buffer = '';
57
- stream.on('data', (chunk) => {
58
- buffer += String(chunk);
59
- const parts = buffer.split(/\r?\n/);
60
- buffer = parts.pop() ?? '';
61
- queue.push(...parts.filter((line) => line.trim()));
62
- pendingResolve?.();
63
- pendingResolve = undefined;
64
- });
65
- stream.on('end', () => {
66
- if (buffer.trim())
67
- queue.push(buffer);
68
- ended = true;
69
- pendingResolve?.();
70
- pendingResolve = undefined;
71
- });
72
- while (!ended || queue.length) {
73
- if (!queue.length) {
74
- await new Promise((resolve) => {
75
- pendingResolve = resolve;
76
- });
77
- continue;
78
- }
79
- yield queue.shift();
80
- }
81
- }
82
- function waitForClose(child) {
83
- return new Promise((resolve, reject) => {
84
- child.once('error', reject);
85
- child.once('close', (code, signal) => {
86
- resolve({ code, signal });
87
- });
88
- });
89
- }
90
- function errorSummary(stderr) {
91
- const summary = stderr.trim().split(/\r?\n/).filter(Boolean).slice(-3).join('\n');
92
- return summary ? `: ${summary}` : '';
93
- }
94
- function parseJsonPayload(stdout) {
95
- const line = stdout.split(/\r?\n/).find((candidate) => candidate.trim());
96
- if (!line) {
97
- throw new Error('Python backend returned empty JSON payload');
98
- }
99
- const payload = JSON.parse(line);
100
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
101
- throw new Error('Python backend JSON payload must be an object');
102
- }
103
- return payload;
104
- }
105
- function projectRootFromArgv(argv) {
106
- const index = argv.indexOf('--project-root');
107
- if (index >= 0 && argv[index + 1]) {
108
- return argv[index + 1];
109
- }
110
- const inline = argv.find((item) => item.startsWith('--project-root='));
111
- if (inline) {
112
- return inline.slice('--project-root='.length) || undefined;
113
- }
114
- return undefined;
115
- }
116
- export class PythonBackend {
117
- kind = 'python';
118
- env;
119
- cwd;
120
- runForwarder;
121
- resolvePythonCli;
122
- spawn;
123
- constructor(options = {}) {
124
- this.env = options.env ?? process.env;
125
- this.cwd = options.cwd ?? process.cwd();
126
- this.runForwarder = options.runForwarder;
127
- this.resolvePythonCli = options.resolvePythonCli;
128
- this.spawn = options.spawn ?? defaultSpawn;
129
- }
130
- async executeCli(argv) {
131
- const env = this.envForArgv(argv);
132
- const forwarderOptions = { env, cwd: this.cwd };
133
- if (this.runForwarder) {
134
- return this.runForwarder(argv, forwarderOptions);
135
- }
136
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
137
- const runner = await import('../../lib/runner.mjs');
138
- return Number(await runner.runForwarder(argv, forwarderOptions));
139
- }
140
- run(options) {
141
- return this.streamEvents(runArgs(options));
142
- }
143
- retryStage(options) {
144
- return this.streamEvents(retryArgs(options));
145
- }
146
- resume(options) {
147
- return this.streamEvents(resumeArgs(options));
148
- }
149
- async startDetached(options) {
150
- const argv = runArgs(options);
151
- argv.push('--detach');
152
- argv.push('--json');
153
- return this.captureJson(argv);
154
- }
155
- async stopJob(jobId, options = {}) {
156
- const argv = jobId === 'latest'
157
- ? ['stop']
158
- : ['jobs', 'stop', jobId];
159
- pushOption(argv, '--project-root', options.projectRoot);
160
- pushOption(argv, '--timeout', options.timeout);
161
- return this.captureJson(argv);
162
- }
163
- async readJob(jobId, options = {}) {
164
- const argv = jobId === 'latest'
165
- ? ['status', '--json']
166
- : ['jobs', 'status', jobId, '--json'];
167
- pushOption(argv, '--project-root', options.projectRoot);
168
- return this.captureJson(argv);
169
- }
170
- async *readLogs(options) {
171
- const argv = options.jobId
172
- ? ['jobs', 'logs', options.jobId, '--project-root', options.projectRoot]
173
- : ['logs', '--project-root', options.projectRoot];
174
- if (options.format)
175
- argv.push('--format', options.format);
176
- pushOption(argv, '--tail', options.tail);
177
- if (options.follow)
178
- argv.push('--follow');
179
- yield* this.streamLines(argv);
180
- }
181
- async *streamEvents(argv) {
182
- for await (const line of this.streamLines(argv)) {
183
- yield parseEventLine(line);
184
- }
185
- }
186
- async *streamLines(argv) {
187
- const env = this.envForArgv(argv);
188
- const resolved = this.resolvePythonCli ? this.resolvePythonCli() : await this.defaultResolvePythonCli(env);
189
- const child = this.spawn(resolved.command, [...resolved.args, ...argv], {
190
- cwd: this.cwd,
191
- env,
192
- stdio: ['ignore', 'pipe', 'pipe']
193
- });
194
- let stderr = '';
195
- child.stderr?.on('data', (chunk) => {
196
- stderr += String(chunk);
197
- });
198
- const closePromise = waitForClose(child);
199
- const stdout = child.stdout;
200
- if (!stdout) {
201
- throw new Error('Python backend did not expose stdout for event streaming');
202
- }
203
- for await (const line of splitLines(stdout)) {
204
- yield line;
205
- }
206
- const { code, signal } = await closePromise;
207
- if (code !== 0) {
208
- throw new Error(`Python backend exited with ${signal ? `signal ${signal}` : `code ${code}`}${errorSummary(stderr)}`);
209
- }
210
- }
211
- async captureJson(argv) {
212
- const env = this.envForArgv(argv);
213
- const resolved = this.resolvePythonCli ? this.resolvePythonCli() : await this.defaultResolvePythonCli(env);
214
- const child = this.spawn(resolved.command, [...resolved.args, ...argv], {
215
- cwd: this.cwd,
216
- env,
217
- stdio: ['ignore', 'pipe', 'pipe']
218
- });
219
- let stdout = '';
220
- let stderr = '';
221
- child.stdout?.on('data', (chunk) => {
222
- stdout += String(chunk);
223
- });
224
- child.stderr?.on('data', (chunk) => {
225
- stderr += String(chunk);
226
- });
227
- const { code, signal } = await waitForClose(child);
228
- if (code !== 0) {
229
- throw new Error(`Python backend exited with ${signal ? `signal ${signal}` : `code ${code}`}${errorSummary(stderr)}`);
230
- }
231
- return parseJsonPayload(stdout);
232
- }
233
- envForArgv(argv) {
234
- const projectRoot = projectRootFromArgv(argv);
235
- return projectRoot ? mergeProjectEnv(projectRoot, this.env) : this.env;
236
- }
237
- async defaultResolvePythonCli(env = this.env) {
238
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
239
- const runner = await import('../../lib/runner.mjs');
240
- return runner.resolveOrInstallPythonCli({ env });
241
- }
242
- }
package/lib/cache.mjs DELETED
@@ -1,14 +0,0 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
-
4
- export function getCacheDir(env = process.env) {
5
- const override = String(env.AUTOVPN_CACHE_DIR ?? '').trim();
6
- if (override) {
7
- return override;
8
- }
9
- return path.join(os.homedir(), '.cache', 'autovpn', 'npm-wrapper');
10
- }
11
-
12
- export function pythonBinName(platform = process.platform) {
13
- return platform === 'win32' ? 'Scripts' : 'bin';
14
- }
package/lib/errors.mjs DELETED
@@ -1,11 +0,0 @@
1
- export class WrapperError extends Error {
2
- constructor(message, options = {}) {
3
- super(message);
4
- this.name = 'WrapperError';
5
- this.cause = options.cause;
6
- }
7
- }
8
-
9
- export function isEnabled(value) {
10
- return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
11
- }
@@ -1,63 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { spawnSync as defaultSpawnSync } from 'node:child_process';
4
-
5
- import { getCacheDir, pythonBinName } from './cache.mjs';
6
- import { isEnabled, WrapperError } from './errors.mjs';
7
-
8
- export function buildPipInstallArgs({ env = process.env, packageVersion }) {
9
- const packageSpec = String(env.AUTOVPN_WHEEL_URL ?? '').trim()
10
- || `${String(env.AUTOVPN_PYTHON_PACKAGE ?? 'vpn-subscription-automation').trim()}==${packageVersion}`;
11
- const args = ['-m', 'pip', 'install'];
12
- if (env.AUTOVPN_PIP_INDEX_URL) {
13
- args.push('--index-url', String(env.AUTOVPN_PIP_INDEX_URL));
14
- }
15
- if (env.AUTOVPN_PIP_EXTRA_INDEX_URL) {
16
- args.push('--extra-index-url', String(env.AUTOVPN_PIP_EXTRA_INDEX_URL));
17
- }
18
- args.push(packageSpec);
19
- return args;
20
- }
21
-
22
- export function installPythonCli({
23
- env = process.env,
24
- packageVersion,
25
- spawnSync = defaultSpawnSync,
26
- cacheDir = getCacheDir(env),
27
- platform = process.platform
28
- } = {}) {
29
- if (isEnabled(env.AUTOVPN_NO_INSTALL)) {
30
- throw new WrapperError('No compatible Python autovpn CLI found and AUTOVPN_NO_INSTALL is enabled.');
31
- }
32
- if (!packageVersion) {
33
- throw new WrapperError('Cannot install Python AutoVPN backend without an npm package version.');
34
- }
35
-
36
- const venvDir = path.join(cacheDir, `python-${packageVersion}`);
37
- const executable = platform === 'win32' ? 'autovpn.exe' : 'autovpn';
38
- const cliPath = path.join(venvDir, pythonBinName(platform), executable);
39
- const forceInstall = isEnabled(env.AUTOVPN_FORCE_INSTALL);
40
-
41
- if (!forceInstall && fs.existsSync(cliPath)) {
42
- return { command: cliPath, args: [], source: 'managed-venv' };
43
- }
44
-
45
- fs.mkdirSync(cacheDir, { recursive: true });
46
- const pythonCommand = String(env.PYTHON ?? env.PYTHON3 ?? 'python3');
47
- const venvResult = spawnSync(pythonCommand, ['-m', 'venv', venvDir], { stdio: 'inherit', env });
48
- if (venvResult.status !== 0) {
49
- throw new WrapperError(`Failed to create AutoVPN Python backend venv with ${pythonCommand}.`);
50
- }
51
-
52
- const pythonPath = path.join(venvDir, pythonBinName(platform), platform === 'win32' ? 'python.exe' : 'python');
53
- const installResult = spawnSync(pythonPath, buildPipInstallArgs({ env, packageVersion }), { stdio: 'inherit', env });
54
- if (installResult.status !== 0) {
55
- throw new WrapperError('Failed to install AutoVPN Python backend into the wrapper-managed venv.');
56
- }
57
-
58
- if (!fs.existsSync(cliPath)) {
59
- throw new WrapperError(`Installed Python backend did not provide autovpn at ${cliPath}.`);
60
- }
61
-
62
- return { command: cliPath, args: [], source: 'managed-venv' };
63
- }