@swimmingliu/autovpn 1.4.0

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.
Files changed (40) hide show
  1. package/README.md +102 -0
  2. package/bin/autovpn.mjs +5 -0
  3. package/dist/artifacts/list.js +99 -0
  4. package/dist/artifacts/preview.js +85 -0
  5. package/dist/backend/node-backend.js +194 -0
  6. package/dist/backend/python-backend.js +242 -0
  7. package/dist/backend/select-backend.js +12 -0
  8. package/dist/backend/types.js +1 -0
  9. package/dist/cli/commands/index.js +143 -0
  10. package/dist/cli/errors.js +7 -0
  11. package/dist/cli/global-options.js +29 -0
  12. package/dist/cli/main.js +231 -0
  13. package/dist/cli/native-commands.js +215 -0
  14. package/dist/cli/output.js +31 -0
  15. package/dist/config/profile.js +80 -0
  16. package/dist/doctor/checks.js +184 -0
  17. package/dist/events/schema.js +45 -0
  18. package/dist/jobs/commands.js +159 -0
  19. package/dist/jobs/logs.js +48 -0
  20. package/dist/jobs/process.js +75 -0
  21. package/dist/jobs/read.js +282 -0
  22. package/dist/jobs/store.js +115 -0
  23. package/dist/pipeline/availability.js +679 -0
  24. package/dist/pipeline/dedupe.js +104 -0
  25. package/dist/pipeline/deploy.js +1103 -0
  26. package/dist/pipeline/extract.js +259 -0
  27. package/dist/pipeline/obfuscate.js +203 -0
  28. package/dist/pipeline/orchestrator.js +1014 -0
  29. package/dist/pipeline/postprocess.js +214 -0
  30. package/dist/pipeline/proxy-runtime.js +228 -0
  31. package/dist/pipeline/render.js +91 -0
  32. package/dist/pipeline/speedtest.js +579 -0
  33. package/dist/runtime/env.js +35 -0
  34. package/dist/runtime/paths.js +66 -0
  35. package/dist/runtime/redaction.js +56 -0
  36. package/lib/cache.mjs +14 -0
  37. package/lib/errors.mjs +11 -0
  38. package/lib/install-python-cli.mjs +63 -0
  39. package/lib/runner.mjs +113 -0
  40. package/package.json +39 -0
