@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.
- package/README.md +7 -54
- package/dist/backend/select-backend.js +0 -3
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/main.js +7 -25
- package/dist/cli/native-commands.js +1 -0
- package/dist/jobs/commands.js +13 -0
- package/dist/jobs/process.js +92 -10
- package/dist/pipeline/availability.js +0 -96
- package/dist/pipeline/dedupe.js +1 -69
- package/dist/pipeline/deploy.js +138 -251
- package/dist/pipeline/extract.js +0 -80
- package/dist/pipeline/obfuscate.js +1 -80
- package/dist/pipeline/orchestrator.js +105 -127
- package/dist/pipeline/postprocess.js +1 -85
- package/dist/pipeline/proxy-runtime.js +5 -0
- package/dist/pipeline/render.js +1 -73
- package/dist/pipeline/speedtest.js +89 -100
- package/dist/runtime/paths.js +11 -2
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +3 -4
- package/dist/backend/python-backend.js +0 -242
- package/lib/cache.mjs +0 -14
- package/lib/errors.mjs +0 -11
- package/lib/install-python-cli.mjs +0 -63
- package/lib/runner.mjs +0 -113
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# AutoVPN CLI
|
|
2
2
|
|
|
3
|
-
Node
|
|
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
|
|
57
|
+
The CLI owns command parsing, profiles, artifacts, detached jobs, pipeline
|
|
58
|
+
execution, Cloudflare deployment, verification, and the server Web UI.
|
|
57
59
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
return String(
|
|
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
|
|
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
|
|
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) {
|
package/dist/jobs/commands.js
CHANGED
|
@@ -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',
|
package/dist/jobs/process.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return true;
|
|
4
|
+
function resolvedArg(value) {
|
|
5
|
+
if (!path.isAbsolute(value)) {
|
|
6
|
+
return value;
|
|
8
7
|
}
|
|
9
8
|
try {
|
|
10
|
-
|
|
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
|
}
|
package/dist/pipeline/dedupe.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
return options.pythonDedupe ? options.pythonDedupe(links) : dedupeVmessLinksWithPython(links, options);
|
|
100
|
-
}
|
|
32
|
+
void options;
|
|
101
33
|
return dedupeVmessLinks(links);
|
|
102
34
|
}
|