@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,242 @@
1
+ import { spawn as defaultSpawn } from 'node:child_process';
2
+ import { parseEventLine } from '../events/schema.js';
3
+ import { mergeProjectEnv } from '../runtime/env.js';
4
+ function pushOption(argv, name, value) {
5
+ if (value !== undefined && value !== '') {
6
+ argv.push(name, String(value));
7
+ }
8
+ }
9
+ function runArgs(options) {
10
+ const argv = ['run', '--project-root', options.projectRoot, '--output', options.output ?? 'jsonl'];
11
+ if (options.resumeLatest)
12
+ argv.push('--resume-latest');
13
+ if (options.skipDeploy)
14
+ argv.push('--skip-deploy');
15
+ if (options.skipVerify)
16
+ argv.push('--skip-verify');
17
+ pushOption(argv, '--event-log', options.eventLog);
18
+ pushOption(argv, '--human-log', options.humanLog);
19
+ return argv;
20
+ }
21
+ function retryArgs(options) {
22
+ const argv = [
23
+ 'retry-stage',
24
+ '--project-root',
25
+ options.projectRoot,
26
+ '--artifact-dir',
27
+ options.artifactDir,
28
+ '--stage',
29
+ options.stage,
30
+ '--output',
31
+ options.output ?? 'jsonl'
32
+ ];
33
+ pushOption(argv, '--event-log', options.eventLog);
34
+ pushOption(argv, '--human-log', options.humanLog);
35
+ return argv;
36
+ }
37
+ function resumeArgs(options) {
38
+ const argv = [
39
+ 'resume',
40
+ options.mode,
41
+ '--project-root',
42
+ options.projectRoot,
43
+ '--session',
44
+ options.session,
45
+ '--output',
46
+ options.output ?? 'jsonl'
47
+ ];
48
+ pushOption(argv, '--event-log', options.eventLog);
49
+ pushOption(argv, '--human-log', options.humanLog);
50
+ return argv;
51
+ }
52
+ async function* splitLines(stream) {
53
+ const queue = [];
54
+ let ended = false;
55
+ let pendingResolve;
56
+ let buffer = '';
57
+ stream.on('data', (chunk) => {
58
+ buffer += String(chunk);
59
+ const parts = buffer.split(/\r?\n/);
60
+ buffer = parts.pop() ?? '';
61
+ queue.push(...parts.filter((line) => line.trim()));
62
+ pendingResolve?.();
63
+ pendingResolve = undefined;
64
+ });
65
+ stream.on('end', () => {
66
+ if (buffer.trim())
67
+ queue.push(buffer);
68
+ ended = true;
69
+ pendingResolve?.();
70
+ pendingResolve = undefined;
71
+ });
72
+ while (!ended || queue.length) {
73
+ if (!queue.length) {
74
+ await new Promise((resolve) => {
75
+ pendingResolve = resolve;
76
+ });
77
+ continue;
78
+ }
79
+ yield queue.shift();
80
+ }
81
+ }
82
+ function waitForClose(child) {
83
+ return new Promise((resolve, reject) => {
84
+ child.once('error', reject);
85
+ child.once('close', (code, signal) => {
86
+ resolve({ code, signal });
87
+ });
88
+ });
89
+ }
90
+ function errorSummary(stderr) {
91
+ const summary = stderr.trim().split(/\r?\n/).filter(Boolean).slice(-3).join('\n');
92
+ return summary ? `: ${summary}` : '';
93
+ }
94
+ function parseJsonPayload(stdout) {
95
+ const line = stdout.split(/\r?\n/).find((candidate) => candidate.trim());
96
+ if (!line) {
97
+ throw new Error('Python backend returned empty JSON payload');
98
+ }
99
+ const payload = JSON.parse(line);
100
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
101
+ throw new Error('Python backend JSON payload must be an object');
102
+ }
103
+ return payload;
104
+ }
105
+ function projectRootFromArgv(argv) {
106
+ const index = argv.indexOf('--project-root');
107
+ if (index >= 0 && argv[index + 1]) {
108
+ return argv[index + 1];
109
+ }
110
+ const inline = argv.find((item) => item.startsWith('--project-root='));
111
+ if (inline) {
112
+ return inline.slice('--project-root='.length) || undefined;
113
+ }
114
+ return undefined;
115
+ }
116
+ export class PythonBackend {
117
+ kind = 'python';
118
+ env;
119
+ cwd;
120
+ runForwarder;
121
+ resolvePythonCli;
122
+ spawn;
123
+ constructor(options = {}) {
124
+ this.env = options.env ?? process.env;
125
+ this.cwd = options.cwd ?? process.cwd();
126
+ this.runForwarder = options.runForwarder;
127
+ this.resolvePythonCli = options.resolvePythonCli;
128
+ this.spawn = options.spawn ?? defaultSpawn;
129
+ }
130
+ async executeCli(argv) {
131
+ const env = this.envForArgv(argv);
132
+ const forwarderOptions = { env, cwd: this.cwd };
133
+ if (this.runForwarder) {
134
+ return this.runForwarder(argv, forwarderOptions);
135
+ }
136
+ // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
137
+ const runner = await import('../../lib/runner.mjs');
138
+ return Number(await runner.runForwarder(argv, forwarderOptions));
139
+ }
140
+ run(options) {
141
+ return this.streamEvents(runArgs(options));
142
+ }
143
+ retryStage(options) {
144
+ return this.streamEvents(retryArgs(options));
145
+ }
146
+ resume(options) {
147
+ return this.streamEvents(resumeArgs(options));
148
+ }
149
+ async startDetached(options) {
150
+ const argv = runArgs(options);
151
+ argv.push('--detach');
152
+ argv.push('--json');
153
+ return this.captureJson(argv);
154
+ }
155
+ async stopJob(jobId, options = {}) {
156
+ const argv = jobId === 'latest'
157
+ ? ['stop']
158
+ : ['jobs', 'stop', jobId];
159
+ pushOption(argv, '--project-root', options.projectRoot);
160
+ pushOption(argv, '--timeout', options.timeout);
161
+ return this.captureJson(argv);
162
+ }
163
+ async readJob(jobId, options = {}) {
164
+ const argv = jobId === 'latest'
165
+ ? ['status', '--json']
166
+ : ['jobs', 'status', jobId, '--json'];
167
+ pushOption(argv, '--project-root', options.projectRoot);
168
+ return this.captureJson(argv);
169
+ }
170
+ async *readLogs(options) {
171
+ const argv = options.jobId
172
+ ? ['jobs', 'logs', options.jobId, '--project-root', options.projectRoot]
173
+ : ['logs', '--project-root', options.projectRoot];
174
+ if (options.format)
175
+ argv.push('--format', options.format);
176
+ pushOption(argv, '--tail', options.tail);
177
+ if (options.follow)
178
+ argv.push('--follow');
179
+ yield* this.streamLines(argv);
180
+ }
181
+ async *streamEvents(argv) {
182
+ for await (const line of this.streamLines(argv)) {
183
+ yield parseEventLine(line);
184
+ }
185
+ }
186
+ async *streamLines(argv) {
187
+ const env = this.envForArgv(argv);
188
+ const resolved = this.resolvePythonCli ? this.resolvePythonCli() : await this.defaultResolvePythonCli(env);
189
+ const child = this.spawn(resolved.command, [...resolved.args, ...argv], {
190
+ cwd: this.cwd,
191
+ env,
192
+ stdio: ['ignore', 'pipe', 'pipe']
193
+ });
194
+ let stderr = '';
195
+ child.stderr?.on('data', (chunk) => {
196
+ stderr += String(chunk);
197
+ });
198
+ const closePromise = waitForClose(child);
199
+ const stdout = child.stdout;
200
+ if (!stdout) {
201
+ throw new Error('Python backend did not expose stdout for event streaming');
202
+ }
203
+ for await (const line of splitLines(stdout)) {
204
+ yield line;
205
+ }
206
+ const { code, signal } = await closePromise;
207
+ if (code !== 0) {
208
+ throw new Error(`Python backend exited with ${signal ? `signal ${signal}` : `code ${code}`}${errorSummary(stderr)}`);
209
+ }
210
+ }
211
+ async captureJson(argv) {
212
+ const env = this.envForArgv(argv);
213
+ const resolved = this.resolvePythonCli ? this.resolvePythonCli() : await this.defaultResolvePythonCli(env);
214
+ const child = this.spawn(resolved.command, [...resolved.args, ...argv], {
215
+ cwd: this.cwd,
216
+ env,
217
+ stdio: ['ignore', 'pipe', 'pipe']
218
+ });
219
+ let stdout = '';
220
+ let stderr = '';
221
+ child.stdout?.on('data', (chunk) => {
222
+ stdout += String(chunk);
223
+ });
224
+ child.stderr?.on('data', (chunk) => {
225
+ stderr += String(chunk);
226
+ });
227
+ const { code, signal } = await waitForClose(child);
228
+ if (code !== 0) {
229
+ throw new Error(`Python backend exited with ${signal ? `signal ${signal}` : `code ${code}`}${errorSummary(stderr)}`);
230
+ }
231
+ return parseJsonPayload(stdout);
232
+ }
233
+ envForArgv(argv) {
234
+ const projectRoot = projectRootFromArgv(argv);
235
+ return projectRoot ? mergeProjectEnv(projectRoot, this.env) : this.env;
236
+ }
237
+ async defaultResolvePythonCli(env = this.env) {
238
+ // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
239
+ const runner = await import('../../lib/runner.mjs');
240
+ return runner.resolveOrInstallPythonCli({ env });
241
+ }
242
+ }
@@ -0,0 +1,12 @@
1
+ import { NodeBackend } from './node-backend.js';
2
+ import { PythonBackend } from './python-backend.js';
3
+ export function selectBackend(options = {}) {
4
+ const backend = String(options.env?.AUTOVPN_BACKEND ?? '').trim().toLowerCase();
5
+ if (backend === 'python') {
6
+ return new PythonBackend(options);
7
+ }
8
+ if (backend && backend !== 'node') {
9
+ throw new Error(`Unsupported AUTOVPN_BACKEND: ${backend}`);
10
+ }
11
+ return new NodeBackend(options);
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { CliUsageError } from '../errors.js';
2
+ import { readOptionValue } from '../global-options.js';
3
+ const TOP_LEVEL_COMMANDS = new Set([
4
+ 'profile',
5
+ 'doctor',
6
+ 'artifacts',
7
+ 'run',
8
+ 'retry-stage',
9
+ 'resume',
10
+ 'jobs',
11
+ 'status',
12
+ 'logs',
13
+ 'stop'
14
+ ]);
15
+ const JOBS_SUBCOMMANDS = new Set(['list', 'status', 'logs', 'stop', 'resume', 'retry']);
16
+ const RESUME_SUBCOMMANDS = new Set(['pipeline', 'speedtest']);
17
+ const PROFILE_SUBCOMMANDS = new Set(['show', 'save', 'summary']);
18
+ const ARTIFACT_SUBCOMMANDS = new Set(['latest', 'list', 'preview']);
19
+ function validateChoice(commandLabel, optionName, value, choices) {
20
+ if (value === undefined || choices.includes(value)) {
21
+ return;
22
+ }
23
+ throw new CliUsageError(`${commandLabel} ${optionName} must be one of: ${choices.join(', ')}`);
24
+ }
25
+ function requireOption(commandLabel, argv, optionName) {
26
+ const value = readOptionValue(argv, optionName);
27
+ if (value === undefined || value === '') {
28
+ throw new CliUsageError(`${commandLabel} requires ${optionName}`);
29
+ }
30
+ return value;
31
+ }
32
+ function findJobsSubcommand(argv) {
33
+ for (let index = 1; index < argv.length; index += 1) {
34
+ const value = argv[index];
35
+ if (value === '--project-root') {
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (value.startsWith('--project-root=')) {
40
+ continue;
41
+ }
42
+ if (JOBS_SUBCOMMANDS.has(value)) {
43
+ return value;
44
+ }
45
+ if (!value.startsWith('-')) {
46
+ return value;
47
+ }
48
+ }
49
+ return '';
50
+ }
51
+ function findSubcommand(argv, choices) {
52
+ for (let index = 1; index < argv.length; index += 1) {
53
+ const value = argv[index];
54
+ if (value === '--project-root' || value === '--format' || value === '--tail' || value === '--output' || value === '--artifact-dir' || value === '--stage') {
55
+ index += 1;
56
+ continue;
57
+ }
58
+ if (value.startsWith('--project-root=')) {
59
+ continue;
60
+ }
61
+ if (choices.has(value)) {
62
+ return value;
63
+ }
64
+ if (!value.startsWith('-')) {
65
+ return value;
66
+ }
67
+ }
68
+ return '';
69
+ }
70
+ export function validateCommand(argv) {
71
+ const command = argv[0];
72
+ if (!command) {
73
+ throw new CliUsageError('missing command');
74
+ }
75
+ if (command.startsWith('-')) {
76
+ throw new CliUsageError(`unknown option: ${command}`);
77
+ }
78
+ if (!TOP_LEVEL_COMMANDS.has(command)) {
79
+ throw new CliUsageError(`unknown command: ${command}`);
80
+ }
81
+ if (command === 'doctor') {
82
+ validateChoice('doctor', '--output', readOptionValue(argv, '--output'), ['human', 'json']);
83
+ return;
84
+ }
85
+ if (command === 'profile') {
86
+ const subcommand = argv[1] ?? '';
87
+ if (!PROFILE_SUBCOMMANDS.has(subcommand)) {
88
+ throw new CliUsageError('profile subcommand must be one of: show, save, summary');
89
+ }
90
+ return;
91
+ }
92
+ if (command === 'artifacts') {
93
+ const subcommand = argv[1] ?? '';
94
+ if (!ARTIFACT_SUBCOMMANDS.has(subcommand)) {
95
+ throw new CliUsageError('artifacts subcommand must be one of: latest, list, preview');
96
+ }
97
+ if (subcommand === 'preview' && !findSubcommand(argv.slice(argv.indexOf(subcommand)), new Set())) {
98
+ throw new CliUsageError('artifacts preview requires artifact_dir');
99
+ }
100
+ return;
101
+ }
102
+ if (command === 'run' || command === 'retry-stage' || command === 'resume') {
103
+ validateChoice(command, '--output', readOptionValue(argv, '--output'), ['jsonl', 'human']);
104
+ if (command === 'retry-stage') {
105
+ requireOption('retry-stage', argv, '--artifact-dir');
106
+ requireOption('retry-stage', argv, '--stage');
107
+ }
108
+ if (command === 'resume') {
109
+ const subcommand = argv[1] ?? '';
110
+ if (!RESUME_SUBCOMMANDS.has(subcommand)) {
111
+ throw new CliUsageError('resume subcommand must be one of: pipeline, speedtest');
112
+ }
113
+ requireOption('resume', argv, '--session');
114
+ }
115
+ return;
116
+ }
117
+ if (command === 'logs') {
118
+ validateChoice('logs', '--format', readOptionValue(argv, '--format'), ['human', 'jsonl']);
119
+ return;
120
+ }
121
+ if (command === 'jobs') {
122
+ const subcommand = findJobsSubcommand(argv);
123
+ if (!JOBS_SUBCOMMANDS.has(subcommand)) {
124
+ throw new CliUsageError('jobs subcommand must be one of: list, status, logs, stop, resume, retry');
125
+ }
126
+ if (['status', 'logs', 'stop', 'resume'].includes(subcommand)) {
127
+ const value = findSubcommand(argv.slice(argv.indexOf(subcommand)), new Set());
128
+ if (!value) {
129
+ throw new CliUsageError(`jobs ${subcommand} requires job_id`);
130
+ }
131
+ }
132
+ if (subcommand === 'logs') {
133
+ validateChoice('jobs logs', '--format', readOptionValue(argv, '--format'), ['human', 'jsonl']);
134
+ }
135
+ if (subcommand === 'resume' || subcommand === 'retry') {
136
+ validateChoice(`jobs ${subcommand}`, '--output', readOptionValue(argv, '--output'), ['jsonl', 'human']);
137
+ }
138
+ if (subcommand === 'retry') {
139
+ requireOption('jobs retry', argv, '--artifact-dir');
140
+ requireOption('jobs retry', argv, '--stage');
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,7 @@
1
+ export class CliUsageError extends Error {
2
+ exitCode = 2;
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'CliUsageError';
6
+ }
7
+ }
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ export function normalizeProjectRootArgs(argv, cwd = process.cwd()) {
3
+ const normalized = [...argv];
4
+ for (let index = 0; index < normalized.length; index += 1) {
5
+ const value = normalized[index];
6
+ if (value === '--project-root' && index + 1 < normalized.length) {
7
+ normalized[index + 1] = path.resolve(cwd, normalized[index + 1]);
8
+ index += 1;
9
+ continue;
10
+ }
11
+ if (value.startsWith('--project-root=')) {
12
+ const [, projectRoot] = value.split('=', 2);
13
+ normalized[index] = `--project-root=${path.resolve(cwd, projectRoot)}`;
14
+ }
15
+ }
16
+ return normalized;
17
+ }
18
+ export function readOptionValue(argv, optionName) {
19
+ for (let index = 0; index < argv.length; index += 1) {
20
+ const value = argv[index];
21
+ if (value === optionName) {
22
+ return argv[index + 1];
23
+ }
24
+ if (value.startsWith(`${optionName}=`)) {
25
+ return value.slice(optionName.length + 1);
26
+ }
27
+ }
28
+ return undefined;
29
+ }
@@ -0,0 +1,231 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { validateCommand } from './commands/index.js';
4
+ import { CliUsageError } from './errors.js';
5
+ import { normalizeProjectRootArgs } from './global-options.js';
6
+ import { runNativeCommand } from './native-commands.js';
7
+ import { defaultIo, renderHelp } from './output.js';
8
+ import { selectBackend } from '../backend/select-backend.js';
9
+ import { loadJob } from '../jobs/read.js';
10
+ import { readOptionValue, resolveProjectRoot } from '../runtime/paths.js';
11
+ import { redactText } from '../runtime/redaction.js';
12
+ async function defaultReadPackageVersion() {
13
+ // @ts-expect-error The Phase 1 runner is plain ESM JavaScript.
14
+ const runner = await import('../../lib/runner.mjs');
15
+ return String(runner.readPackageVersion());
16
+ }
17
+ function defaultCreateBackend(options) {
18
+ return selectBackend(options);
19
+ }
20
+ async function defaultRunForwarder(argv, options) {
21
+ // @ts-expect-error The Phase 1 runner is plain ESM JavaScript.
22
+ const runner = await import('../../lib/runner.mjs');
23
+ return Number(await runner.runForwarder(argv, options));
24
+ }
25
+ async function defaultReadStdin() {
26
+ const chunks = [];
27
+ for await (const chunk of process.stdin) {
28
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
29
+ }
30
+ return Buffer.concat(chunks).toString('utf8');
31
+ }
32
+ function isEnabled(value) {
33
+ return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
34
+ }
35
+ async function resolvePackageVersion(options) {
36
+ if (options.packageVersion) {
37
+ return options.packageVersion;
38
+ }
39
+ if (options.readPackageVersion) {
40
+ return String(await options.readPackageVersion());
41
+ }
42
+ return defaultReadPackageVersion();
43
+ }
44
+ function hasFlag(argv, flag) {
45
+ return argv.includes(flag);
46
+ }
47
+ function eventOutputFormat(argv) {
48
+ return readOptionValue(argv, '--output') === 'human' ? 'human' : 'jsonl';
49
+ }
50
+ function positionalAfter(argv, start) {
51
+ for (let index = start; index < argv.length; index += 1) {
52
+ const value = argv[index];
53
+ if (value === '--project-root' || value === '--format' || value === '--tail' || value === '--output' || value === '--artifact-dir' || value === '--stage') {
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (value.startsWith('--')) {
58
+ continue;
59
+ }
60
+ return value;
61
+ }
62
+ return '';
63
+ }
64
+ function resolveResumeSessionDir(job) {
65
+ const candidates = [
66
+ String(job.resume_from ?? ''),
67
+ String((job.options ?? {}).session_dir ?? ''),
68
+ String(job.session_dir ?? '')
69
+ ];
70
+ for (const candidate of candidates) {
71
+ if (candidate && fs.existsSync(path.join(candidate, 'session.json'))) {
72
+ return candidate;
73
+ }
74
+ }
75
+ return '';
76
+ }
77
+ async function runForegroundPipeline(argv, backend, io, cwd) {
78
+ if (backend.kind !== 'node') {
79
+ return undefined;
80
+ }
81
+ const output = eventOutputFormat(argv);
82
+ let events;
83
+ if (argv[0] === 'run' && !hasFlag(argv, '--detach') && typeof backend.run === 'function') {
84
+ events = backend.run({
85
+ projectRoot: resolveProjectRoot(argv, cwd),
86
+ skipDeploy: hasFlag(argv, '--skip-deploy'),
87
+ skipVerify: hasFlag(argv, '--skip-verify'),
88
+ resumeLatest: hasFlag(argv, '--resume-latest'),
89
+ output,
90
+ eventLog: readOptionValue(argv, '--event-log'),
91
+ humanLog: readOptionValue(argv, '--human-log')
92
+ });
93
+ }
94
+ else if (argv[0] === 'retry-stage' && typeof backend.retryStage === 'function') {
95
+ events = backend.retryStage({
96
+ projectRoot: resolveProjectRoot(argv, cwd),
97
+ artifactDir: readOptionValue(argv, '--artifact-dir') ?? '',
98
+ stage: readOptionValue(argv, '--stage') ?? '',
99
+ output,
100
+ eventLog: readOptionValue(argv, '--event-log'),
101
+ humanLog: readOptionValue(argv, '--human-log')
102
+ });
103
+ }
104
+ else if (argv[0] === 'resume' && typeof backend.resume === 'function') {
105
+ events = backend.resume({
106
+ projectRoot: resolveProjectRoot(argv, cwd),
107
+ mode: argv[1] === 'speedtest' ? 'speedtest' : 'pipeline',
108
+ session: readOptionValue(argv, '--session') ?? '',
109
+ output,
110
+ eventLog: readOptionValue(argv, '--event-log'),
111
+ humanLog: readOptionValue(argv, '--human-log')
112
+ });
113
+ }
114
+ else if (argv[0] === 'jobs' && argv[1] === 'retry' && !hasFlag(argv, '--detach') && typeof backend.retryStage === 'function') {
115
+ events = backend.retryStage({
116
+ projectRoot: resolveProjectRoot(argv, cwd),
117
+ artifactDir: path.resolve(cwd, readOptionValue(argv, '--artifact-dir') ?? ''),
118
+ stage: readOptionValue(argv, '--stage') ?? '',
119
+ output
120
+ });
121
+ }
122
+ else if (argv[0] === 'jobs' && argv[1] === 'resume' && !hasFlag(argv, '--detach')) {
123
+ const projectRoot = resolveProjectRoot(argv, cwd);
124
+ const sourceJob = loadJob(projectRoot, positionalAfter(argv, 2));
125
+ const sessionDir = resolveResumeSessionDir(sourceJob);
126
+ if (!sessionDir && String(sourceJob.kind ?? '') === 'run' && typeof backend.run === 'function') {
127
+ const sourceOptions = sourceJob.options ?? {};
128
+ events = backend.run({
129
+ projectRoot,
130
+ resumeLatest: true,
131
+ skipDeploy: Boolean(sourceOptions.skip_deploy),
132
+ skipVerify: Boolean(sourceOptions.skip_verify),
133
+ output
134
+ });
135
+ }
136
+ else if (sessionDir && typeof backend.resume === 'function') {
137
+ events = backend.resume({
138
+ projectRoot,
139
+ mode: 'pipeline',
140
+ session: sessionDir,
141
+ output
142
+ });
143
+ }
144
+ else if (!sessionDir) {
145
+ throw new Error('cannot resume job without session.json');
146
+ }
147
+ }
148
+ if (!events) {
149
+ return undefined;
150
+ }
151
+ for await (const event of events) {
152
+ const backendEvent = event;
153
+ if (output === 'human') {
154
+ if (backendEvent.type === 'log' && typeof backendEvent.message === 'string') {
155
+ io.writeStdout(`${backendEvent.message}\n`);
156
+ }
157
+ else if (backendEvent.type === 'stage') {
158
+ io.writeStdout(`[${String(backendEvent.stage ?? '')}] ${String(backendEvent.status ?? '')}\n`);
159
+ }
160
+ else if (backendEvent.type === 'summary') {
161
+ io.writeStdout(`summary: ${String(backendEvent.run_status ?? '')} ${String(backendEvent.artifact_dir ?? '')}\n`);
162
+ }
163
+ continue;
164
+ }
165
+ io.writeStdout(`${JSON.stringify(backendEvent)}\n`);
166
+ }
167
+ return 0;
168
+ }
169
+ export async function runCliShell(argv, options = {}) {
170
+ const env = options.env ?? process.env;
171
+ const io = options.io ?? defaultIo();
172
+ const runForwarder = options.runForwarder ?? defaultRunForwarder;
173
+ const cwd = options.cwd ?? process.cwd();
174
+ if (env.AUTOVPN_CLI_SHELL === 'python') {
175
+ return runForwarder(argv, { env, cwd });
176
+ }
177
+ if (isEnabled(env.AUTOVPN_WRAPPER_PROBE) && argv.length === 1 && argv[0] === '--version') {
178
+ return 42;
179
+ }
180
+ if (argv[0] === '--help' || argv[0] === '-h') {
181
+ io.writeStdout(renderHelp());
182
+ return 0;
183
+ }
184
+ if (argv[0] === '--version') {
185
+ io.writeStdout(`autovpn ${await resolvePackageVersion(options)}\n`);
186
+ return 0;
187
+ }
188
+ try {
189
+ const normalizedArgv = normalizeProjectRootArgs(argv, cwd);
190
+ validateCommand(normalizedArgv);
191
+ const createBackend = options.createBackend ?? defaultCreateBackend;
192
+ const backend = createBackend({ env, cwd, runForwarder });
193
+ let pythonFallbackBackend;
194
+ const runExplicitPythonFallback = (fallbackArgv) => {
195
+ pythonFallbackBackend ??= createBackend({
196
+ env: { ...env, AUTOVPN_BACKEND: 'python' },
197
+ cwd,
198
+ runForwarder
199
+ });
200
+ return pythonFallbackBackend.executeCli(fallbackArgv);
201
+ };
202
+ const nativeResult = await runNativeCommand(normalizedArgv, {
203
+ cwd,
204
+ env,
205
+ io,
206
+ readStdin: options.readStdin ?? defaultReadStdin,
207
+ pythonFallback: runExplicitPythonFallback,
208
+ spawn: options.spawn,
209
+ now: options.now,
210
+ jobId: options.jobId,
211
+ sleep: options.sleep
212
+ });
213
+ if (nativeResult !== undefined) {
214
+ return nativeResult;
215
+ }
216
+ const foregroundResult = await runForegroundPipeline(normalizedArgv, backend, io, cwd);
217
+ if (foregroundResult !== undefined) {
218
+ return foregroundResult;
219
+ }
220
+ return await backend.executeCli(normalizedArgv);
221
+ }
222
+ catch (error) {
223
+ if (error instanceof CliUsageError) {
224
+ io.writeStderr(`autovpn: ${error.message}\n`);
225
+ return error.exitCode;
226
+ }
227
+ const message = redactText(error instanceof Error ? error.message : String(error));
228
+ io.writeStderr(`autovpn npm wrapper error: ${message}\n`);
229
+ return 1;
230
+ }
231
+ }