@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
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # AutoVPN CLI
2
+
3
+ Node-first npm CLI for AutoVPN headless and Agent workflows.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx -y @swimmingliu/autovpn@1.4.0 doctor --project-root . --output json
9
+ npm install -g @swimmingliu/autovpn@1.4.0
10
+ autovpn run --project-root . --skip-deploy --skip-verify --output jsonl
11
+ ```
12
+
13
+ GitHub Release tarballs remain available for pinned/offline installs:
14
+
15
+ ```bash
16
+ npm install -g https://github.com/SwimmingLiu/auto-vpn/releases/download/v<version>/swimmingliu-autovpn-<version>.tgz
17
+ ```
18
+
19
+ For Agent-friendly JSONL output, prefer foreground runs:
20
+
21
+ ```bash
22
+ autovpn doctor --project-root . --output json
23
+ autovpn run --project-root . --skip-deploy --skip-verify --output jsonl
24
+ autovpn artifacts latest --project-root .
25
+ ```
26
+
27
+ ## Runtime Shape
28
+
29
+ The CLI is currently Node-first with explicit Python rollback:
30
+
31
+ - Node.js handles `--help`, `--version`, argument validation, `doctor --output json`, `profile summary --json`, `artifacts latest/list/preview`, `status --json`, `logs`, read-only `jobs` commands, and detached job management.
32
+ - Node is the default backend for pipeline actions. Detached run/resume/retry workers spawn the Node CLI worker.
33
+ - Plain Cloudflare Pages deploy, primary blocked-project fallback, share-project `SUB` sync, share-project fallback, custom-domain binding, custom-domain DNS upsert, and verify are Node-native. Python backend and stage fallback remain available for rollback.
34
+
35
+ Node-orchestrated foreground run:
36
+
37
+ ```bash
38
+ autovpn run --project-root . --output jsonl
39
+ ```
40
+
41
+ Current Node backend notes:
42
+
43
+ - Detached job management runs in Node for `run --detach`, `jobs resume --detach`, and `jobs retry --detach`; detached run/resume/retry workers also use the Node CLI worker.
44
+ - Non-detached `retry-stage` runs through the Node backend for retryable artifact stages from `speedtest` through `verify`; non-detached `resume pipeline`, `resume speedtest`, and `run --resume-latest` continue existing sessions through the Node backend.
45
+ - Add `--skip-deploy --skip-verify` when you want an offline Node pipeline check.
46
+ - Plain Node foreground deploy/verify runs use Node for Wrangler deploy, primary blocked-project fallback, share-project sync/fallback, custom-domain binding, custom-domain DNS upsert, and verify.
47
+ - Deploy and verify can be rolled back with `AUTOVPN_STAGE_BACKEND_DEPLOY=python` and `AUTOVPN_STAGE_BACKEND_VERIFY=python`.
48
+ - `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node, and Node has direct HTTP speedtest and availability runtimes. Node also has opt-in Mihomo-backed paths: set `AUTOVPN_SPEEDTEST_RUNTIME=mihomo` for controller delay probing and candidate downloads through the local Mihomo proxy, and set `AUTOVPN_AVAILABILITY_RUNTIME=mihomo` to check provider availability through the same per-node proxy runtime.
49
+ - Project `.env` is loaded before resolving profile and artifact paths. Explicit process environment values still win over `.env`.
50
+
51
+ Fallback flags for migrated commands:
52
+
53
+ ```bash
54
+ AUTOVPN_CLI_SHELL=python autovpn <args>
55
+ AUTOVPN_BACKEND=python autovpn run --project-root . --output jsonl
56
+ AUTOVPN_DOCTOR_BACKEND=python autovpn doctor --output json
57
+ AUTOVPN_PROFILE_BACKEND=python autovpn profile summary --json
58
+ AUTOVPN_ARTIFACTS_BACKEND=python autovpn artifacts latest
59
+ ```
60
+
61
+ Job state, logs, stop, detached run, detached resume, and detached retry commands are Node-owned in v3 and intentionally ignore the old `AUTOVPN_JOBS_BACKEND` rollback flag.
62
+
63
+ ## Python Backend Resolution
64
+
65
+ When a command still needs Python, the wrapper resolves the backend in this order:
66
+
67
+ 1. `AUTOVPN_PYTHON_CLI`
68
+ 2. PATH `autovpn`, accepted only when `autovpn --version` matches this npm package version
69
+ 3. wrapper-managed Python virtual environment under `AUTOVPN_CACHE_DIR` or the user cache
70
+
71
+ For Python-backed commands, the wrapper forwards argv, stdin, stdout, stderr, and exit code.
72
+
73
+ ## Environment
74
+
75
+ - `AUTOVPN_CACHE_DIR`
76
+ - `AUTOVPN_WHEEL_URL`
77
+ - `AUTOVPN_PYTHON_PACKAGE`
78
+ - `AUTOVPN_PIP_INDEX_URL`
79
+ - `AUTOVPN_PIP_EXTRA_INDEX_URL`
80
+ - `AUTOVPN_NO_INSTALL`
81
+ - `AUTOVPN_FORCE_INSTALL`
82
+ - `AUTOVPN_ALLOW_VERSION_MISMATCH`
83
+ - `AUTOVPN_PYTHON_CLI`
84
+ - `AUTOVPN_CLI_SHELL`
85
+ - `AUTOVPN_BACKEND`
86
+ - `AUTOVPN_PIPELINE_BACKEND`
87
+ - `AUTOVPN_STAGE_BACKEND_EXTRACT`
88
+ - `AUTOVPN_STAGE_BACKEND_DEDUPE`
89
+ - `AUTOVPN_STAGE_BACKEND_SPEEDTEST`
90
+ - `AUTOVPN_STAGE_BACKEND_AVAILABILITY`
91
+ - `AUTOVPN_STAGE_BACKEND_POSTPROCESS`
92
+ - `AUTOVPN_STAGE_BACKEND_RENDER`
93
+ - `AUTOVPN_STAGE_BACKEND_OBFUSCATE`
94
+ - `AUTOVPN_STAGE_BACKEND_DEPLOY`
95
+ - `AUTOVPN_STAGE_BACKEND_VERIFY`
96
+ - `AUTOVPN_DOCTOR_BACKEND`
97
+ - `AUTOVPN_PROFILE_BACKEND`
98
+ - `AUTOVPN_ARTIFACTS_BACKEND`
99
+ - `VPN_AUTOMATION_RUNTIME_ROOT`
100
+ - `VPN_AUTOMATION_PROFILE_PATH`
101
+ - `VPN_AUTOMATION_ARTIFACTS_ROOT`
102
+ - `VPN_AUTOMATION_UPSTREAM_PROXY`
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCliShell } from '../dist/cli/main.js';
3
+
4
+ const code = await runCliShell(process.argv.slice(2));
5
+ process.exitCode = code;
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { safeDeployment, redactText } from '../runtime/redaction.js';
4
+ import { resolveArtifactsRoot } from '../runtime/paths.js';
5
+ function loadJson(filePath) {
6
+ if (!fs.existsSync(filePath)) {
7
+ return {};
8
+ }
9
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
10
+ }
11
+ function latestArtifactDir(artifactsRoot) {
12
+ if (!fs.existsSync(artifactsRoot)) {
13
+ return '';
14
+ }
15
+ const candidates = fs.readdirSync(artifactsRoot)
16
+ .map((name) => path.join(artifactsRoot, name))
17
+ .filter((item) => fs.statSync(item).isDirectory())
18
+ .sort((left, right) => {
19
+ const leftStat = fs.statSync(left);
20
+ const rightStat = fs.statSync(right);
21
+ return rightStat.mtimeMs - leftStat.mtimeMs || path.basename(right).localeCompare(path.basename(left));
22
+ });
23
+ return candidates[0] ?? '';
24
+ }
25
+ export function artifactLatest(projectRoot, env = process.env) {
26
+ const latest = latestArtifactDir(resolveArtifactsRoot(projectRoot, env));
27
+ if (!latest) {
28
+ return { ok: false, artifact_dir: '' };
29
+ }
30
+ const reportPath = path.join(latest, 'pipeline_report.json');
31
+ const payload = { ok: true, artifact_dir: latest };
32
+ if (!fs.existsSync(reportPath)) {
33
+ return { ...payload, run_status: '', stage_status: {}, counts: {}, source_counts: {}, deployment: {}, error: '' };
34
+ }
35
+ const report = loadJson(reportPath);
36
+ return {
37
+ ...payload,
38
+ run_status: report.run_status ?? '',
39
+ stage_status: report.stage_status ?? {},
40
+ counts: report.counts ?? {},
41
+ source_counts: report.source_counts ?? {},
42
+ deployment: safeDeployment((report.deployment ?? {})),
43
+ error: redactText(String(report.error ?? ''))
44
+ };
45
+ }
46
+ function hasNonEmptyFile(filePath) {
47
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile() && fs.statSync(filePath).size > 0;
48
+ }
49
+ function retryableStages(artifactDir, stageStatus) {
50
+ const stages = [];
51
+ if (hasNonEmptyFile(path.join(artifactDir, 'vpn_node_deduped.txt')))
52
+ stages.push('speedtest');
53
+ if (hasNonEmptyFile(path.join(artifactDir, 'vpn_node_speedtest.txt')))
54
+ stages.push('availability');
55
+ if (hasNonEmptyFile(path.join(artifactDir, 'vpn_node_availability.txt')))
56
+ stages.push('postprocess');
57
+ if (hasNonEmptyFile(path.join(artifactDir, 'vpn_node_emoji.txt')))
58
+ stages.push('render');
59
+ if (fs.existsSync(path.join(artifactDir, 'vmess_node.js')))
60
+ stages.push('obfuscate');
61
+ if (fs.existsSync(path.join(artifactDir, '_worker.js')))
62
+ stages.push('deploy');
63
+ if (stageStatus.deploy === 'success')
64
+ stages.push('verify');
65
+ return stages;
66
+ }
67
+ function isArtifactDir(artifactDir) {
68
+ if (!/^\d{8}-\d{6}$/.test(path.basename(artifactDir))) {
69
+ return false;
70
+ }
71
+ return fs.existsSync(path.join(artifactDir, 'pipeline_report.json')) || fs.existsSync(path.join(artifactDir, 'run.db'));
72
+ }
73
+ export function artifactList(projectRoot, env = process.env) {
74
+ const artifactsRoot = resolveArtifactsRoot(projectRoot, env);
75
+ if (!fs.existsSync(artifactsRoot)) {
76
+ return { ok: true, items: [] };
77
+ }
78
+ const items = fs.readdirSync(artifactsRoot)
79
+ .map((name) => path.join(artifactsRoot, name))
80
+ .filter((item) => fs.statSync(item).isDirectory() && isArtifactDir(item))
81
+ .sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs)
82
+ .slice(0, 20)
83
+ .map((artifactDir) => {
84
+ const report = loadJson(path.join(artifactDir, 'pipeline_report.json'));
85
+ const stageStatus = (report.stage_status ?? {});
86
+ return {
87
+ artifact_dir: artifactDir,
88
+ artifact_name: path.basename(artifactDir),
89
+ run_status: report.run_status ?? '',
90
+ stage_status: stageStatus,
91
+ counts: report.counts ?? {},
92
+ source_counts: report.source_counts ?? {},
93
+ retry_context: report.retry_context ?? {},
94
+ retryable_stages: retryableStages(artifactDir, stageStatus),
95
+ updated_at: new Date(fs.statSync(artifactDir).mtimeMs).toISOString().replace('Z', '')
96
+ };
97
+ });
98
+ return { ok: true, items };
99
+ }
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { redactText, safeDeployment } from '../runtime/redaction.js';
4
+ const FINAL_NODE_FILES = ['vpn_node_emoji.txt', 'vpn_node_availability.txt', 'vpn_node_speedtest.txt'];
5
+ function formatBytes(size) {
6
+ if (size >= 1024 * 1024) {
7
+ return `${(size / 1024 / 1024).toFixed(1)} MB`;
8
+ }
9
+ if (size >= 1024) {
10
+ return `${Math.ceil(size / 1024)} KB`;
11
+ }
12
+ return `${size} B`;
13
+ }
14
+ function loadJson(filePath) {
15
+ if (!fs.existsSync(filePath)) {
16
+ return {};
17
+ }
18
+ try {
19
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
20
+ }
21
+ catch {
22
+ return { error: `invalid ${path.basename(filePath)}` };
23
+ }
24
+ }
25
+ function fileInventory(artifactDir) {
26
+ return fs.readdirSync(artifactDir)
27
+ .map((name) => path.join(artifactDir, name))
28
+ .filter((item) => fs.statSync(item).isFile())
29
+ .map((item) => ({ name: path.basename(item), size: formatBytes(fs.statSync(item).size) }))
30
+ .sort((left, right) => String(left.name).localeCompare(String(right.name)));
31
+ }
32
+ function decodeVmessRegion(link) {
33
+ const value = link.trim();
34
+ if (!value.startsWith('vmess://')) {
35
+ return 'OTHER';
36
+ }
37
+ let encoded = value.slice('vmess://'.length);
38
+ encoded += '='.repeat((4 - (encoded.length % 4)) % 4);
39
+ try {
40
+ const payload = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
41
+ const match = String(payload.ps ?? '').toUpperCase().match(/\b([A-Z]{2})\b/);
42
+ return match ? match[1] : 'OTHER';
43
+ }
44
+ catch {
45
+ return 'OTHER';
46
+ }
47
+ }
48
+ function safeNodeCounts(artifactDir) {
49
+ const nodeSource = FINAL_NODE_FILES.find((name) => fs.existsSync(path.join(artifactDir, name))) ?? '';
50
+ if (!nodeSource) {
51
+ return { node_source: '', final_node_count: 0, regions: [] };
52
+ }
53
+ const lines = fs.readFileSync(path.join(artifactDir, nodeSource), 'utf8').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
54
+ const counts = new Map();
55
+ for (const line of lines) {
56
+ const region = decodeVmessRegion(line);
57
+ counts.set(region, (counts.get(region) ?? 0) + 1);
58
+ }
59
+ return {
60
+ node_source: nodeSource,
61
+ final_node_count: lines.length,
62
+ regions: [...counts.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([region_code, count]) => ({ region_code, count }))
63
+ };
64
+ }
65
+ export function previewArtifact(artifactDir) {
66
+ const absolute = path.resolve(artifactDir);
67
+ const resolved = fs.existsSync(absolute) ? fs.realpathSync(absolute) : absolute;
68
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
69
+ return { ok: false, artifact_dir: resolved };
70
+ }
71
+ const report = loadJson(path.join(resolved, 'pipeline_report.json'));
72
+ return {
73
+ ok: true,
74
+ artifact_dir: resolved,
75
+ run_status: report.run_status ?? '',
76
+ stage_status: report.stage_status ?? {},
77
+ counts: report.counts ?? {},
78
+ source_counts: report.source_counts ?? {},
79
+ deployment: safeDeployment((report.deployment ?? {})),
80
+ retry_context: report.retry_context ?? {},
81
+ error: redactText(String(report.error ?? '')),
82
+ files: fileInventory(resolved),
83
+ safe_node_counts: safeNodeCounts(resolved)
84
+ };
85
+ }
@@ -0,0 +1,194 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { resumeNodePipeline, retryNodePipelineStage, runNodePipeline } from '../pipeline/orchestrator.js';
5
+ import { resolveArtifactsRoot } from '../runtime/paths.js';
6
+ const require = createRequire(import.meta.url);
7
+ function unsupported(method) {
8
+ return new Error(`Node backend ${method} is not available yet; use AUTOVPN_BACKEND=python`);
9
+ }
10
+ function latestIncompleteRunArtifact(projectRoot, env) {
11
+ const artifactsRoot = resolveArtifactsRoot(projectRoot, env);
12
+ if (!fs.existsSync(artifactsRoot)) {
13
+ return undefined;
14
+ }
15
+ const { DatabaseSync } = require('node:sqlite');
16
+ const candidates = fs.readdirSync(artifactsRoot, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory())
18
+ .map((entry) => {
19
+ const artifactDir = path.join(artifactsRoot, entry.name);
20
+ const dbPath = path.join(artifactDir, 'run.db');
21
+ return { artifactDir, dbPath };
22
+ })
23
+ .filter((candidate) => fs.existsSync(candidate.dbPath))
24
+ .sort((left, right) => fs.statSync(right.artifactDir).mtimeMs - fs.statSync(left.artifactDir).mtimeMs);
25
+ for (const candidate of candidates) {
26
+ let db;
27
+ try {
28
+ db = new DatabaseSync(candidate.dbPath);
29
+ const runRow = db.prepare('SELECT status FROM runs ORDER BY run_id DESC LIMIT 1').get();
30
+ const runStatus = String(runRow?.status ?? '').trim();
31
+ if (['success', 'failed', 'stopped'].includes(runStatus)) {
32
+ continue;
33
+ }
34
+ const stageRows = db.prepare('SELECT stage_name, status FROM stage_events ORDER BY rowid ASC').all();
35
+ const stageStatus = new Map(stageRows.map((row) => [String(row.stage_name ?? ''), String(row.status ?? '')]));
36
+ if (stageStatus.get('verify') === 'success') {
37
+ continue;
38
+ }
39
+ return candidate.artifactDir;
40
+ }
41
+ catch {
42
+ continue;
43
+ }
44
+ finally {
45
+ db?.close();
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+ function writeResumeLatestSession(artifactDir, options) {
51
+ const sessionDir = path.join(artifactDir, '.node-resume-latest');
52
+ fs.mkdirSync(sessionDir, { recursive: true });
53
+ fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify({
54
+ artifact_dir: artifactDir,
55
+ event_log: options.eventLog ?? path.join(sessionDir, 'events.jsonl'),
56
+ human_log: options.humanLog ?? path.join(sessionDir, 'human.log')
57
+ }), 'utf8');
58
+ return sessionDir;
59
+ }
60
+ function pushEvent(queue, event) {
61
+ queue.events.push(event);
62
+ const wake = queue.wake.shift();
63
+ wake?.();
64
+ }
65
+ function finishQueue(queue, error) {
66
+ queue.done = true;
67
+ queue.error = error;
68
+ for (const wake of queue.wake.splice(0)) {
69
+ wake();
70
+ }
71
+ }
72
+ function waitForEvent(queue) {
73
+ return new Promise((resolve) => {
74
+ queue.wake.push(resolve);
75
+ });
76
+ }
77
+ export class NodeBackend {
78
+ kind = 'node';
79
+ env;
80
+ cwd;
81
+ constructor(options = {}) {
82
+ this.env = options.env ?? process.env;
83
+ this.cwd = options.cwd ?? process.cwd();
84
+ }
85
+ async *run(options) {
86
+ const queue = { events: [], wake: [], done: false };
87
+ const runner = options.resumeLatest
88
+ ? async () => {
89
+ const artifactDir = latestIncompleteRunArtifact(options.projectRoot, this.env);
90
+ if (!artifactDir) {
91
+ throw new Error('No incomplete run.db found to resume');
92
+ }
93
+ pushEvent(queue, { type: 'resume_latest_state', artifact_dir: artifactDir });
94
+ const session = writeResumeLatestSession(artifactDir, options);
95
+ return resumeNodePipeline({
96
+ projectRoot: options.projectRoot,
97
+ mode: 'pipeline',
98
+ session,
99
+ skipDeploy: options.skipDeploy,
100
+ skipVerify: options.skipVerify,
101
+ output: options.output,
102
+ eventLog: options.eventLog,
103
+ humanLog: options.humanLog
104
+ }, {
105
+ env: this.env,
106
+ emit: (event) => pushEvent(queue, event)
107
+ });
108
+ }
109
+ : async () => runNodePipeline(options, {
110
+ env: this.env,
111
+ emit: (event) => pushEvent(queue, event)
112
+ });
113
+ void runner()
114
+ .then(() => finishQueue(queue))
115
+ .catch((error) => finishQueue(queue, error));
116
+ while (!queue.done || queue.events.length > 0) {
117
+ if (queue.events.length === 0) {
118
+ await waitForEvent(queue);
119
+ continue;
120
+ }
121
+ yield queue.events.shift();
122
+ }
123
+ if (queue.error) {
124
+ throw queue.error;
125
+ }
126
+ }
127
+ async *retryStage(options) {
128
+ const queue = { events: [], wake: [], done: false };
129
+ void retryNodePipelineStage({
130
+ projectRoot: options.projectRoot,
131
+ artifactDir: options.artifactDir,
132
+ stage: options.stage,
133
+ output: options.output,
134
+ eventLog: options.eventLog,
135
+ humanLog: options.humanLog
136
+ }, {
137
+ env: this.env,
138
+ emit: (event) => pushEvent(queue, event)
139
+ })
140
+ .then(() => finishQueue(queue))
141
+ .catch((error) => finishQueue(queue, error));
142
+ while (!queue.done || queue.events.length > 0) {
143
+ if (queue.events.length === 0) {
144
+ await waitForEvent(queue);
145
+ continue;
146
+ }
147
+ yield queue.events.shift();
148
+ }
149
+ if (queue.error) {
150
+ throw queue.error;
151
+ }
152
+ }
153
+ async *resume(options) {
154
+ const queue = { events: [], wake: [], done: false };
155
+ void resumeNodePipeline({
156
+ projectRoot: options.projectRoot,
157
+ mode: options.mode,
158
+ session: options.session,
159
+ output: options.output,
160
+ eventLog: options.eventLog,
161
+ humanLog: options.humanLog
162
+ }, {
163
+ env: this.env,
164
+ emit: (event) => pushEvent(queue, event)
165
+ })
166
+ .then(() => finishQueue(queue))
167
+ .catch((error) => finishQueue(queue, error));
168
+ while (!queue.done || queue.events.length > 0) {
169
+ if (queue.events.length === 0) {
170
+ await waitForEvent(queue);
171
+ continue;
172
+ }
173
+ yield queue.events.shift();
174
+ }
175
+ if (queue.error) {
176
+ throw queue.error;
177
+ }
178
+ }
179
+ async startDetached(_options) {
180
+ throw unsupported('startDetached');
181
+ }
182
+ async stopJob(_jobId) {
183
+ throw unsupported('stopJob');
184
+ }
185
+ async readJob(_jobId) {
186
+ throw unsupported('readJob');
187
+ }
188
+ async *readLogs(_options) {
189
+ throw unsupported('readLogs');
190
+ }
191
+ async executeCli(_argv) {
192
+ throw unsupported('executeCli');
193
+ }
194
+ }