@swimmingliu/autovpn 1.6.5 → 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.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # AutoVPN CLI
2
2
 
3
- Node-first npm CLI for AutoVPN headless and Agent workflows.
3
+ Node.js npm CLI for AutoVPN headless and Agent workflows. Node.js `>=22.5.0`
4
+ is required.
4
5
 
5
6
  ## Usage
6
7
 
@@ -53,78 +54,30 @@ terminator for internet-facing deployments.
53
54
 
54
55
  ## Runtime Shape
55
56
 
56
- The CLI is currently Node-first with explicit Python rollback:
57
+ The CLI owns command parsing, profiles, artifacts, detached jobs, pipeline
58
+ execution, Cloudflare deployment, verification, and the server Web UI.
57
59
 
58
- - Node.js handles `--help`, `--version`, argument validation, `doctor --output json`, `profile summary --json`, `artifacts latest/list/preview`, `status --json`, `logs`, read-only `jobs` commands, and detached job management.
59
- - Node is the default backend for pipeline actions. Detached run/resume/retry workers spawn the Node CLI worker.
60
- - Plain Cloudflare Pages deploy, primary blocked-project fallback, share-project `SUB` sync, share-project fallback, custom-domain binding, custom-domain DNS upsert, and verify are Node-native. Python backend and stage fallback remain available for rollback.
61
-
62
- Node-orchestrated foreground run:
60
+ Foreground run:
63
61
 
64
62
  ```bash
65
63
  autovpn run --project-root . --output jsonl
66
64
  ```
67
65
 
68
- Current Node backend notes:
66
+ Runtime notes:
69
67
 
70
68
  - Detached job management runs in Node for `run --detach`, `jobs resume --detach`, and `jobs retry --detach`; detached run/resume/retry workers also use the Node CLI worker.
71
69
  - Non-detached `retry-stage` runs through the Node backend for retryable artifact stages from `speedtest` through `verify`; non-detached `resume pipeline`, `resume speedtest`, and `run --resume-latest` continue existing sessions through the Node backend.
72
70
  - Add `--skip-deploy --skip-verify` when you want an offline Node pipeline check.
73
71
  - Plain Node foreground deploy/verify runs use Node for Wrangler deploy, primary blocked-project fallback, share-project sync/fallback, custom-domain binding, custom-domain DNS upsert, and verify.
