@swimmingliu/autovpn 1.5.1 → 1.5.3

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/main.js CHANGED
@@ -47,6 +47,15 @@ async function resolvePackageVersion(options) {
47
47
  function hasFlag(argv, flag) {
48
48
  return argv.includes(flag);
49
49
  }
50
+ function hasProxyFlag(argv) {
51
+ return argv.some((value) => value === '--proxy' || value.startsWith('--proxy='));
52
+ }
53
+ function isPipelineProxyCommand(argv) {
54
+ if (!hasProxyFlag(argv)) {
55
+ return false;
56
+ }
57
+ return argv[0] === 'run' || (argv[0] === 'resume' && argv[1] === 'pipeline');
58
+ }
50
59
  function eventOutputFormat(argv) {
51
60
  return readOptionValue(argv, '--output') === 'human' ? 'human' : 'jsonl';
52
61
  }
@@ -229,6 +238,9 @@ export async function runCliShell(argv, options = {}) {
229
238
  });
230
239
  return pythonFallbackBackend.executeCli(fallbackArgv);
231
240
  };
241
+ if (isPipelineProxyCommand(normalizedArgv)) {
242
+ return runExplicitPythonFallback(normalizedArgv);
243
+ }
232
244
  const nativeResult = await runNativeCommand(normalizedArgv, {
233
245
  cwd,
234
246
  env,
@@ -2,6 +2,7 @@ import http from 'node:http';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath, URL } from 'node:url';
5
+ import QRCode from 'qrcode';
5
6
  import { renderWebAdapterScript } from './web-adapter.js';
6
7
  import { redactText } from '../runtime/redaction.js';
7
8
  const SENSITIVE_KEYS = new Set([
@@ -139,6 +140,20 @@ export async function createAutoVpnServer(options) {
139
140
  writeJson(response, 200, await options.runtime.loadState());
140
141
  return;
141
142
  }
143
+ if (request.method === 'POST' && url.pathname === '/api/profile') {
144
+ const body = await readJsonBody(request);
145
+ writeJson(response, 200, await options.runtime.saveProfile?.(body) ?? { ok: false, error: 'profile_save_unavailable' });
146
+ return;
147
+ }
148
+ if (request.method === 'POST' && url.pathname === '/api/qr') {
149
+ const body = await readJsonBody(request);
150
+ const text = String(body.text ?? '');
151
+ writeJson(response, 200, {
152
+ ok: true,
153
+ dataUrl: await QRCode.toDataURL(text, { margin: 1, width: 220 })
154
+ });
155
+ return;
156
+ }
142
157
  if (request.method === 'GET' && url.pathname === '/api/events') {
143
158
  response.writeHead(200, {
144
159
  'Content-Type': 'text/event-stream; charset=utf-8',
@@ -165,6 +180,14 @@ export async function createAutoVpnServer(options) {
165
180
  writeJson(response, 200, await options.runtime.stopRun?.() ?? { ok: true, requested: false });
166
181
  return;
167
182
  }
183
+ if (request.method === 'POST' && url.pathname === '/api/runs/retry-stage') {
184
+ const body = await readJsonBody(request);
185
+ writeJson(response, 202, await options.runtime.startRetry?.({
186
+ artifactDir: String(body.artifactDir ?? ''),
187
+ stage: String(body.stage ?? '')
188
+ }) ?? { ok: false, error: 'retry_stage_unavailable' });
189
+ return;
190
+ }
168
191
  if (request.method === 'GET' && !url.pathname.startsWith('/api/')) {
169
192
  if (await serveRendererAsset(url, response)) {
170
193
  return;
@@ -1,7 +1,7 @@
1
1
  import { artifactLatest, artifactList } from '../artifacts/list.js';
2
2
  import { previewArtifact } from '../artifacts/preview.js';
3
- import { profilePayload } from '../config/profile.js';
4
- import { startDetachedRun as defaultStartDetachedRun, stopManagedJob as defaultStopManagedJob } from '../jobs/commands.js';
3
+ import { profilePayload, saveProfilePayload } from '../config/profile.js';
4
+ import { startDetachedRetry as defaultStartDetachedRetry, startDetachedRun as defaultStartDetachedRun, stopManagedJob as defaultStopManagedJob } from '../jobs/commands.js';
5
5
  import { followLog as defaultFollowLog } from '../jobs/logs.js';
6
6
  const DEPLOY_SECRET_KEYS = new Set([
7
7
  'cloudflare_api_token',
@@ -30,6 +30,28 @@ export function sanitizeProfileForServer(profile) {
30
30
  }
31
31
  return safe;
32
32
  }
33
+ function preserveRedactedSecrets(incoming, current) {
34
+ const merged = structuredClone(incoming ?? {});
35
+ const currentSources = (current.sources ?? {});
36
+ for (const [name, source] of Object.entries((merged.sources ?? {}))) {
37
+ const currentSource = currentSources[name] ?? {};
38
+ if (source && typeof source === 'object') {
39
+ for (const key of ['url', 'key']) {
40
+ if (source[key] === '<redacted>') {
41
+ source[key] = currentSource[key] ?? '';
42
+ }
43
+ }
44
+ }
45
+ }
46
+ const deploy = (merged.deploy ?? {});
47
+ const currentDeploy = (current.deploy ?? {});
48
+ for (const key of DEPLOY_SECRET_KEYS) {
49
+ if (deploy[key] === '<redacted>') {
50
+ deploy[key] = currentDeploy[key] ?? '';
51
+ }
52
+ }
53
+ return merged;
54
+ }
33
55
  function normalizeLatestArtifact(projectRoot, env) {
34
56
  const latest = artifactLatest(projectRoot, env);
35
57
  if (!latest.ok || !latest.artifact_dir) {
@@ -61,6 +83,7 @@ export function createServerRuntime(options) {
61
83
  let unsubscribeLogs;
62
84
  const subscribers = new Set();
63
85
  const startDetachedRun = options.startDetachedRun ?? defaultStartDetachedRun;
86
+ const startDetachedRetry = options.startDetachedRetry ?? defaultStartDetachedRetry;
64
87
  const stopManagedJob = options.stopManagedJob ?? defaultStopManagedJob;
65
88
  const followLog = options.followLog ?? defaultFollowLog;
66
89
  function publish(event) {
@@ -112,6 +135,14 @@ export function createServerRuntime(options) {
112
135
  deployment: (artifact?.deployment ?? {})
113
136
  };
114
137
  },
138
+ async saveProfile(profile) {
139
+ const current = profilePayload(options.projectRoot, options.env);
140
+ const merged = preserveRedactedSecrets(profile, current);
141
+ return {
142
+ ok: true,
143
+ profile: sanitizeProfileForServer(saveProfilePayload(options.projectRoot, merged, options.env))
144
+ };
145
+ },
115
146
  async startRun(runOptions = {}) {
116
147
  if (runState === 'running' || runState === 'stopping') {
117
148
  return { ok: false, error: 'run_already_active' };
@@ -131,6 +162,29 @@ export function createServerRuntime(options) {
131
162
  followJob(activeJobId);
132
163
  return { ok: true, runId: activeJobId, job_id: activeJobId, status: job.status ?? 'running' };
133
164
  },
165
+ async startRetry(retryOptions = {}) {
166
+ if (runState === 'running' || runState === 'stopping') {
167
+ return { ok: false, error: 'run_already_active' };
168
+ }
169
+ const artifactDir = String(retryOptions.artifactDir ?? '').trim();
170
+ const stage = String(retryOptions.stage ?? '').trim();
171
+ if (!artifactDir || !stage) {
172
+ return { ok: false, error: 'artifact_dir_and_stage_required' };
173
+ }
174
+ runState = 'running';
175
+ const job = await startDetachedRetry({
176
+ projectRoot: options.projectRoot,
177
+ artifactDir,
178
+ stage,
179
+ outputFormat: 'jsonl'
180
+ }, {
181
+ env: options.env,
182
+ cwd: options.projectRoot
183
+ });
184
+ activeJobId = String(job.job_id ?? '');
185
+ followJob(activeJobId);
186
+ return { ok: true, runId: activeJobId, job_id: activeJobId, status: job.status ?? 'running' };
187
+ },
134
188
  async stopRun() {
135
189
  if (runState !== 'running' || !activeJobId) {
136
190
  return { ok: true, requested: false };
@@ -35,7 +35,10 @@ export function renderWebAdapterScript() {
35
35
  const payload = await state();
36
36
  return payload.profile || {};
37
37
  },
38
- saveProfile: async () => ({ ok: false, error: 'profile_save_unavailable_in_server_mode' }),
38
+ saveProfile: async (profile) => request('/api/profile', {
39
+ method: 'POST',
40
+ body: JSON.stringify(profile || {})
41
+ }),
39
42
  runPipeline: async (options = {}) => request('/api/runs', {
40
43
  method: 'POST',
41
44
  body: JSON.stringify({
@@ -50,7 +53,10 @@ export function renderWebAdapterScript() {
50
53
  return { ok: true };
51
54
  },
52
55
  openPath: async () => ({ ok: false, error: 'open_path_unavailable_in_browser' }),
53
- generateQr: async () => ({ ok: false, dataUrl: '' }),
56
+ generateQr: async (text) => request('/api/qr', {
57
+ method: 'POST',
58
+ body: JSON.stringify({ text: String(text || '') })
59
+ }),
54
60
  previewArtifact: async () => {
55
61
  const payload = await state();
56
62
  return payload.artifact ? { ok: true, ...payload.artifact } : { ok: false };
@@ -63,7 +69,13 @@ export function renderWebAdapterScript() {
63
69
  const payload = await state();
64
70
  return { ok: true, items: payload.retryArtifacts || [] };
65
71
  },
66
- retryStage: async () => ({ ok: false, error: 'retry_stage_unavailable_in_server_mode' }),
72
+ retryStage: async (options = {}) => request('/api/runs/retry-stage', {
73
+ method: 'POST',
74
+ body: JSON.stringify({
75
+ artifactDir: String(options.artifactDir || ''),
76
+ stage: String(options.stage || '')
77
+ })
78
+ }),
67
79
  copyText: async (text) => {
68
80
  await navigator.clipboard.writeText(String(text || ''));
69
81
  return { ok: true };
@@ -5,7 +5,7 @@ const ZH_MESSAGES = {
5
5
  locale: 'zh-CN',
6
6
  appTitle: 'AutoVPN',
7
7
  sidebarTitle: 'AutoVPN',
8
- sidebarVersion: 'v.1.3.0',
8
+ sidebarVersion: 'v.1.5.3',
9
9
  brandSubtitle: '概览、运行、结果、订阅、日志、设置统一管理',
10
10
  languageLabel: '',
11
11
  saveButton: '保存配置',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmingliu/autovpn",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "npm wrapper for the AutoVPN headless CLI",
5
5
  "type": "module",
6
6
  "repository": {
@@ -31,9 +31,11 @@
31
31
  "license": "AGPL-3.0-only",
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.0.0",
34
+ "@types/qrcode": "^1.5.6",
34
35
  "typescript": "^5.9.2"
35
36
  },
36
37
  "dependencies": {
37
- "@iarna/toml": "^2.2.5"
38
+ "@iarna/toml": "^2.2.5",
39
+ "qrcode": "^1.5.4"
38
40
  }
39
41
  }