@swimmingliu/autovpn 1.4.1 → 1.4.2
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/dist/cli/native-commands.js +1 -1
- package/dist/doctor/checks.js +47 -23
- package/dist/pipeline/deploy.js +15 -7
- package/dist/runtime/managed-tools.js +269 -0
- package/package.json +3 -3
|
@@ -70,7 +70,7 @@ export async function runNativeCommand(argv, context) {
|
|
|
70
70
|
return context.pythonFallback(argv);
|
|
71
71
|
if (readOptionValue(argv, '--output') !== 'json')
|
|
72
72
|
return undefined;
|
|
73
|
-
const result = runDoctor(projectRoot, argv, context.env);
|
|
73
|
+
const result = await runDoctor(projectRoot, argv, context.env);
|
|
74
74
|
context.io.writeStdout(jsonLine(result.payload));
|
|
75
75
|
return result.code;
|
|
76
76
|
}
|
package/dist/doctor/checks.js
CHANGED
|
@@ -5,6 +5,9 @@ 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 { normalizeManagedToolCommandForSpawn, resolveManagedNpmTool } from '../runtime/managed-tools.js';
|
|
9
|
+
const JAVASCRIPT_OBFUSCATOR_VERSION = '5.4.3';
|
|
10
|
+
const WRANGLER_VERSION = '4.106.0';
|
|
8
11
|
function check(name, status, message, details = {}) {
|
|
9
12
|
return { name, status, message, details };
|
|
10
13
|
}
|
|
@@ -21,19 +24,27 @@ function pathWritable(targetPath) {
|
|
|
21
24
|
return false;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
|
-
function commandPath(name, env) {
|
|
27
|
+
function commandPath(name, env, platform = process.platform) {
|
|
25
28
|
const pathValue = String(env.PATH ?? process.env.PATH ?? '');
|
|
29
|
+
const extensions = platform === 'win32'
|
|
30
|
+
? String(env.PATHEXT ?? process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
31
|
+
.split(';')
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
: [''];
|
|
26
34
|
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
for (const extension of extensions) {
|
|
36
|
+
const candidate = path.join(dir, `${name}${extension}`);
|
|
37
|
+
if (fs.existsSync(candidate)) {
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
return '';
|
|
33
43
|
}
|
|
34
44
|
function safeRun(command, env) {
|
|
35
45
|
try {
|
|
36
|
-
const
|
|
46
|
+
const { executable, args } = normalizeManagedToolCommandForSpawn(command);
|
|
47
|
+
const result = spawnSync(executable, args, {
|
|
37
48
|
encoding: 'utf8',
|
|
38
49
|
env: { ...process.env, ...env },
|
|
39
50
|
timeout: 5000
|
|
@@ -110,8 +121,8 @@ function checkProxyRuntime(env) {
|
|
|
110
121
|
checks.push(check('proxy_environment', 'pass', 'Proxy environment inspected', { configured_keys: configuredKeys }));
|
|
111
122
|
return checks;
|
|
112
123
|
}
|
|
113
|
-
function checkNodeTools(projectRoot, env) {
|
|
114
|
-
const missing = ['node', 'npm'
|
|
124
|
+
async function checkNodeTools(projectRoot, env, options = {}) {
|
|
125
|
+
const missing = ['node', 'npm'].filter((name) => !commandPath(name, env, options.platform));
|
|
115
126
|
const checks = [
|
|
116
127
|
check('node_binaries', missing.length ? 'fail' : 'pass', missing.length ? 'Node.js command line tools are missing' : 'Node.js command line tools are available', { missing })
|
|
117
128
|
];
|
|
@@ -120,17 +131,22 @@ function checkNodeTools(projectRoot, env) {
|
|
|
120
131
|
path.join(projectRoot, 'electron', 'runtime', 'node-vendor', 'node_modules', 'playwright')
|
|
121
132
|
].some((candidate) => fs.existsSync(candidate));
|
|
122
133
|
checks.push(check('playwright', hasPlaywright ? 'pass' : 'warn', hasPlaywright ? 'Playwright package is installed' : 'Playwright package was not found; run npx playwright install --with-deps chromium-headless-shell'));
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
try {
|
|
135
|
+
const obfuscator = await (options.resolveManagedNpmTool ?? resolveManagedNpmTool)({
|
|
136
|
+
packageName: 'javascript-obfuscator',
|
|
137
|
+
binaryName: 'javascript-obfuscator',
|
|
138
|
+
version: JAVASCRIPT_OBFUSCATOR_VERSION,
|
|
139
|
+
projectRoot,
|
|
140
|
+
installMissing: false
|
|
141
|
+
});
|
|
142
|
+
checks.push(check('javascript_obfuscator', 'pass', 'javascript-obfuscator is available', { source: obfuscator.source, version: obfuscator.version, path: obfuscator.command }));
|
|
126
143
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
checks.push(check('javascript_obfuscator', result.ok ? 'pass' : 'fail', result.ok ? 'javascript-obfuscator is available' : 'javascript-obfuscator is not available', { result: result.message }));
|
|
144
|
+
catch (error) {
|
|
145
|
+
checks.push(check('javascript_obfuscator', 'fail', error instanceof Error ? error.message : String(error)));
|
|
130
146
|
}
|
|
131
147
|
return checks;
|
|
132
148
|
}
|
|
133
|
-
function checkCloudflare(summary, deploy, env) {
|
|
149
|
+
async function checkCloudflare(summary, deploy, env, projectRoot, options = {}) {
|
|
134
150
|
const checks = [];
|
|
135
151
|
const deploySummary = summary.deploy ?? {};
|
|
136
152
|
const hasCredentials = deploySummary.cloudflare_api_token === 'set'
|
|
@@ -143,17 +159,23 @@ function checkCloudflare(summary, deploy, env) {
|
|
|
143
159
|
const pagesProjectUrl = String(deploySummary.pages_project_url ?? '');
|
|
144
160
|
const hasDeployUrl = Boolean(String(deploySummary.project_name ?? '').trim() && URL.canParse(pagesProjectUrl));
|
|
145
161
|
checks.push(check('deploy_urls', hasDeployUrl ? 'pass' : (deploy ? 'fail' : 'warn'), hasDeployUrl ? 'Deploy URL settings are internally consistent' : 'Deploy URL settings are incomplete', { has_project_name: Boolean(String(deploySummary.project_name ?? '').trim()), has_pages_url: URL.canParse(pagesProjectUrl) }));
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
try {
|
|
163
|
+
const wrangler = await (options.resolveManagedNpmTool ?? resolveManagedNpmTool)({
|
|
164
|
+
packageName: 'wrangler',
|
|
165
|
+
binaryName: 'wrangler',
|
|
166
|
+
version: WRANGLER_VERSION,
|
|
167
|
+
projectRoot,
|
|
168
|
+
installMissing: false
|
|
169
|
+
});
|
|
170
|
+
const result = (options.safeRun ?? safeRun)([wrangler.command, 'pages', 'deploy', '--help'], env);
|
|
171
|
+
checks.push(check('wrangler', result.ok ? 'pass' : (deploy ? 'fail' : 'warn'), result.ok ? 'Wrangler Pages deploy command is available' : 'Wrangler Pages deploy command is not available', { source: wrangler.source, version: wrangler.version, path: wrangler.command, result: result.message, deploy_required: deploy }));
|
|
149
172
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
checks.push(check('wrangler', result.ok ? 'pass' : (deploy ? 'fail' : 'warn'), result.ok ? 'Wrangler Pages deploy command is available' : 'Wrangler Pages deploy command is not available', { result: result.message, deploy_required: deploy }));
|
|
173
|
+
catch (error) {
|
|
174
|
+
checks.push(check('wrangler', deploy ? 'fail' : 'warn', error instanceof Error ? error.message : String(error), { deploy_required: deploy }));
|
|
153
175
|
}
|
|
154
176
|
return checks;
|
|
155
177
|
}
|
|
156
|
-
export function runDoctor(projectRoot, argv, env = process.env) {
|
|
178
|
+
export async function runDoctor(projectRoot, argv, env = process.env, options = {}) {
|
|
157
179
|
const deploy = argv.includes('--deploy');
|
|
158
180
|
const strict = argv.includes('--strict');
|
|
159
181
|
const profilePath = resolveProfilePath(projectRoot, env);
|
|
@@ -162,6 +184,8 @@ export function runDoctor(projectRoot, argv, env = process.env) {
|
|
|
162
184
|
const summary = profileSummary(projectRoot, env);
|
|
163
185
|
const sourceValues = Object.values((summary.sources ?? {}));
|
|
164
186
|
const configuredSources = sourceValues.filter((source) => source.enabled && source.url === 'set' && source.key === 'set');
|
|
187
|
+
const nodeToolChecks = await checkNodeTools(projectRoot, env, options);
|
|
188
|
+
const cloudflareChecks = await checkCloudflare(summary, deploy, env, projectRoot, options);
|
|
165
189
|
const checks = [
|
|
166
190
|
check('node_version', 'pass', `Node ${process.versions.node}`, { required: '>=20' }),
|
|
167
191
|
check('project_root', 'pass', 'Project root resolved', { path: projectRoot }),
|
|
@@ -174,8 +198,8 @@ export function runDoctor(projectRoot, argv, env = process.env) {
|
|
|
174
198
|
: check('sources', 'warn', 'No enabled source has both URL and key configured', { configured_count: 0, key_state: 'missing' }),
|
|
175
199
|
checkSpeedTestConfig(profile),
|
|
176
200
|
...checkProxyRuntime(env),
|
|
177
|
-
...
|
|
178
|
-
...
|
|
201
|
+
...nodeToolChecks,
|
|
202
|
+
...cloudflareChecks
|
|
179
203
|
];
|
|
180
204
|
const hasFailures = checks.some((item) => item.status === 'fail');
|
|
181
205
|
const hasWarnings = checks.some((item) => item.status === 'warn');
|
package/dist/pipeline/deploy.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
3
3
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { mergeProjectEnv } from '../runtime/env.js';
|
|
5
|
+
import { normalizeManagedToolCommandForSpawn, resolveManagedNpmTool } from '../runtime/managed-tools.js';
|
|
5
6
|
const PYTHON_DEPLOY_HELPER = `
|
|
6
7
|
import json
|
|
7
8
|
import sys
|
|
@@ -77,8 +78,8 @@ function nonNegativeInt(value) {
|
|
|
77
78
|
function getString(source, key) {
|
|
78
79
|
return clean(source[key]);
|
|
79
80
|
}
|
|
80
|
-
export function buildPagesDeployCommand(bundleDir, projectName) {
|
|
81
|
-
return [
|
|
81
|
+
export function buildPagesDeployCommand(wranglerExecutable, bundleDir, projectName) {
|
|
82
|
+
return [wranglerExecutable, 'pages', 'deploy', bundleDir, '--project-name', projectName, '--branch', PAGES_PRODUCTION_BRANCH];
|
|
82
83
|
}
|
|
83
84
|
export function derivePagesProjectUrl(projectName) {
|
|
84
85
|
return `https://${projectName}.pages.dev`;
|
|
@@ -405,7 +406,7 @@ async function stageShareProjectWorkerBundle(projectRoot, env) {
|
|
|
405
406
|
return { sourcePath, bundleDir, workerEntry };
|
|
406
407
|
}
|
|
407
408
|
function runCommandWithSpawn(command, options, spawnImpl = defaultSpawn) {
|
|
408
|
-
const
|
|
409
|
+
const { executable, args } = normalizeManagedToolCommandForSpawn(command);
|
|
409
410
|
const child = spawnImpl(executable, args, {
|
|
410
411
|
cwd: options.cwd,
|
|
411
412
|
env: { ...process.env, ...options.env },
|
|
@@ -848,7 +849,7 @@ async function syncShareProjectSub(deploy, options) {
|
|
|
848
849
|
const shareCustomDomains = typeof options.client.listPagesDomains === 'function'
|
|
849
850
|
? (await options.client.listPagesDomains(requestedName)).map((item) => clean(item.name)).filter(Boolean)
|
|
850
851
|
: [];
|
|
851
|
-
const deployShareProject = async (projectName) => runPagesDeployAttempts(buildPagesDeployCommand(shareBundle.bundleDir, projectName), buildWranglerAuthEnv(options.credentials), resolveDeployProxyUrl(options.env), {
|
|
852
|
+
const deployShareProject = async (projectName) => runPagesDeployAttempts(buildPagesDeployCommand(options.wranglerExecutable, shareBundle.bundleDir, projectName), buildWranglerAuthEnv(options.credentials), resolveDeployProxyUrl(options.env), {
|
|
852
853
|
cwd: shareBundle.bundleDir,
|
|
853
854
|
runCommand: options.runCommand
|
|
854
855
|
});
|
|
@@ -946,9 +947,15 @@ export async function deployPagesWithBackend(input, options = {}) {
|
|
|
946
947
|
VPN_AUTOMATION_DEFAULT_PAGES_SECRET_ADMIN: getString(deploy, 'pages_secret_admin') || 'swimmingliu'
|
|
947
948
|
};
|
|
948
949
|
const credentials = resolveCloudflareCredentials(deploy, runtimeEnv);
|
|
949
|
-
const command = buildPagesDeployCommand(input.bundleDir, projectName);
|
|
950
950
|
const baseEnv = buildWranglerAuthEnv(credentials);
|
|
951
951
|
const runCommand = options.runCommand ?? ((commandToRun, runOptions) => runCommandWithSpawn(commandToRun, runOptions, options.spawn));
|
|
952
|
+
const wrangler = await (options.resolveManagedNpmTool ?? resolveManagedNpmTool)({
|
|
953
|
+
packageName: 'wrangler',
|
|
954
|
+
binaryName: 'wrangler',
|
|
955
|
+
version: '4.106.0',
|
|
956
|
+
projectRoot: input.projectRoot
|
|
957
|
+
});
|
|
958
|
+
const command = buildPagesDeployCommand(wrangler.command, input.bundleDir, projectName);
|
|
952
959
|
const proxyUrl = resolveDeployProxyUrl(env);
|
|
953
960
|
let activeCommand = command;
|
|
954
961
|
let finalProjectName = projectName;
|
|
@@ -984,7 +991,7 @@ export async function deployPagesWithBackend(input, options = {}) {
|
|
|
984
991
|
await ensureCustomDomainBound(client, finalProjectName, customDomain, { previousProjectName: projectName });
|
|
985
992
|
customDomainBound = true;
|
|
986
993
|
}
|
|
987
|
-
activeCommand = buildPagesDeployCommand(input.bundleDir, finalProjectName);
|
|
994
|
+
activeCommand = buildPagesDeployCommand(wrangler.command, input.bundleDir, finalProjectName);
|
|
988
995
|
const fallbackDeploy = await runPagesDeployAttempts(activeCommand, baseEnv, proxyUrl, {
|
|
989
996
|
cwd: input.bundleDir,
|
|
990
997
|
runCommand
|
|
@@ -1010,7 +1017,8 @@ export async function deployPagesWithBackend(input, options = {}) {
|
|
|
1010
1017
|
credentials,
|
|
1011
1018
|
client,
|
|
1012
1019
|
env,
|
|
1013
|
-
runCommand
|
|
1020
|
+
runCommand,
|
|
1021
|
+
wranglerExecutable: wrangler.command
|
|
1014
1022
|
});
|
|
1015
1023
|
}
|
|
1016
1024
|
if (result.returncode === 0 && customDomain) {
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { access, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { resolveUserRuntimeRoot } from './paths.js';
|
|
6
|
+
export class ManagedToolError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, code = 'MANAGED_TOOL_ERROR') {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ManagedToolError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function resolveManagedNpmTool(options) {
|
|
15
|
+
const packageName = validatePackageName(options.packageName);
|
|
16
|
+
const binaryName = validateSinglePathPart(options.binaryName, 'binaryName');
|
|
17
|
+
const version = validateSinglePathPart(options.version, 'version');
|
|
18
|
+
const toolsRoot = path.resolve(options.toolsRoot ?? path.join(resolveUserRuntimeRoot(), 'tools'));
|
|
19
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
20
|
+
const timeoutMs = validateTimeoutMs(options.timeoutMs ?? 120000);
|
|
21
|
+
const platform = options.platform ?? process.platform;
|
|
22
|
+
const runCommand = options.runCommand ??
|
|
23
|
+
((command, commandOptions) => runCommandWithSpawn(command, { ...commandOptions, timeoutMs }));
|
|
24
|
+
const managed = managedToolPaths(toolsRoot, packageName, version, binaryName);
|
|
25
|
+
const allowProjectFallback = options.allowProjectFallback ?? true;
|
|
26
|
+
const installMissing = options.installMissing ?? true;
|
|
27
|
+
const managedBinary = await firstExecutable(resolveManagedNpmToolBinaryCandidates(managed.binDir, binaryName, platform));
|
|
28
|
+
if (managedBinary) {
|
|
29
|
+
const verifiedVersion = await verifyToolVersion(managedBinary, managed.installDir, runCommand, 'managed');
|
|
30
|
+
return {
|
|
31
|
+
command: managedBinary,
|
|
32
|
+
args: [],
|
|
33
|
+
source: 'managed',
|
|
34
|
+
packageName,
|
|
35
|
+
version: verifiedVersion,
|
|
36
|
+
requestedVersion: version
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (allowProjectFallback) {
|
|
40
|
+
const projectBinDir = path.join(projectRoot, 'node_modules', '.bin');
|
|
41
|
+
const projectBinary = await firstExecutable(resolveManagedNpmToolBinaryCandidates(projectBinDir, binaryName, platform));
|
|
42
|
+
if (projectBinary) {
|
|
43
|
+
const verifiedVersion = await verifyToolVersion(projectBinary, projectRoot, runCommand, 'project');
|
|
44
|
+
return {
|
|
45
|
+
command: projectBinary,
|
|
46
|
+
args: [],
|
|
47
|
+
source: 'project',
|
|
48
|
+
packageName,
|
|
49
|
+
version: verifiedVersion,
|
|
50
|
+
requestedVersion: version
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!installMissing) {
|
|
55
|
+
throw new ManagedToolError(`Managed npm tool ${packageName}@${version} is not installed`, 'MANAGED_TOOL_MISSING');
|
|
56
|
+
}
|
|
57
|
+
await installManagedTool(packageName, version, managed.installDir, runCommand);
|
|
58
|
+
const installedBinary = await firstExecutable(resolveManagedNpmToolBinaryCandidates(managed.binDir, binaryName, platform));
|
|
59
|
+
if (!installedBinary) {
|
|
60
|
+
throw new ManagedToolError(`npm install completed but ${binaryName} was not found at the managed tool path`, 'MANAGED_TOOL_BINARY_MISSING');
|
|
61
|
+
}
|
|
62
|
+
const verifiedVersion = await verifyToolVersion(installedBinary, managed.installDir, runCommand, 'managed');
|
|
63
|
+
return {
|
|
64
|
+
command: installedBinary,
|
|
65
|
+
args: [],
|
|
66
|
+
source: 'managed',
|
|
67
|
+
packageName,
|
|
68
|
+
version: verifiedVersion,
|
|
69
|
+
requestedVersion: version
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function resolveManagedNpmToolBinaryCandidates(binDir, binaryName, platform = process.platform) {
|
|
73
|
+
const safeBinaryName = validateSinglePathPart(binaryName, 'binaryName');
|
|
74
|
+
const primary = path.join(binDir, safeBinaryName);
|
|
75
|
+
if (platform === 'win32' && !safeBinaryName.toLowerCase().endsWith('.cmd')) {
|
|
76
|
+
return [primary, `${primary}.cmd`];
|
|
77
|
+
}
|
|
78
|
+
return [primary];
|
|
79
|
+
}
|
|
80
|
+
export function normalizeManagedToolCommandForSpawn(command, platform = process.platform) {
|
|
81
|
+
const [executable, ...args] = command;
|
|
82
|
+
if (platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable)) {
|
|
83
|
+
return {
|
|
84
|
+
executable: 'cmd.exe',
|
|
85
|
+
args: ['/d', '/s', '/c', [executable, ...args].map(quoteWindowsCmdArgument).join(' ')]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { executable, args };
|
|
89
|
+
}
|
|
90
|
+
function managedToolPaths(toolsRoot, packageName, version, binaryName) {
|
|
91
|
+
const installDir = path.join(toolsRoot, 'npm', packageName, version);
|
|
92
|
+
return {
|
|
93
|
+
installDir,
|
|
94
|
+
binDir: path.join(installDir, 'node_modules', '.bin')
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function installManagedTool(packageName, version, installDir, runCommand) {
|
|
98
|
+
await mkdir(installDir, { recursive: true });
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await runCommand(['npm', 'install', '--no-save', '--no-audit', '--no-fund', `${packageName}@${version}`], {
|
|
102
|
+
cwd: installDir,
|
|
103
|
+
env: { NPM_CONFIG_YES: 'true' }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new ManagedToolError(`npm install failed for ${packageName}@${version}: ${safeErrorMessage(error)}`, 'MANAGED_TOOL_INSTALL_FAILED');
|
|
108
|
+
}
|
|
109
|
+
if (result.returncode !== 0) {
|
|
110
|
+
throw new ManagedToolError(`npm install failed for ${packageName}@${version}: ${safeCommandMessage(result)}`, 'MANAGED_TOOL_INSTALL_FAILED');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function verifyToolVersion(binaryPath, cwd, runCommand, source) {
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = await runCommand([binaryPath, '--version'], { cwd, env: {} });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new ManagedToolError(`Managed npm tool ${source} verification failed: ${safeErrorMessage(error)}`, 'MANAGED_TOOL_VERSION_FAILED');
|
|
120
|
+
}
|
|
121
|
+
if (result.returncode !== 0) {
|
|
122
|
+
throw new ManagedToolError(`Managed npm tool ${source} verification failed: ${safeCommandMessage(result)}`, 'MANAGED_TOOL_VERSION_FAILED');
|
|
123
|
+
}
|
|
124
|
+
return firstOutputLine(result.stdout || result.stderr) || 'unknown';
|
|
125
|
+
}
|
|
126
|
+
async function isExecutable(filePath) {
|
|
127
|
+
try {
|
|
128
|
+
await access(filePath, constants.X_OK);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function firstExecutable(candidates) {
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
if (await isExecutable(candidate)) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
function runCommandWithSpawn(command, options, spawnImpl = defaultSpawn) {
|
|
144
|
+
const { executable, args } = normalizeManagedToolCommandForSpawn(command);
|
|
145
|
+
const child = spawnImpl(executable, args, {
|
|
146
|
+
cwd: options.cwd,
|
|
147
|
+
env: { ...process.env, ...options.env },
|
|
148
|
+
detached: process.platform !== 'win32',
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
150
|
+
});
|
|
151
|
+
let stdout = '';
|
|
152
|
+
let stderr = '';
|
|
153
|
+
child.stdout?.on('data', (chunk) => {
|
|
154
|
+
stdout += String(chunk);
|
|
155
|
+
});
|
|
156
|
+
child.stderr?.on('data', (chunk) => {
|
|
157
|
+
stderr += String(chunk);
|
|
158
|
+
});
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
let settled = false;
|
|
161
|
+
const timeout = setTimeout(() => {
|
|
162
|
+
if (settled) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
settled = true;
|
|
166
|
+
killTimedOutChild(child);
|
|
167
|
+
child.stdout?.destroy();
|
|
168
|
+
child.stderr?.destroy();
|
|
169
|
+
resolve({
|
|
170
|
+
returncode: 1,
|
|
171
|
+
stdout,
|
|
172
|
+
stderr: `${stderr}${stderr ? '\n' : ''}command timed out after ${options.timeoutMs}ms`
|
|
173
|
+
});
|
|
174
|
+
}, options.timeoutMs);
|
|
175
|
+
child.on('error', (error) => {
|
|
176
|
+
if (settled) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
reject(error);
|
|
182
|
+
});
|
|
183
|
+
child.on('close', (exitCode) => {
|
|
184
|
+
if (settled) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
settled = true;
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
resolve({ returncode: exitCode ?? 1, stdout, stderr });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function quoteWindowsCmdArgument(value) {
|
|
194
|
+
const escaped = value.replace(/(["^&|<>()%!])/g, '^$1');
|
|
195
|
+
return `"${escaped}"`;
|
|
196
|
+
}
|
|
197
|
+
function firstOutputLine(value) {
|
|
198
|
+
return value.trim().split(/\r?\n/)[0]?.trim() ?? '';
|
|
199
|
+
}
|
|
200
|
+
function killTimedOutChild(child) {
|
|
201
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Fall back to killing the direct child below.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
child.kill('SIGKILL');
|
|
211
|
+
}
|
|
212
|
+
function safeCommandMessage(result) {
|
|
213
|
+
const output = [result.stderr, result.stdout].filter((value) => value.trim()).join('\n').trim();
|
|
214
|
+
const message = output || `exit code ${result.returncode}`;
|
|
215
|
+
return truncate(message.replace(/\s+/g, ' '), 640);
|
|
216
|
+
}
|
|
217
|
+
function safeErrorMessage(error) {
|
|
218
|
+
if (error instanceof Error) {
|
|
219
|
+
return truncate(error.message.replace(/\s+/g, ' '), 640);
|
|
220
|
+
}
|
|
221
|
+
return truncate(String(error).replace(/\s+/g, ' '), 640);
|
|
222
|
+
}
|
|
223
|
+
function truncate(value, maxLength) {
|
|
224
|
+
if (value.length <= maxLength) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
return `${value.slice(0, maxLength)}...`;
|
|
228
|
+
}
|
|
229
|
+
function validatePackageName(value) {
|
|
230
|
+
const packageName = requireNonEmpty(value, 'packageName');
|
|
231
|
+
if (path.isAbsolute(packageName) || packageName.includes('\\')) {
|
|
232
|
+
throw new ManagedToolError('packageName contains unsafe path characters', 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
233
|
+
}
|
|
234
|
+
const parts = packageName.split('/');
|
|
235
|
+
const scoped = packageName.startsWith('@');
|
|
236
|
+
if ((scoped && parts.length !== 2) || (!scoped && parts.length !== 1)) {
|
|
237
|
+
throw new ManagedToolError('packageName contains unsafe path segments', 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
238
|
+
}
|
|
239
|
+
for (const part of parts) {
|
|
240
|
+
rejectUnsafePathPart(part, 'packageName');
|
|
241
|
+
}
|
|
242
|
+
return packageName;
|
|
243
|
+
}
|
|
244
|
+
function validateSinglePathPart(value, name) {
|
|
245
|
+
const part = requireNonEmpty(value, name);
|
|
246
|
+
if (path.isAbsolute(part) || part.includes('/') || part.includes('\\')) {
|
|
247
|
+
throw new ManagedToolError(`${name} contains unsafe path characters`, 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
248
|
+
}
|
|
249
|
+
rejectUnsafePathPart(part, name);
|
|
250
|
+
return part;
|
|
251
|
+
}
|
|
252
|
+
function rejectUnsafePathPart(part, name) {
|
|
253
|
+
if (!part || part === '.' || part === '..') {
|
|
254
|
+
throw new ManagedToolError(`${name} contains unsafe path segments`, 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function validateTimeoutMs(value) {
|
|
258
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
259
|
+
throw new ManagedToolError('timeoutMs must be a positive number', 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function requireNonEmpty(value, name) {
|
|
264
|
+
const trimmed = String(value ?? '').trim();
|
|
265
|
+
if (!trimmed) {
|
|
266
|
+
throw new ManagedToolError(`${name} is required`, 'MANAGED_TOOL_INVALID_OPTIONS');
|
|
267
|
+
}
|
|
268
|
+
return trimmed;
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swimmingliu/autovpn",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "npm wrapper for the AutoVPN headless CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -p tsconfig.json",
|
|
25
25
|
"prepack": "npm run build",
|
|
26
|
-
"test": "npm run build && node --test test/*.test.mjs test/parity/*.test.mjs test/jobs/*.test.mjs test/pipeline/*.test.mjs test/runtime/*.test.mjs"
|
|
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"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=22.5.0"
|
|
30
30
|
},
|
|
31
|
-
"license": "
|
|
31
|
+
"license": "AGPL-3.0-only",
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^24.0.0",
|
|
34
34
|
"typescript": "^5.9.2"
|