74
- - Deploy and verify can be rolled back with `AUTOVPN_STAGE_BACKEND_DEPLOY=python` and `AUTOVPN_STAGE_BACKEND_VERIFY=python`.
75
- - `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node. Speedtest and availability use the per-node Mihomo runtime by default so candidate measurements and provider checks traverse each candidate node. Set `AUTOVPN_SPEEDTEST_RUNTIME=direct` or `AUTOVPN_AVAILABILITY_RUNTIME=direct` only for direct-host diagnostic checks.
72
+ - Empty offline runs complete without requiring an external language runtime. Speedtest and availability use the per-node Mihomo runtime by default so candidate measurements and provider checks traverse each candidate node. Set `AUTOVPN_SPEEDTEST_RUNTIME=direct` or `AUTOVPN_AVAILABILITY_RUNTIME=direct` only for direct-host diagnostic checks.
76
73
  - Project `.env` is loaded before resolving profile and artifact paths. Explicit process environment values still win over `.env`.
77
74
  - `autovpn serve` is Node-native and exposes the browser UI plus `/api/health`,
78
75
  `/api/state`, `/api/runs`, `/api/runs/current/stop`, and `/api/events`.
79
76
 
80
- Fallback flags for migrated commands:
81
-
82
- ```bash
83
- AUTOVPN_CLI_SHELL=python autovpn <args>
84
- AUTOVPN_BACKEND=python autovpn run --project-root . --output jsonl
85
- AUTOVPN_DOCTOR_BACKEND=python autovpn doctor --output json
86
- AUTOVPN_PROFILE_BACKEND=python autovpn profile summary --json
87
- AUTOVPN_ARTIFACTS_BACKEND=python autovpn artifacts latest
88
- ```
89
-
90
- Job state, logs, stop, detached run, detached resume, and detached retry commands are Node-owned in v3 and intentionally ignore the old `AUTOVPN_JOBS_BACKEND` rollback flag.
91
-
92
- ## Python Backend Resolution
93
-
94
- When a command still needs Python, the wrapper resolves the backend in this order:
95
-
96
- 1. `AUTOVPN_PYTHON_CLI`
97
- 2. PATH `autovpn`, accepted only when `autovpn --version` matches this npm package version
98
- 3. wrapper-managed Python virtual environment under `AUTOVPN_CACHE_DIR` or the user cache
99
-
100
- For Python-backed commands, the wrapper forwards argv, stdin, stdout, stderr, and exit code.
101
-
102
77
  ## Environment
103
78
 
104
- - `AUTOVPN_CACHE_DIR`
105
- - `AUTOVPN_WHEEL_URL`
106
- - `AUTOVPN_PYTHON_PACKAGE`
107
- - `AUTOVPN_PIP_INDEX_URL`
108
- - `AUTOVPN_PIP_EXTRA_INDEX_URL`
109
79
  - `AUTOVPN_NO_INSTALL`
110
80
  - `AUTOVPN_FORCE_INSTALL`
111
- - `AUTOVPN_ALLOW_VERSION_MISMATCH`
112
- - `AUTOVPN_PYTHON_CLI`
113
- - `AUTOVPN_CLI_SHELL`
114
- - `AUTOVPN_BACKEND`
115
- - `AUTOVPN_PIPELINE_BACKEND`
116
- - `AUTOVPN_STAGE_BACKEND_EXTRACT`
117
- - `AUTOVPN_STAGE_BACKEND_DEDUPE`
118
- - `AUTOVPN_STAGE_BACKEND_SPEEDTEST`
119
- - `AUTOVPN_STAGE_BACKEND_AVAILABILITY`
120
- - `AUTOVPN_STAGE_BACKEND_POSTPROCESS`
121
- - `AUTOVPN_STAGE_BACKEND_RENDER`
122
- - `AUTOVPN_STAGE_BACKEND_OBFUSCATE`
123
- - `AUTOVPN_STAGE_BACKEND_DEPLOY`
124
- - `AUTOVPN_STAGE_BACKEND_VERIFY`
125
- - `AUTOVPN_DOCTOR_BACKEND`
126
- - `AUTOVPN_PROFILE_BACKEND`
127
- - `AUTOVPN_ARTIFACTS_BACKEND`
128
81
  - `AUTOVPN_SERVER_HOST`
129
82
  - `AUTOVPN_SERVER_PORT`
130
83
  - `AUTOVPN_SERVER_TOKEN`
@@ -1,9 +1,6 @@
1
1
  import { NodeBackend } from './node-backend.js';
2
2
  export function selectBackend(options = {}) {
3
3
  const backend = String(options.env?.AUTOVPN_BACKEND ?? '').trim().toLowerCase();
4
- if (backend === 'python') {
5
- throw new Error('AUTOVPN_BACKEND=python is no longer supported; AutoVPN now runs on the NodeJS engine');
6
- }
7
4
  if (backend && backend !== 'node') {
8
5
  throw new Error(`Unsupported AUTOVPN_BACKEND: ${backend}`);
9
6
  }
@@ -79,6 +79,10 @@ export function validateCommand(argv) {
79
79
  if (!TOP_LEVEL_COMMANDS.has(command)) {
80
80
  throw new CliUsageError(`unknown command: ${command}`);
81
81
  }
82
+ const internalJobToken = readOptionValue(argv, '--internal-job-token');
83
+ if (internalJobToken !== undefined && !/^[a-f0-9]{64}$/i.test(internalJobToken)) {
84
+ throw new CliUsageError('invalid internal job token');
85
+ }
82
86
  if (command === 'doctor') {
83
87
  validateChoice('doctor', '--output', readOptionValue(argv, '--output'), ['human', 'json']);
84
88
  return;
package/dist/cli/main.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  import { validateCommand } from './commands/index.js';
4
5
  import { CliUsageError } from './errors.js';
5
6
  import { normalizeProjectRootArgs } from './global-options.js';
@@ -13,18 +14,13 @@ import { createAutoVpnServer } from '../server/http.js';
13
14
  import { parseServeOptions } from '../server/options.js';
14
15
  import { createServerRuntime } from '../server/runtime.js';
15
16
  async function defaultReadPackageVersion() {
16
- // @ts-expect-error The Phase 1 runner is plain ESM JavaScript.
17
- const runner = await import('../../lib/runner.mjs');
18
- return String(runner.readPackageVersion());
17
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
18
+ const manifest = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
19
+ return String(manifest.version ?? '');
19
20
  }
20
21
  function defaultCreateBackend(options) {
21
22
  return selectBackend(options);
22
23
  }
23
- async function defaultRunForwarder(argv, options) {
24
- // @ts-expect-error The Phase 1 runner is plain ESM JavaScript.
25
- const runner = await import('../../lib/runner.mjs');
26
- return Number(await runner.runForwarder(argv, options));
27
- }
28
24
  async function defaultReadStdin() {
29
25
  const chunks = [];
30
26
  for await (const chunk of process.stdin) {
@@ -32,13 +28,6 @@ async function defaultReadStdin() {
32
28
  }
33
29
  return Buffer.concat(chunks).toString('utf8');
34
30
  }
35
- function isEnabled(value) {
36
- return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
37
- }
38
- function wantsDeprecatedPythonShell(env) {
39
- return String(env.AUTOVPN_CLI_SHELL ?? '').trim().toLowerCase() === 'python'
40
- || String(env.AUTOVPN_BACKEND ?? '').trim().toLowerCase() === 'python';
41
- }
42
31
  async function resolvePackageVersion(options) {
43
32
  if (options.packageVersion) {
44
33
  return options.packageVersion;
@@ -199,15 +188,7 @@ async function runForegroundPipeline(argv, backend, io, cwd) {
199
188
  export async function runCliShell(argv, options = {}) {
200
189
  const env = options.env ?? process.env;
201
190
  const io = options.io ?? defaultIo();
202
- const runForwarder = options.runForwarder ?? defaultRunForwarder;
203
191
  const cwd = options.cwd ?? process.cwd();
204
- if (wantsDeprecatedPythonShell(env)) {
205
- io.writeStderr('autovpn: Python backend is no longer supported; AutoVPN now runs on the NodeJS engine\n');
206
- return 2;
207
- }
208
- if (isEnabled(env.AUTOVPN_WRAPPER_PROBE) && argv.length === 1 && argv[0] === '--version') {
209
- return 42;
210
- }
211
192
  if (argv[0] === '--help' || argv[0] === '-h') {
212
193
  io.writeStdout(renderHelp());
213
194
  return 0;
@@ -220,7 +201,7 @@ export async function runCliShell(argv, options = {}) {
220
201
  const normalizedArgv = normalizeProjectRootArgs(argv, cwd);
221
202
  validateCommand(normalizedArgv);
222
203
  const createBackend = options.createBackend ?? defaultCreateBackend;
223
- const backend = createBackend({ env, cwd, runForwarder });
204
+ const backend = createBackend({ env, cwd });
224
205
  if (normalizedArgv[0] === 'serve') {
225
206
  const serveOptions = parseServeOptions(normalizedArgv, { cwd, env });
226
207
  const serverFactory = options.createServer ?? createAutoVpnServer;
@@ -256,7 +237,7 @@ export async function runCliShell(argv, options = {}) {
256
237
  return 0;
257
238
  }
258
239
  if (isPipelineProxyCommand(normalizedArgv)) {
259
- throw new Error('--proxy is now handled by serve and Node runtime proxy settings; Python proxy mode is no longer supported');
240
+ throw new Error('--proxy is handled by serve and Node runtime proxy settings');
260
241
  }
261
242
  const nativeResult = await runNativeCommand(normalizedArgv, {
262
243
  cwd,
@@ -266,6 +247,7 @@ export async function runCliShell(argv, options = {}) {
266
247
  spawn: options.spawn,
267
248
  now: options.now,
268
249
  jobId: options.jobId,
250
+ jobToken: options.jobToken,
269
251
  sleep: options.sleep
270
252
  });
271
253
  if (nativeResult !== undefined) {
@@ -47,6 +47,7 @@ function jobOptions(context) {
47
47
  spawn: context.spawn,
48
48
  now: context.now,
49
49
  jobId: context.jobId,
50
+ jobToken: context.jobToken,
50
51
  sleep: context.sleep
51
52
  };
52
53
  }
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { parse } from '@iarna/toml';
6
6
  import { profileSummary } from '../config/profile.js';
7
7
  import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
8
+ import { resolveShareWorkerTemplatePath, resolveWorkerTemplatePath } from '../runtime/templates.js';
8
9
  import { normalizeManagedToolCommandForSpawn, resolveManagedNpmTool } from '../runtime/managed-tools.js';
9
10
  const JAVASCRIPT_OBFUSCATOR_VERSION = '5.4.3';
10
11
  const WRANGLER_VERSION = '4.106.0';
@@ -201,13 +202,27 @@ export async function runDoctor(projectRoot, argv, env = process.env, options =
201
202
  const configuredSources = sourceValues.filter((source) => source.enabled && source.url === 'set' && source.key === 'set');
202
203
  const nodeToolChecks = await checkNodeTools(projectRoot, env, options);
203
204
  const cloudflareChecks = await checkCloudflare(summary, deploy, env, projectRoot, options);
205
+ let workerTemplatePath = '';
206
+ let shareWorkerTemplatePath = '';
207
+ try {
208
+ workerTemplatePath = resolveWorkerTemplatePath(projectRoot);
209
+ }
210
+ catch {
211
+ workerTemplatePath = '';
212
+ }
213
+ try {
214
+ shareWorkerTemplatePath = resolveShareWorkerTemplatePath(projectRoot, env);
215
+ }
216
+ catch {
217
+ shareWorkerTemplatePath = '';
218
+ }
204
219
  const checks = [
205
220
  check('node_version', 'pass', `Node ${process.versions.node}`, { required: '>=20' }),
206
221
  check('project_root', 'pass', 'Project root resolved', { path: projectRoot }),
207
222
  check('profile_path', pathWritable(profilePath) ? 'pass' : 'fail', pathWritable(profilePath) ? 'Profile is readable and writable' : 'Profile path is not writable', { profile_path: profilePath, exists: fs.existsSync(profilePath) }),
208
223
  check('artifacts_root', pathWritable(artifactsRoot) ? 'pass' : 'fail', pathWritable(artifactsRoot) ? 'Artifacts root is writable' : 'Artifacts root is not writable', { path: artifactsRoot }),
209
- check('worker_template', fs.existsSync(path.join(projectRoot, 'templates', 'vmess_node.js')) ? 'pass' : 'fail', fs.existsSync(path.join(projectRoot, 'templates', 'vmess_node.js')) ? 'Worker template exists' : 'Worker template is missing', { path: path.join(projectRoot, 'templates', 'vmess_node.js') }),
210
- check('share_worker_template', fs.existsSync(path.join(projectRoot, 'templates', 'share-worker', 'vpn.js')) ? 'pass' : 'warn', fs.existsSync(path.join(projectRoot, 'templates', 'share-worker', 'vpn.js')) ? 'Share worker template exists' : 'Share worker template is missing', { path: path.join(projectRoot, 'templates', 'share-worker', 'vpn.js') }),
224
+ check('worker_template', workerTemplatePath ? 'pass' : 'fail', workerTemplatePath ? 'Worker template exists' : 'Worker template is missing', { path: workerTemplatePath || path.join(projectRoot, 'templates', 'vmess_node.js') }),
225
+ check('share_worker_template', shareWorkerTemplatePath ? 'pass' : 'warn', shareWorkerTemplatePath ? 'Share worker template exists' : 'Share worker template is missing', { path: shareWorkerTemplatePath || path.join(projectRoot, 'templates', 'share-worker', 'vpn.js') }),
211
226
  configuredSources.length
212
227
  ? check('sources', 'pass', 'At least one enabled source is configured', { configured_count: configuredSources.length })
213
228
  : check('sources', 'warn', 'No enabled source has both URL and key configured', { configured_count: 0, key_state: 'missing' }),
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { spawn as defaultSpawn } from 'node:child_process';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { publicJobPayload } from './read.js';
@@ -16,6 +17,13 @@ function pushFlag(argv, enabled, flag) {
16
17
  if (enabled)
17
18
  argv.push(flag);
18
19
  }
20
+ function createJobToken(options) {
21
+ const token = options.jobToken?.() ?? randomBytes(32).toString('hex');
22
+ if (!/^[a-f0-9]{64}$/i.test(token)) {
23
+ throw new Error('internal job token must be 64 hexadecimal characters');
24
+ }
25
+ return token.toLowerCase();
26
+ }
19
27
  function spawnDetached(command, args, job, options) {
20
28
  const stdoutFd = fs.openSync(String(job.stdout_log), 'a');
21
29
  const stderrFd = fs.openSync(String(job.stderr_log), 'a');
@@ -79,6 +87,7 @@ export async function startDetachedRun(command, options = {}) {
79
87
  output_format: outputFormat
80
88
  }
81
89
  });
90
+ runArgs.push('--internal-job-token', createJobToken(options));
82
91
  runArgs.push('--event-log', String(job.event_log), '--human-log', String(job.human_log));
83
92
  pushFlag(runArgs, command.resumeLatest, '--resume-latest');
84
93
  pushFlag(runArgs, command.skipDeploy, '--skip-deploy');
@@ -115,6 +124,8 @@ export async function startDetachedResume(command, options = {}) {
115
124
  command.sessionDir,
116
125
  '--output',
117
126
  outputFormat,
127
+ '--internal-job-token',
128
+ createJobToken(options),
118
129
  '--event-log',
119
130
  String(job.event_log),
120
131
  '--human-log',
@@ -148,6 +159,8 @@ export async function startDetachedRetry(command, options = {}) {
148
159
  command.stage,
149
160
  '--output',
150
161
  outputFormat,
162
+ '--internal-job-token',
163
+ createJobToken(options),
151
164
  '--event-log',
152
165
  String(job.event_log),
153
166
  '--human-log',
@@ -1,22 +1,104 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawnSync as defaultSpawnSync } from 'node:child_process';
4
- export function processMatchesJob(pid, command) {
5
- const cmdlinePath = path.join('/proc', String(pid), 'cmdline');
6
- if (!fs.existsSync(cmdlinePath)) {
7
- return true;
4
+ function resolvedArg(value) {
5
+ if (!path.isAbsolute(value)) {
6
+ return value;
8
7
  }
9
8
  try {
10
- const cmdline = fs.readFileSync(cmdlinePath).toString('utf8').replaceAll('\0', ' ');
11
- const markers = ['vpn_automation.backend'];
12
- if (command.length > 0) {
13
- markers.push(path.basename(String(command[0])));
14
- }
15
- return markers.some((marker) => marker.length > 0 && cmdline.includes(marker));
9
+ return fs.realpathSync(value);
16
10
  }
17
11
  catch {
12
+ return path.resolve(value);
13
+ }
14
+ }
15
+ function jobIdentity(command) {
16
+ const expected = command.map(String);
17
+ const tokenIndex = expected.indexOf('--internal-job-token');
18
+ const projectRootIndex = expected.indexOf('--project-root');
19
+ const token = expected[tokenIndex + 1] ?? '';
20
+ if (expected.length < 7
21
+ || path.basename(expected[1] ?? '') !== 'autovpn.mjs'
22
+ || projectRootIndex < 3
23
+ || !expected[projectRootIndex + 1]
24
+ || tokenIndex < 3
25
+ || !/^[a-f0-9]{64}$/i.test(token)) {
26
+ return undefined;
27
+ }
28
+ return {
29
+ executable: resolvedArg(expected[0]),
30
+ entry: resolvedArg(expected[1]),
31
+ projectRoot: resolvedArg(expected[projectRootIndex + 1]),
32
+ token: token.toLowerCase()
33
+ };
34
+ }
35
+ function escaped(value) {
36
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ }
38
+ function commandTextMatchesJob(commandLine, command) {
39
+ const identity = jobIdentity(command);
40
+ if (!identity || !commandLine.trim()) {
41
+ return false;
42
+ }
43
+ const tokenPattern = new RegExp(`(?:^|[\\s"'])${escaped(identity.token)}(?:$|[\\s"'])`, 'i');
44
+ const normalized = commandLine.replaceAll('\\', '/');
45
+ const projectRootIndex = command.indexOf('--project-root');
46
+ const containsPath = (value) => [value, resolvedArg(value)]
47
+ .map((candidate) => candidate.replaceAll('\\', '/'))
48
+ .some((candidate) => normalized.includes(candidate));
49
+ return tokenPattern.test(commandLine)
50
+ && containsPath(command[1])
51
+ && containsPath(command[projectRootIndex + 1])
52
+ && containsPath(command[0]);
53
+ }
54
+ export function cmdlineMatchesJob(cmdline, command) {
55
+ const actual = cmdline.toString('utf8').split('\0').filter((value) => value.length > 0);
56
+ const expected = command.map(String);
57
+ const identity = jobIdentity(expected);
58
+ if (!identity || actual.length !== expected.length) {
59
+ return false;
60
+ }
61
+ if (path.basename(expected[1]) !== 'autovpn.mjs' || path.basename(actual[1]) !== 'autovpn.mjs') {
62
+ return false;
63
+ }
64
+ const projectRootIndex = expected.indexOf('--project-root');
65
+ return expected.every((value, index) => {
66
+ if (index <= 1 || index === projectRootIndex + 1) {
67
+ return resolvedArg(actual[index]) === resolvedArg(value);
68
+ }
69
+ return actual[index] === value;
70
+ });
71
+ }
72
+ export function processMatchesJob(pid, command, options = {}) {
73
+ const platform = options.platform ?? process.platform;
74
+ if (!jobIdentity(command)) {
75
+ return false;
76
+ }
77
+ const cmdlinePath = path.join('/proc', String(pid), 'cmdline');
78
+ if (platform === 'linux') {
79
+ if (!fs.existsSync(cmdlinePath)) {
80
+ return false;
81
+ }
82
+ try {
83
+ return cmdlineMatchesJob(fs.readFileSync(cmdlinePath), command);
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ const spawnSync = options.spawnSync ?? defaultSpawnSync;
90
+ const lookup = platform === 'win32'
91
+ ? spawnSync('powershell.exe', [
92
+ '-NoProfile',
93
+ '-NonInteractive',
94
+ '-Command',
95
+ `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`
96
+ ], { encoding: 'utf8', windowsHide: true })
97
+ : spawnSync('ps', ['-ww', '-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
98
+ if (lookup.status !== 0 || typeof lookup.stdout !== 'string') {
18
99
  return false;
19
100
  }
101
+ return commandTextMatchesJob(lookup.stdout, command);
20
102
  }
21
103
  function defaultIsAlive(pid) {
22
104
  if (pid <= 0)
@@ -1,9 +1,6 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
1
  import http from 'node:http';
4
2
  import net from 'node:net';
5
3
  import tls from 'node:tls';
6
- import { mergeProjectEnv } from '../runtime/env.js';
7
4
  import { openMihomoRuntime as defaultOpenMihomoRuntime } from './proxy-runtime.js';
8
5
  const PROVIDER_TARGETS = [
9
6
  { name: 'gemini', url: 'https://gemini.google.com', allowed_hosts: ['gemini.google.com'], negative_phrases: [] },
@@ -20,42 +17,6 @@ const CHALLENGE_PHRASES = [
20
17
  const GEMINI_REGION_MARKERS = [',2,1,200,"', ',2,1,200,\\"'];
21
18
  const CLAUDE_BLOCKED_CODES = new Set(['AF', 'BY', 'CN', 'CU', 'HK', 'IR', 'KP', 'MO', 'RU', 'SY']);
22
19
  const GEMINI_BLOCKED_CODES = new Set(['CHN', 'RUS', 'BLR', 'CUB', 'IRN', 'PRK', 'SYR', 'HKG', 'MAC']);
23
- const PYTHON_AVAILABILITY_HELPER = `
24
- import json
25
- import sys
26
- from vpn_automation.config.models import AvailabilityTargetConfig, SpeedTestConfig
27
- from vpn_automation.pipeline.availability import ProviderTarget, check_link_availability_batch
28
- from vpn_automation.pipeline.speedtest import SpeedTestResult
29
-
30
- payload = json.load(sys.stdin)
31
- config = SpeedTestConfig(**payload["config"])
32
- results = [SpeedTestResult(**item) for item in payload.get("results", [])]
33
- raw_targets = payload.get("targets", None)
34
- targets = None
35
- if isinstance(raw_targets, list):
36
- targets = tuple(
37
- ProviderTarget(
38
- name=str(item["name"]),
39
- url=str(item["url"]),
40
- allowed_hosts=tuple(item.get("allowed_hosts") or []),
41
- negative_phrases=tuple(item.get("negative_phrases") or []),
42
- )
43
- for item in raw_targets
44
- )
45
- elif isinstance(raw_targets, dict):
46
- targets = {name: AvailabilityTargetConfig(**value) for name, value in raw_targets.items()}
47
- output = [
48
- item.to_dict()
49
- for item in check_link_availability_batch(
50
- results,
51
- config,
52
- runtime_path=payload.get("runtime_path", ""),
53
- targets=targets,
54
- )
55
- ]
56
- json.dump(output, sys.stdout, ensure_ascii=False)
57
- sys.stdout.write("\\n")
58
- `;
59
20
  function providerResultWithDefaults(result) {
60
21
  return {
61
22
  provider: result.provider,
@@ -735,63 +696,6 @@ async function checkBatchInNode(input, options) {
735
696
  });
736
697
  });
737
698
  }
738
- export function selectPipelineStageBackend(stage, env = process.env) {
739
- void stage;
740
- void env;
741
- return 'node';
742
- }
743
- async function defaultResolvePythonCli(env) {
744
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
745
- const runner = await import('../../lib/runner.mjs');
746
- return runner.resolveOrInstallPythonCli({ env });
747
- }
748
- function pythonCommandFor(resolved) {
749
- const command = resolved.command;
750
- const name = path.basename(command).toLowerCase();
751
- if (['autovpn', 'autovpn.exe'].includes(name)) {
752
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
753
- return path.join(path.dirname(command), executable);
754
- }
755
- return process.platform === 'win32' ? 'python.exe' : 'python3';
756
- }
757
- async function availabilityWithPython(input, options) {
758
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
759
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
760
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_AVAILABILITY_HELPER], {
761
- cwd: options.cwd ?? process.cwd(),
762
- env,
763
- stdio: ['pipe', 'pipe', 'pipe']
764
- });
765
- let stdout = '';
766
- let stderr = '';
767
- child.stdout?.on('data', (chunk) => {
768
- stdout += String(chunk);
769
- });
770
- child.stderr?.on('data', (chunk) => {
771
- stderr += String(chunk);
772
- });
773
- const completion = new Promise((resolve, reject) => {
774
- child.on('error', reject);
775
- child.on('close', (code) => {
776
- if (code !== 0) {
777
- reject(new Error(`Python availability backend failed with exit code ${code}: ${stderr.trim()}`));
778
- return;
779
- }
780
- try {
781
- resolve(JSON.parse(stdout));
782
- }
783
- catch (error) {
784
- reject(new Error(`Python availability backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
785
- }
786
- });
787
- });
788
- child.stdin?.write(JSON.stringify(input));
789
- child.stdin?.end();
790
- return completion;
791
- }
792
699
  export async function checkLinkAvailabilityBatchWithBackend(input, options = {}) {
793
- if (selectPipelineStageBackend('availability', options.env ?? process.env) === 'python') {
794
- return options.pythonAvailability ? options.pythonAvailability(input) : availabilityWithPython(input, options);
795
- }
796
700
  return checkBatchInNode(input, options);
797
701
  }
@@ -1,15 +1,3 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
- import { mergeProjectEnv } from '../runtime/env.js';
4
- const PYTHON_DEDUPE_HELPER = `
5
- import json
6
- import sys
7
- from vpn_automation.pipeline.dedupe import dedupe_vmess_links
8
-
9
- payload = json.load(sys.stdin)
10
- json.dump(dedupe_vmess_links(payload["links"]), sys.stdout, ensure_ascii=False)
11
- sys.stdout.write("\\n")
12
- `;
13
1
  export function parseVmessLink(link) {
14
2
  const encoded = link.replace(/^vmess:\/\//, '');
15
3
  const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
@@ -40,63 +28,7 @@ export function dedupeVmessLinks(links) {
40
28
  }
41
29
  return result;
42
30
  }
43
- export function selectPipelineStageBackend(stage, env = process.env) {
44
- void stage;
45
- void env;
46
- return 'node';
47
- }
48
- async function defaultResolvePythonCli(env) {
49
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
50
- const runner = await import('../../lib/runner.mjs');
51
- return runner.resolveOrInstallPythonCli({ env });
52
- }
53
- function pythonCommandFor(resolved) {
54
- const command = resolved.command;
55
- const name = path.basename(command).toLowerCase();
56
- if (['autovpn', 'autovpn.exe'].includes(name)) {
57
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
58
- return path.join(path.dirname(command), executable);
59
- }
60
- return process.platform === 'win32' ? 'python.exe' : 'python3';
61
- }
62
- async function dedupeVmessLinksWithPython(links, options) {
63
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
64
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
65
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_DEDUPE_HELPER], {
66
- cwd: options.cwd ?? process.cwd(),
67
- env,
68
- stdio: ['pipe', 'pipe', 'pipe']
69
- });
70
- let stdout = '';
71
- let stderr = '';
72
- child.stdout?.on('data', (chunk) => {
73
- stdout += String(chunk);
74
- });
75
- child.stderr?.on('data', (chunk) => {
76
- stderr += String(chunk);
77
- });
78
- const completion = new Promise((resolve, reject) => {
79
- child.on('error', reject);
80
- child.on('close', (code) => {
81
- if (code !== 0) {
82
- reject(new Error(`Python dedupe backend failed with exit code ${code}: ${stderr.trim()}`));
83
- return;
84
- }
85
- try {
86
- resolve(JSON.parse(stdout));
87
- }
88
- catch (error) {
89
- reject(new Error(`Python dedupe backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
90
- }
91
- });
92
- });
93
- child.stdin?.write(JSON.stringify({ links }));
94
- child.stdin?.end();
95
- return completion;
96
- }
97
31
  export async function dedupeVmessLinksWithBackend(links, options = {}) {
98
- if (selectPipelineStageBackend('dedupe', options.env ?? process.env) === 'python') {
99
- return options.pythonDedupe ? options.pythonDedupe(links) : dedupeVmessLinksWithPython(links, options);
100
- }
32
+ void options;
101
33
  return dedupeVmessLinks(links);
102
34
  }