@@ -0,0 +1,215 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { artifactLatest, artifactList } from '../artifacts/list.js';
4
+ import { previewArtifact } from '../artifacts/preview.js';
5
+ import { profilePayload, profileSummary, saveProfilePayload } from '../config/profile.js';
6
+ import { runDoctor } from '../doctor/checks.js';
7
+ import { publicStartedPayload, startDetachedResume, startDetachedRetry, startDetachedRun, stopManagedJob } from '../jobs/commands.js';
8
+ import { followLog } from '../jobs/logs.js';
9
+ import { latestJobId, listJobs, loadJob, publicJobPayload, singleActiveJobId, tailLog } from '../jobs/read.js';
10
+ import { readOptionValue, resolveProjectRoot } from '../runtime/paths.js';
11
+ function wantsPython(env, key) {
12
+ return String(env[key] ?? '').trim().toLowerCase() === 'python';
13
+ }
14
+ function jsonLine(payload) {
15
+ return `${JSON.stringify(payload)}\n`;
16
+ }
17
+ function positionalAfter(argv, start) {
18
+ for (let index = start; index < argv.length; index += 1) {
19
+ const value = argv[index];
20
+ if (value === '--project-root' || value === '--format' || value === '--tail' || value === '--output') {
21
+ index += 1;
22
+ continue;
23
+ }
24
+ if (value.startsWith('--')) {
25
+ continue;
26
+ }
27
+ return value;
28
+ }
29
+ return '';
30
+ }
31
+ function hasFlag(argv, flag) {
32
+ return argv.includes(flag);
33
+ }
34
+ function outputFormat(argv) {
35
+ return readOptionValue(argv, '--output') === 'human' ? 'human' : 'jsonl';
36
+ }
37
+ function jobOptions(context) {
38
+ return {
39
+ env: context.env,
40
+ cwd: context.cwd,
41
+ spawn: context.spawn,
42
+ now: context.now,
43
+ jobId: context.jobId,
44
+ sleep: context.sleep
45
+ };
46
+ }
47
+ async function writeFollowLog(projectRoot, jobId, argv, context) {
48
+ for await (const chunk of followLog(projectRoot, jobId, argv, { env: context.env, sleep: context.sleep })) {
49
+ context.io.writeStdout(chunk);
50
+ }
51
+ }
52
+ function resolveResumeSessionDir(job) {
53
+ const candidates = [
54
+ String(job.resume_from ?? ''),
55
+ String((job.options ?? {}).session_dir ?? ''),
56
+ String(job.session_dir ?? '')
57
+ ];
58
+ for (const candidate of candidates) {
59
+ if (candidate && fs.existsSync(path.join(candidate, 'session.json'))) {
60
+ return candidate;
61
+ }
62
+ }
63
+ return '';
64
+ }
65
+ export async function runNativeCommand(argv, context) {
66
+ const projectRoot = resolveProjectRoot(argv, context.cwd);
67
+ const command = argv[0];
68
+ if (command === 'doctor') {
69
+ if (wantsPython(context.env, 'AUTOVPN_DOCTOR_BACKEND'))
70
+ return context.pythonFallback(argv);
71
+ if (readOptionValue(argv, '--output') !== 'json')
72
+ return undefined;
73
+ const result = runDoctor(projectRoot, argv, context.env);
74
+ context.io.writeStdout(jsonLine(result.payload));
75
+ return result.code;
76
+ }
77
+ if (command === 'profile') {
78
+ if (wantsPython(context.env, 'AUTOVPN_PROFILE_BACKEND'))
79
+ return context.pythonFallback(argv);
80
+ if (argv[1] === 'show') {
81
+ context.io.writeStdout(jsonLine(profilePayload(projectRoot, context.env)));
82
+ return 0;
83
+ }
84
+ if (argv[1] === 'summary') {
85
+ context.io.writeStdout(jsonLine(profileSummary(projectRoot, context.env)));
86
+ return 0;
87
+ }
88
+ if (argv[1] === 'save') {
89
+ const payload = JSON.parse(await context.readStdin());
90
+ context.io.writeStdout(jsonLine(saveProfilePayload(projectRoot, payload, context.env)));
91
+ return 0;
92
+ }
93
+ }
94
+ if (command === 'artifacts') {
95
+ if (wantsPython(context.env, 'AUTOVPN_ARTIFACTS_BACKEND'))
96
+ return context.pythonFallback(argv);
97
+ const subcommand = argv[1];
98
+ if (subcommand === 'latest') {
99
+ context.io.writeStdout(jsonLine(artifactLatest(projectRoot, context.env)));
100
+ return 0;
101
+ }
102
+ if (subcommand === 'list') {
103
+ context.io.writeStdout(jsonLine(artifactList(projectRoot, context.env)));
104
+ return 0;
105
+ }
106
+ if (subcommand === 'preview') {
107
+ context.io.writeStdout(jsonLine(previewArtifact(path.resolve(context.cwd, positionalAfter(argv, 2)))));
108
+ return 0;
109
+ }
110
+ }
111
+ if (command === 'run' && hasFlag(argv, '--detach')) {
112
+ const job = await startDetachedRun({
113
+ projectRoot,
114
+ resumeLatest: hasFlag(argv, '--resume-latest'),
115
+ skipDeploy: hasFlag(argv, '--skip-deploy'),
116
+ skipVerify: hasFlag(argv, '--skip-verify'),
117
+ outputFormat: outputFormat(argv)
118
+ }, jobOptions(context));
119
+ context.io.writeStdout(jsonLine(publicStartedPayload(job)));
120
+ return 0;
121
+ }
122
+ if (command === 'jobs') {
123
+ const subcommand = positionalAfter(argv, 1);
124
+ if (subcommand === 'list') {
125
+ context.io.writeStdout(jsonLine(listJobs(projectRoot, context.env)));
126
+ return 0;
127
+ }
128
+ if (subcommand === 'status') {
129
+ const jobId = positionalAfter(argv, argv.indexOf(subcommand) + 1);
130
+ context.io.writeStdout(jsonLine(publicJobPayload(loadJob(projectRoot, jobId, context.env))));
131
+ return 0;
132
+ }
133
+ if (subcommand === 'logs') {
134
+ const jobId = positionalAfter(argv, argv.indexOf(subcommand) + 1);
135
+ if (argv.includes('--follow')) {
136
+ await writeFollowLog(projectRoot, jobId, argv, context);
137
+ return 0;
138
+ }
139
+ context.io.writeStdout(tailLog(projectRoot, jobId, argv, context.env));
140
+ return 0;
141
+ }
142
+ if (subcommand === 'stop') {
143
+ const jobId = positionalAfter(argv, argv.indexOf(subcommand) + 1);
144
+ const job = await stopManagedJob(projectRoot, jobId, {
145
+ ...jobOptions(context),
146
+ timeoutMs: Number.parseFloat(readOptionValue(argv, '--timeout') ?? '4') * 1000
147
+ });
148
+ context.io.writeStdout(jsonLine(publicJobPayload(job)));
149
+ return 0;
150
+ }
151
+ if (subcommand === 'resume' && hasFlag(argv, '--detach')) {
152
+ const sourceJobId = positionalAfter(argv, argv.indexOf(subcommand) + 1);
153
+ const sourceJob = loadJob(projectRoot, sourceJobId, context.env);
154
+ const sessionDir = resolveResumeSessionDir(sourceJob);
155
+ if (!sessionDir && String(sourceJob.kind ?? '') === 'run') {
156
+ const sourceOptions = sourceJob.options ?? {};
157
+ const job = await startDetachedRun({
158
+ projectRoot,
159
+ sourceJobId,
160
+ resumeLatest: true,
161
+ skipDeploy: Boolean(sourceOptions.skip_deploy),
162
+ skipVerify: Boolean(sourceOptions.skip_verify),
163
+ outputFormat: outputFormat(argv)
164
+ }, jobOptions(context));
165
+ context.io.writeStdout(jsonLine(publicStartedPayload(job)));
166
+ return 0;
167
+ }
168
+ if (!sessionDir) {
169
+ throw new Error('cannot resume job without session.json');
170
+ }
171
+ const job = await startDetachedResume({
172
+ projectRoot,
173
+ sourceJobId,
174
+ sessionDir,
175
+ outputFormat: outputFormat(argv)
176
+ }, jobOptions(context));
177
+ context.io.writeStdout(jsonLine(publicStartedPayload(job)));
178
+ return 0;
179
+ }
180
+ if (subcommand === 'retry' && hasFlag(argv, '--detach')) {
181
+ const job = await startDetachedRetry({
182
+ projectRoot,
183
+ artifactDir: path.resolve(context.cwd, readOptionValue(argv, '--artifact-dir') ?? ''),
184
+ stage: readOptionValue(argv, '--stage') ?? '',
185
+ outputFormat: outputFormat(argv)
186
+ }, jobOptions(context));
187
+ context.io.writeStdout(jsonLine(publicStartedPayload(job)));
188
+ return 0;
189
+ }
190
+ }
191
+ if (command === 'status') {
192
+ context.io.writeStdout(jsonLine(publicJobPayload(loadJob(projectRoot, latestJobId(projectRoot, context.env), context.env))));
193
+ return 0;
194
+ }
195
+ if (command === 'logs') {
196
+ const jobId = latestJobId(projectRoot, context.env);
197
+ const logFormat = readOptionValue(argv, '--format');
198
+ const syntheticArgv = logFormat ? argv : [...argv, '--format', 'human'];
199
+ if (argv.includes('--follow')) {
200
+ await writeFollowLog(projectRoot, jobId, syntheticArgv, context);
201
+ return 0;
202
+ }
203
+ context.io.writeStdout(tailLog(projectRoot, jobId, syntheticArgv, context.env));
204
+ return 0;
205
+ }
206
+ if (command === 'stop') {
207
+ const job = await stopManagedJob(projectRoot, singleActiveJobId(projectRoot, context.env), {
208
+ ...jobOptions(context),
209
+ timeoutMs: Number.parseFloat(readOptionValue(argv, '--timeout') ?? '4') * 1000
210
+ });
211
+ context.io.writeStdout(jsonLine(publicJobPayload(job)));
212
+ return 0;
213
+ }
214
+ return undefined;
215
+ }
@@ -0,0 +1,31 @@
1
+ export function defaultIo() {
2
+ return {
3
+ writeStdout(chunk) {
4
+ process.stdout.write(chunk);
5
+ },
6
+ writeStderr(chunk) {
7
+ process.stderr.write(chunk);
8
+ }
9
+ };
10
+ }
11
+ export function renderHelp() {
12
+ return `AutoVPN headless command line interface
13
+
14
+ Usage:
15
+ autovpn --help
16
+ autovpn --version
17
+ autovpn <command> [options]
18
+
19
+ Commands:
20
+ profile show|save|summary
21
+ doctor
22
+ artifacts latest|list|preview
23
+ run
24
+ retry-stage
25
+ resume pipeline|speedtest
26
+ jobs list|status|logs|stop|resume|retry
27
+ status
28
+ logs
29
+ stop
30
+ `;
31
+ }
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse, stringify } from '@iarna/toml';
4
+ import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
5
+ const DEFAULT_SOURCE_ORDER = ['leiting', 'heidong', 'mifeng', 'xuanfeng-area', 'xuanfeng-all-area'];
6
+ function state(value) {
7
+ return String(value ?? '').trim() ? 'set' : 'missing';
8
+ }
9
+ function readProfile(profilePath) {
10
+ if (!fs.existsSync(profilePath)) {
11
+ return {};
12
+ }
13
+ return parse(fs.readFileSync(profilePath, 'utf8'));
14
+ }
15
+ export function profileSummary(projectRoot, env = process.env) {
16
+ const profilePath = resolveProfilePath(projectRoot, env);
17
+ const stateRoot = path.dirname(profilePath);
18
+ const payload = readProfile(profilePath);
19
+ const rawSources = (payload.sources ?? {});
20
+ const sourceNames = [
21
+ ...DEFAULT_SOURCE_ORDER,
22
+ ...Object.keys(rawSources).filter((name) => !DEFAULT_SOURCE_ORDER.includes(name)).sort()
23
+ ];
24
+ const sources = Object.fromEntries(sourceNames.map((name) => {
25
+ const config = rawSources[name] ?? {};
26
+ return [
27
+ name,
28
+ {
29
+ enabled: Boolean(config?.enabled ?? true),
30
+ url: state(config?.url),
31
+ key: state(config?.key)
32
+ }
33
+ ];
34
+ }));
35
+ const deploy = (payload.deploy ?? {});
36
+ return {
37
+ ok: true,
38
+ sources,
39
+ deploy: {
40
+ project_name: String(deploy.project_name ?? ''),
41
+ pages_project_url: String(deploy.pages_project_url ?? ''),
42
+ cloudflare_api_token: state(deploy.cloudflare_api_token),
43
+ cloudflare_global_key: state(deploy.cloudflare_global_key),
44
+ cloudflare_email: state(deploy.cloudflare_email),
45
+ account_id: state(deploy.account_id),
46
+ subscription_url: state(deploy.subscription_url),
47
+ verify_subscription_url: state(deploy.verify_subscription_url),
48
+ secret_query: state(deploy.secret_query)
49
+ },
50
+ paths: {
51
+ project_root: projectRoot,
52
+ artifacts_root: resolveArtifactsRoot(projectRoot, env),
53
+ state_root: stateRoot,
54
+ profile_path: profilePath
55
+ }
56
+ };
57
+ }
58
+ export function profilePayload(projectRoot, env = process.env) {
59
+ const profilePath = resolveProfilePath(projectRoot, env);
60
+ const paths = {
61
+ project_root: projectRoot,
62
+ artifacts_root: resolveArtifactsRoot(projectRoot, env),
63
+ state_root: path.dirname(profilePath),
64
+ profile_path: profilePath
65
+ };
66
+ return {
67
+ ...readProfile(profilePath),
68
+ paths,
69
+ workspace: paths
70
+ };
71
+ }
72
+ export function saveProfilePayload(projectRoot, payload, env = process.env) {
73
+ const profilePath = resolveProfilePath(projectRoot, env);
74
+ fs.mkdirSync(path.dirname(profilePath), { recursive: true });
75
+ const persisted = { ...payload };
76
+ delete persisted.paths;
77
+ delete persisted.workspace;
78
+ fs.writeFileSync(profilePath, stringify(persisted), 'utf8');
79
+ return profilePayload(projectRoot, env);
80
+ }
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { parse } from '@iarna/toml';
6
+ import { profileSummary } from '../config/profile.js';
7
+ import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
8
+ function check(name, status, message, details = {}) {
9
+ return { name, status, message, details };
10
+ }
11
+ function pathWritable(targetPath) {
12
+ const target = fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory() ? targetPath : path.dirname(targetPath);
13
+ try {
14
+ fs.mkdirSync(target, { recursive: true });
15
+ const probe = path.join(target, '.doctor-write-test');
16
+ fs.writeFileSync(probe, 'ok', 'utf8');
17
+ fs.rmSync(probe, { force: true });
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function commandPath(name, env) {
25
+ const pathValue = String(env.PATH ?? process.env.PATH ?? '');
26
+ 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;
30
+ }
31
+ }
32
+ return '';
33
+ }
34
+ function safeRun(command, env) {
35
+ try {
36
+ const result = spawnSync(command[0], command.slice(1), {
37
+ encoding: 'utf8',
38
+ env: { ...process.env, ...env },
39
+ timeout: 5000
40
+ });
41
+ const output = String(result.stdout || result.stderr || '').trim().split(/\r?\n/)[0] ?? '';
42
+ return { ok: result.status === 0, message: output || `exit ${result.status ?? 1}` };
43
+ }
44
+ catch (error) {
45
+ const err = error;
46
+ return { ok: false, message: `${err.name}: ${err.message}` };
47
+ }
48
+ }
49
+ function canBindLocalhost() {
50
+ const server = net.createServer();
51
+ try {
52
+ server.listen(0, '127.0.0.1');
53
+ server.close();
54
+ return true;
55
+ }
56
+ catch {
57
+ server.close();
58
+ return false;
59
+ }
60
+ }
61
+ function checkSpeedTestConfig(profile) {
62
+ const speed = (profile.speed_test ?? {});
63
+ const invalid = [];
64
+ if (Number(speed.timeout_seconds ?? 0) < 1)
65
+ invalid.push('timeout_seconds');
66
+ if (Number(speed.concurrency ?? 0) < 1)
67
+ invalid.push('concurrency');
68
+ if (Number(speed.min_download_mb_s ?? 0) < 0)
69
+ invalid.push('min_download_mb_s');
70
+ if (Number(speed.max_download_bytes ?? 0) < 1)
71
+ invalid.push('max_download_bytes');
72
+ if (!String(speed.probe_url ?? '').trim())
73
+ invalid.push('probe_url');
74
+ if (invalid.length) {
75
+ return check('speed_test_config', 'fail', 'Speed test settings are invalid', { invalid_fields: invalid });
76
+ }
77
+ return check('speed_test_config', 'pass', 'Speed test settings are valid', {
78
+ speed_url_count: Array.isArray(speed.urls) ? speed.urls.length : 0,
79
+ has_probe_url: Boolean(String(speed.probe_url ?? '').trim())
80
+ });
81
+ }
82
+ function loadProfile(profilePath) {
83
+ if (!fs.existsSync(profilePath)) {
84
+ return {};
85
+ }
86
+ return parse(fs.readFileSync(profilePath, 'utf8'));
87
+ }
88
+ function checkProxyRuntime(env) {
89
+ const checks = [];
90
+ const mihomo = commandPath('mihomo', env);
91
+ if (!mihomo) {
92
+ checks.push(check('mihomo', 'fail', 'mihomo binary is missing'));
93
+ }
94
+ else {
95
+ const result = safeRun([mihomo, '-v'], env);
96
+ checks.push(check('mihomo', result.ok ? 'pass' : 'fail', result.ok ? 'mihomo is executable' : 'mihomo version command failed', {
97
+ path: mihomo,
98
+ version: result.message
99
+ }));
100
+ }
101
+ checks.push(check('localhost_port', canBindLocalhost() ? 'pass' : 'fail', canBindLocalhost() ? 'Localhost port binding works' : 'Localhost port binding failed'));
102
+ const configuredKeys = [
103
+ 'VPN_AUTOMATION_UPSTREAM_PROXY',
104
+ 'VPN_AUTOMATION_DEPLOY_PROXY',
105
+ 'VPN_AUTOMATION_CLOUDFLARE_PROXY',
106
+ 'HTTP_PROXY',
107
+ 'HTTPS_PROXY',
108
+ 'ALL_PROXY'
109
+ ].filter((key) => env[key] || process.env[key]);
110
+ checks.push(check('proxy_environment', 'pass', 'Proxy environment inspected', { configured_keys: configuredKeys }));
111
+ return checks;
112
+ }
113
+ function checkNodeTools(projectRoot, env) {
114
+ const missing = ['node', 'npm', 'npx'].filter((name) => !commandPath(name, env));
115
+ const checks = [
116
+ 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
+ ];
118
+ const hasPlaywright = [
119
+ path.join(projectRoot, 'node_modules', 'playwright'),
120
+ path.join(projectRoot, 'electron', 'runtime', 'node-vendor', 'node_modules', 'playwright')
121
+ ].some((candidate) => fs.existsSync(candidate));
122
+ 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'));
126
+ }
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 }));
130
+ }
131
+ return checks;
132
+ }
133
+ function checkCloudflare(summary, deploy, env) {
134
+ const checks = [];
135
+ const deploySummary = summary.deploy ?? {};
136
+ const hasCredentials = deploySummary.cloudflare_api_token === 'set'
137
+ || Boolean((env.CLOUDFLARE_API_TOKEN ?? '').trim())
138
+ || (deploySummary.cloudflare_global_key === 'set' && deploySummary.cloudflare_email === 'set')
139
+ || (Boolean((env.CLOUDFLARE_API_KEY ?? '').trim()) && Boolean((env.CLOUDFLARE_EMAIL ?? '').trim()));
140
+ checks.push(check('cloudflare_credentials', hasCredentials ? 'pass' : (deploy ? 'fail' : 'warn'), hasCredentials ? 'Cloudflare credentials are configured' : 'Cloudflare credentials are missing', { auth_state: hasCredentials ? 'set' : 'missing', deploy_required: deploy }));
141
+ const hasAccount = deploySummary.account_id === 'set' || Boolean((env.CLOUDFLARE_ACCOUNT_ID ?? '').trim());
142
+ checks.push(check('cloudflare_account', hasAccount ? 'pass' : (deploy ? 'fail' : 'warn'), hasAccount ? 'Cloudflare account ID is configured' : 'Cloudflare account ID is missing', { account_state: hasAccount ? 'set' : 'missing', deploy_required: deploy }));
143
+ const pagesProjectUrl = String(deploySummary.pages_project_url ?? '');
144
+ const hasDeployUrl = Boolean(String(deploySummary.project_name ?? '').trim() && URL.canParse(pagesProjectUrl));
145
+ 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'));
149
+ }
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 }));
153
+ }
154
+ return checks;
155
+ }
156
+ export function runDoctor(projectRoot, argv, env = process.env) {
157
+ const deploy = argv.includes('--deploy');
158
+ const strict = argv.includes('--strict');
159
+ const profilePath = resolveProfilePath(projectRoot, env);
160
+ const artifactsRoot = resolveArtifactsRoot(projectRoot, env);
161
+ const profile = loadProfile(profilePath);
162
+ const summary = profileSummary(projectRoot, env);
163
+ const sourceValues = Object.values((summary.sources ?? {}));
164
+ const configuredSources = sourceValues.filter((source) => source.enabled && source.url === 'set' && source.key === 'set');
165
+ const checks = [
166
+ check('node_version', 'pass', `Node ${process.versions.node}`, { required: '>=20' }),
167
+ check('project_root', 'pass', 'Project root resolved', { path: projectRoot }),
168
+ 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) }),
169
+ check('artifacts_root', pathWritable(artifactsRoot) ? 'pass' : 'fail', pathWritable(artifactsRoot) ? 'Artifacts root is writable' : 'Artifacts root is not writable', { path: artifactsRoot }),
170
+ 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') }),
171
+ 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') }),
172
+ configuredSources.length
173
+ ? check('sources', 'pass', 'At least one enabled source is configured', { configured_count: configuredSources.length })
174
+ : check('sources', 'warn', 'No enabled source has both URL and key configured', { configured_count: 0, key_state: 'missing' }),
175
+ checkSpeedTestConfig(profile),
176
+ ...checkProxyRuntime(env),
177
+ ...checkNodeTools(projectRoot, env),
178
+ ...checkCloudflare(summary, deploy, env)
179
+ ];
180
+ const hasFailures = checks.some((item) => item.status === 'fail');
181
+ const hasWarnings = checks.some((item) => item.status === 'warn');
182
+ const ok = !hasFailures && !(strict && hasWarnings);
183
+ return { code: ok ? 0 : 1, payload: { ok, deploy, strict, project_root: projectRoot, checks } };
184
+ }
@@ -0,0 +1,45 @@
1
+ const KNOWN_EVENT_TYPES = new Set([
2
+ 'run_started',
3
+ 'log',
4
+ 'stage',
5
+ 'summary',
6
+ 'run_failed',
7
+ 'extract_source_started',
8
+ 'extract_request_result',
9
+ 'extract_decrypt_result',
10
+ 'extract_iteration',
11
+ 'extract_source_completed',
12
+ 'extract_source_failed',
13
+ 'speedtest_runtime',
14
+ 'speedtest_probe_result',
15
+ 'speedtest_selected',
16
+ 'speedtest_result',
17
+ 'speedtest_resume_state',
18
+ 'resume_pipeline_state',
19
+ 'availability_link_result'
20
+ ]);
21
+ export function normalizeEvent(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ throw new Error('Backend event must be an object');
24
+ }
25
+ const event = value;
26
+ if (typeof event.type !== 'string' || !event.type.trim()) {
27
+ throw new Error('Backend event is missing string type');
28
+ }
29
+ const normalized = { ...event, type: event.type.trim() };
30
+ return normalized;
31
+ }
32
+ export function parseEventLine(line) {
33
+ try {
34
+ return normalizeEvent(JSON.parse(line));
35
+ }
36
+ catch (error) {
37
+ if (error instanceof SyntaxError) {
38
+ throw new Error(`Invalid backend event JSON: ${error.message}`);
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ export function isKnownEventType(type) {
44
+ return KNOWN_EVENT_TYPES.has(type);
45
+ }