@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.
@@ -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
  }
@@ -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 candidate = path.join(dir, name);
28
- if (fs.existsSync(candidate)) {
29
- return candidate;
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 result = spawnSync(command[0], command.slice(1), {
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', 'npx'].filter((name) => !commandPath(name, env));
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
- const npx = commandPath('npx', env);
124
- if (!npx) {
125
- checks.push(check('javascript_obfuscator', 'fail', 'npx is missing'));
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
- else {
128
- const result = safeRun([npx, 'javascript-obfuscator', '--version'], env);
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
- const npx = commandPath('npx', env);
147
- if (!npx) {
148
- checks.push(check('wrangler', deploy ? 'fail' : 'warn', 'npx is missing'));
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
- else {
151
- const result = safeRun([npx, 'wrangler', 'pages', 'deploy', '--help'], env);
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
- ...checkNodeTools(projectRoot, env),
178
- ...checkCloudflare(summary, deploy, env)
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');
@@ -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 ['npx', 'wrangler', 'pages', 'deploy', bundleDir, '--project-name', projectName, '--branch', PAGES_PRODUCTION_BRANCH];
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 [executable, ...args] = command;
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.1",
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": "UNLICENSED",
31
+ "license": "AGPL-3.0-only",
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.0.0",
34
34
  "typescript": "^5.9.2"