@minato-aqukin/autodl-cli 0.1.2 → 0.2.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.
@@ -0,0 +1,1704 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __nodeCreateRequire } from "node:module";
3
+ const require = __nodeCreateRequire(import.meta.url);
4
+ import {
5
+ AutoDLError,
6
+ SSHError,
7
+ UsageError,
8
+ assertNotAborted,
9
+ configDir,
10
+ connectSSH,
11
+ withSSH
12
+ } from "./chunk-GHJVQKKC.js";
13
+
14
+ // src/ssh/files.ts
15
+ import { createHash } from "crypto";
16
+ import { createReadStream, createWriteStream } from "fs";
17
+ import * as fs from "fs/promises";
18
+ import { basename, dirname, join, parse, posix, resolve } from "path";
19
+ import { Transform, Writable } from "stream";
20
+ import { pipeline } from "stream/promises";
21
+ import { z } from "zod";
22
+ function missing(err) {
23
+ return typeof err === "object" && err !== null && "code" in err && (err.code === "ENOENT" || err.code === 2);
24
+ }
25
+ function interrupted() {
26
+ return Object.assign(new Error("SSH transport disconnected"), { code: "ECONNRESET" });
27
+ }
28
+ function kind(stats) {
29
+ return stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other";
30
+ }
31
+ function kindFromMode(mode) {
32
+ const fileType = mode & 61440;
33
+ if (fileType === 40960) return "symlink";
34
+ if (fileType === 16384) return "directory";
35
+ if (fileType === 32768) return "file";
36
+ return "other";
37
+ }
38
+ function fromAttrs(attrs) {
39
+ return {
40
+ kind: kindFromMode(attrs.mode),
41
+ size: attrs.size,
42
+ mtime: attrs.mtime * 1e3
43
+ };
44
+ }
45
+ var localIO = {
46
+ async stat(path) {
47
+ try {
48
+ const s = await fs.lstat(path);
49
+ return { kind: kind(s), size: s.size, mtime: s.mtimeMs };
50
+ } catch (err) {
51
+ if (missing(err)) return null;
52
+ throw err;
53
+ }
54
+ },
55
+ async entries(path) {
56
+ const names = await fs.readdir(path, { withFileTypes: true });
57
+ const out = [];
58
+ for (const entry of names) {
59
+ if (entry.name === "." || entry.name === "..") continue;
60
+ if (entry.isSymbolicLink())
61
+ out.push({ name: entry.name, kind: "symlink", size: 0, mtime: 0 });
62
+ else if (entry.isDirectory())
63
+ out.push({ name: entry.name, kind: "directory", size: 0, mtime: 0 });
64
+ else if (entry.isFile()) {
65
+ const s = await fs.lstat(join(path, entry.name));
66
+ out.push({ name: entry.name, kind: "file", size: s.size, mtime: s.mtimeMs });
67
+ } else out.push({ name: entry.name, kind: "other", size: 0, mtime: 0 });
68
+ }
69
+ return out;
70
+ },
71
+ async mkdir(path) {
72
+ await fs.mkdir(path);
73
+ },
74
+ async rename(from, to, overwrite) {
75
+ if (overwrite) await fs.rename(from, to);
76
+ else {
77
+ const s = await fs.lstat(from);
78
+ if (s.isFile()) {
79
+ await fs.link(from, to);
80
+ await fs.unlink(from);
81
+ } else {
82
+ if (await localIO.stat(to)) throw new Error(`\u76EE\u6807\u5DF2\u5B58\u5728\uFF1A${to}`);
83
+ await fs.rename(from, to);
84
+ }
85
+ }
86
+ },
87
+ unlink: (path) => fs.unlink(path),
88
+ rmdir: (path) => fs.rmdir(path),
89
+ async times(path, mtime) {
90
+ await fs.utimes(path, mtime / 1e3, mtime / 1e3);
91
+ },
92
+ read: (path, start = 0, end) => createReadStream(path, { start, ...end === void 0 ? {} : { end } }),
93
+ write: (path, start, resume) => createWriteStream(path, { flags: resume ? "r+" : "wx", start, mode: 384 })
94
+ };
95
+ var RemoteIO = class {
96
+ constructor(conn, sftp, signal) {
97
+ this.conn = conn;
98
+ this.sftp = sftp;
99
+ this.signal = signal;
100
+ conn.on("close", this.onClose);
101
+ conn.on("error", this.onError);
102
+ sftp.on("close", this.onClose);
103
+ sftp.on("error", this.onError);
104
+ signal?.addEventListener("abort", this.onAbort, { once: true });
105
+ }
106
+ conn;
107
+ sftp;
108
+ signal;
109
+ failure = null;
110
+ pending = /* @__PURE__ */ new Set();
111
+ streams = /* @__PURE__ */ new Set();
112
+ onClose = () => this.fail(interrupted());
113
+ onError = (err) => this.fail(err);
114
+ onAbort = () => this.fail(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
115
+ fail(err) {
116
+ this.failure ??= err;
117
+ for (const reject of this.pending) reject(err);
118
+ for (const stream of this.streams) stream.destroy(err);
119
+ }
120
+ close() {
121
+ this.signal?.removeEventListener("abort", this.onAbort);
122
+ this.conn.off("close", this.onClose);
123
+ this.conn.off("error", this.onError);
124
+ this.sftp.off("close", this.onClose);
125
+ this.sftp.off("error", this.onError);
126
+ this.sftp.end();
127
+ }
128
+ call(start) {
129
+ if (this.failure) return Promise.reject(this.failure);
130
+ if (this.signal?.aborted) return Promise.reject(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
131
+ const { promise, resolve: done, reject } = Promise.withResolvers();
132
+ const fail = (err) => {
133
+ this.pending.delete(fail);
134
+ reject(err);
135
+ };
136
+ this.pending.add(fail);
137
+ try {
138
+ start((err, value) => {
139
+ this.pending.delete(fail);
140
+ if (err) reject(err);
141
+ else done(value);
142
+ });
143
+ } catch (err) {
144
+ fail(err instanceof Error ? err : new Error(String(err)));
145
+ }
146
+ return promise;
147
+ }
148
+ async stat(path) {
149
+ try {
150
+ const stats = await this.call((cb) => this.sftp.lstat(path, cb));
151
+ return { kind: kind(stats), size: stats.size, mtime: stats.mtime * 1e3 };
152
+ } catch (err) {
153
+ if (missing(err)) return null;
154
+ throw err;
155
+ }
156
+ }
157
+ async entries(path) {
158
+ const list = await this.call((cb) => this.sftp.readdir(path, cb));
159
+ return list.filter((entry) => entry.filename !== "." && entry.filename !== "..").map((entry) => ({ name: entry.filename, ...fromAttrs(entry.attrs) }));
160
+ }
161
+ async mkdir(path) {
162
+ await this.call((cb) => this.sftp.mkdir(path, (err) => cb(err, void 0)));
163
+ }
164
+ async rename(from, to, overwrite) {
165
+ await this.call((cb) => {
166
+ if (overwrite) this.sftp.ext_openssh_rename(from, to, (err) => cb(err, void 0));
167
+ else this.sftp.rename(from, to, (err) => cb(err, void 0));
168
+ });
169
+ }
170
+ async unlink(path) {
171
+ await this.call((cb) => this.sftp.unlink(path, (err) => cb(err, void 0)));
172
+ }
173
+ async rmdir(path) {
174
+ await this.call((cb) => this.sftp.rmdir(path, (err) => cb(err, void 0)));
175
+ }
176
+ async times(path, mtime) {
177
+ await this.call(
178
+ (cb) => this.sftp.utimes(path, mtime / 1e3, mtime / 1e3, (err) => cb(err, void 0))
179
+ );
180
+ }
181
+ track(stream) {
182
+ this.streams.add(stream);
183
+ stream.on("error", this.onError);
184
+ stream.once("close", () => this.streams.delete(stream));
185
+ if (this.failure) queueMicrotask(() => stream.destroy(this.failure ?? interrupted()));
186
+ return stream;
187
+ }
188
+ read(path, start = 0, end) {
189
+ return this.track(
190
+ this.sftp.createReadStream(path, { start, ...end === void 0 ? {} : { end } })
191
+ );
192
+ }
193
+ write(path, start, resume) {
194
+ return this.track(
195
+ this.sftp.createWriteStream(path, { flags: resume ? "r+" : "wx", start, mode: 384 })
196
+ );
197
+ }
198
+ };
199
+ async function openSftp(client, uuid, signal) {
200
+ const conn = await connectSSH(client, uuid, {
201
+ ...signal ? { signal } : {},
202
+ connectAttempts: 1
203
+ });
204
+ assertNotAborted(signal);
205
+ const { promise, resolve: done, reject } = Promise.withResolvers();
206
+ const lost = () => reject(interrupted());
207
+ const abort = () => {
208
+ reject(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
209
+ conn.end();
210
+ };
211
+ conn.once("close", lost);
212
+ conn.once("error", reject);
213
+ signal?.addEventListener("abort", abort, { once: true });
214
+ conn.sftp((err, handle) => {
215
+ if (err) reject(err);
216
+ else done(handle);
217
+ });
218
+ try {
219
+ const handle = await promise;
220
+ const io = new RemoteIO(conn, handle, signal);
221
+ assertNotAborted(signal);
222
+ return { conn, io };
223
+ } catch (err) {
224
+ conn.off("close", lost);
225
+ conn.off("error", reject);
226
+ signal?.removeEventListener("abort", abort);
227
+ conn.end();
228
+ throw err;
229
+ }
230
+ }
231
+ async function remote(client, uuid, fn, signal) {
232
+ const { conn, io } = await openSftp(client, uuid, signal);
233
+ try {
234
+ return await fn(io);
235
+ } finally {
236
+ io.close();
237
+ conn.end();
238
+ }
239
+ }
240
+ function fullPath(side, path) {
241
+ if (!path.trim() || path.includes("\0")) throw new Error("\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A\u6216\u5305\u542B NUL");
242
+ return side === "local" ? resolve(path) : posix.resolve("/", path);
243
+ }
244
+ function safeName(name, local) {
245
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0") || local && process.platform === "win32" && (/[\\:<>"|?*]/.test(name) || /[ .]$/.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name))) {
246
+ throw new Error(`\u4E0D\u5B89\u5168\u6216\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u540D\uFF1A${JSON.stringify(name)}`);
247
+ }
248
+ }
249
+ async function ensureDir(io, path, parent) {
250
+ const info = await io.stat(path);
251
+ if (info) {
252
+ if (info.kind !== "directory") throw new Error(`\u76EE\u6807\u4E0D\u662F\u666E\u901A\u76EE\u5F55\uFF08\u4E0D\u8DDF\u968F\u7B26\u53F7\u94FE\u63A5\uFF09\uFF1A${path}`);
253
+ return;
254
+ }
255
+ const up = parent(path);
256
+ if (up === path) throw new Error(`\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE\uFF1A${path}`);
257
+ await ensureDir(io, up, parent);
258
+ try {
259
+ await io.mkdir(path);
260
+ } catch (err) {
261
+ if ((await io.stat(path))?.kind !== "directory") throw err;
262
+ }
263
+ }
264
+ async function digest(io, path, signal, end) {
265
+ assertNotAborted(signal);
266
+ const hash = createHash("sha256");
267
+ await pipeline(
268
+ io.read(path, 0, end),
269
+ new Writable({
270
+ write(chunk, _encoding, done) {
271
+ hash.update(chunk);
272
+ done();
273
+ }
274
+ }),
275
+ { signal }
276
+ );
277
+ return hash.digest("hex");
278
+ }
279
+ function sameVersion(a, b) {
280
+ return a?.kind === b?.kind && a?.size === b?.size && a?.mtime === b?.mtime;
281
+ }
282
+ var CheckpointSchema = z.object({
283
+ version: z.literal(1),
284
+ key: z.string(),
285
+ choice: z.enum(["overwrite", "skip", "keep-both"]).optional(),
286
+ files: z.record(
287
+ z.object({ size: z.number(), mtime: z.number(), target: z.string(), complete: z.boolean() })
288
+ )
289
+ });
290
+ var FileWorkspace = class {
291
+ constructor(client, uuid) {
292
+ this.client = client;
293
+ this.uuid = uuid;
294
+ }
295
+ client;
296
+ uuid;
297
+ session = null;
298
+ connecting = null;
299
+ disposed = false;
300
+ use(side, fn) {
301
+ if (side === "local") return fn(localIO);
302
+ return this.withSession((io) => fn(io));
303
+ }
304
+ /**
305
+ * One SFTP connection for all short browsing operations. Directory contents
306
+ * are still read live on every call — only the TCP handshake and SSH
307
+ * authentication are reused. A dead connection is dropped and the next
308
+ * operation reconnects transparently. Long transfers keep their own
309
+ * connection so browsing and transfers never block each other.
310
+ */
311
+ async withSession(fn) {
312
+ if (this.disposed) throw new Error("\u6587\u4EF6\u4F1A\u8BDD\u5DF2\u5173\u95ED");
313
+ const live = this.session ?? await this.connect();
314
+ try {
315
+ return await fn(live.io);
316
+ } catch (err) {
317
+ this.drop(live);
318
+ throw err;
319
+ }
320
+ }
321
+ async connect() {
322
+ if (this.session) return this.session;
323
+ this.connecting ??= openSftp(this.client, this.uuid).then(
324
+ (session) => {
325
+ this.session = session;
326
+ this.connecting = null;
327
+ session.conn.once("close", () => this.drop(session));
328
+ return session;
329
+ },
330
+ (err) => {
331
+ this.connecting = null;
332
+ throw err;
333
+ }
334
+ );
335
+ return this.connecting;
336
+ }
337
+ drop(session) {
338
+ if (this.session !== session) return;
339
+ this.session = null;
340
+ try {
341
+ session.io.close();
342
+ } catch {
343
+ }
344
+ try {
345
+ session.conn.end();
346
+ } catch {
347
+ }
348
+ }
349
+ /** Release the browsing connection; safe to call more than once. */
350
+ dispose() {
351
+ this.disposed = true;
352
+ this.connecting = null;
353
+ const session = this.session;
354
+ this.session = null;
355
+ if (!session) return;
356
+ try {
357
+ session.io.close();
358
+ } catch {
359
+ }
360
+ try {
361
+ session.conn.end();
362
+ } catch {
363
+ }
364
+ }
365
+ async list(side, path) {
366
+ const root = fullPath(side, path);
367
+ const paths = side === "local" ? { join } : posix;
368
+ return this.use(side, async (io) => {
369
+ if ((await io.stat(root))?.kind !== "directory") throw new Error(`\u4E0D\u662F\u666E\u901A\u76EE\u5F55\uFF1A${root}`);
370
+ const entries = [];
371
+ for (const item of await io.entries(root)) {
372
+ safeName(item.name, side === "local");
373
+ const { name, ...info } = item;
374
+ entries.push({ name, path: paths.join(root, name), ...info });
375
+ }
376
+ return entries.sort(
377
+ (a, b) => Number(b.kind === "directory") - Number(a.kind === "directory") || a.name.localeCompare(b.name)
378
+ );
379
+ });
380
+ }
381
+ async mkdir(side, path) {
382
+ await this.use(
383
+ side,
384
+ (io) => ensureDir(io, fullPath(side, path), side === "local" ? dirname : posix.dirname)
385
+ );
386
+ }
387
+ async rename(side, from, to) {
388
+ const source = fullPath(side, from), target = fullPath(side, to);
389
+ if (source === target) return;
390
+ const root = side === "local" ? parse(source).root : "/";
391
+ if (source === root) throw new Error("\u4E0D\u80FD\u79FB\u52A8\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55");
392
+ await this.use(side, async (io) => {
393
+ if (await io.stat(target)) throw new Error(`\u76EE\u6807\u5DF2\u5B58\u5728\uFF0C\u672A\u8986\u76D6\uFF1A${target}`);
394
+ await io.rename(source, target, false);
395
+ });
396
+ }
397
+ async remove(side, path) {
398
+ if (path === "." || path === "..") throw new Error("\u4E0D\u80FD\u5220\u9664\u5F53\u524D\u4F4D\u7F6E\u6216\u7236\u76EE\u5F55");
399
+ const root = fullPath(side, path);
400
+ if (root === (side === "local" ? parse(root).root : "/"))
401
+ throw new Error("\u4E0D\u80FD\u5220\u9664\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55");
402
+ await this.use(side, async (io) => {
403
+ const walk = async (current) => {
404
+ const info = await io.stat(current);
405
+ if (!info) throw new Error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${current}`);
406
+ if (info.kind === "directory") {
407
+ for (const item of await io.entries(current)) {
408
+ safeName(item.name, side === "local");
409
+ await walk(
410
+ side === "local" ? join(current, item.name) : posix.join(current, item.name)
411
+ );
412
+ }
413
+ await io.rmdir(current);
414
+ } else await io.unlink(current);
415
+ };
416
+ await walk(root);
417
+ });
418
+ }
419
+ async transfer(request, callbacks, signal) {
420
+ if (request.uuid !== this.uuid || !/^[\da-f-]{36}$/i.test(request.id) || !request.sources.length)
421
+ throw new Error("\u4F20\u8F93\u4EFB\u52A1\u6807\u8BC6\u6216\u6765\u6E90\u65E0\u6548");
422
+ assertNotAborted(signal);
423
+ const upload = request.direction === "upload";
424
+ const sourcePaths = upload ? { join, dirname, basename } : posix;
425
+ const targetPaths = upload ? posix : { join, dirname, basename };
426
+ const destination = fullPath(upload ? "remote" : "local", request.destination);
427
+ const sources = request.sources.map((p) => fullPath(upload ? "local" : "remote", p));
428
+ const key = createHash("sha256").update(
429
+ JSON.stringify([this.uuid, upload, sources, destination, request.sync, request.checksum])
430
+ ).digest("hex");
431
+ const metadataDir = join(configDir(), "transfers", "partials");
432
+ const metadataPath = join(metadataDir, `${request.id}.json`);
433
+ let checkpoint = { version: 1, key, files: {} };
434
+ try {
435
+ checkpoint = CheckpointSchema.parse(JSON.parse(await fs.readFile(metadataPath, "utf8")));
436
+ } catch (err) {
437
+ if (!missing(err)) throw new Error(`\u7EED\u4F20\u8BB0\u5F55\u4E0D\u53EF\u8BFB\u53D6\uFF1A${metadataPath}`, { cause: err });
438
+ }
439
+ if (checkpoint.key !== key) throw new Error("\u7EED\u4F20\u4EFB\u52A1\u8DEF\u5F84\u6216\u9009\u9879\u5DF2\u6539\u53D8\uFF0C\u8BF7\u521B\u5EFA\u65B0\u4EFB\u52A1");
440
+ const save = async () => {
441
+ await fs.mkdir(metadataDir, { recursive: true, mode: 448 });
442
+ const temporary = `${metadataPath}.${process.pid}.tmp`;
443
+ await fs.writeFile(temporary, JSON.stringify(checkpoint), { mode: 384 });
444
+ await fs.rename(temporary, metadataPath);
445
+ };
446
+ await save();
447
+ return remote(
448
+ this.client,
449
+ this.uuid,
450
+ async (remoteIO) => {
451
+ const source = upload ? localIO : remoteIO;
452
+ const target = upload ? remoteIO : localIO;
453
+ const files = [];
454
+ const directories = [];
455
+ const skipped = [];
456
+ const destinations = /* @__PURE__ */ new Set();
457
+ const walk = async (from, to) => {
458
+ assertNotAborted(signal);
459
+ if (destinations.has(to)) throw new Error(`\u591A\u4E2A\u6765\u6E90\u6620\u5C04\u5230\u540C\u4E00\u76EE\u6807\uFF1A${to}`);
460
+ destinations.add(to);
461
+ const info = await source.stat(from);
462
+ if (!info) throw new Error(`\u6765\u6E90\u4E0D\u5B58\u5728\uFF1A${from}`);
463
+ if (info.kind === "symlink" || info.kind === "other") {
464
+ skipped.push(from);
465
+ return;
466
+ }
467
+ if (info.kind === "directory") {
468
+ directories.push(to);
469
+ for (const item of await source.entries(from)) {
470
+ safeName(item.name, !upload);
471
+ await walk(sourcePaths.join(from, item.name), targetPaths.join(to, item.name));
472
+ }
473
+ } else files.push({ source: from, target: to, info });
474
+ };
475
+ for (const path of sources) {
476
+ const name = sourcePaths.basename(path);
477
+ safeName(name, !upload);
478
+ await walk(path, targetPaths.join(destination, name));
479
+ }
480
+ await ensureDir(target, destination, targetPaths.dirname);
481
+ for (const dir of directories) {
482
+ assertNotAborted(signal);
483
+ await ensureDir(target, dir, targetPaths.dirname);
484
+ }
485
+ const total = files.reduce((sum, file) => sum + file.info.size, 0);
486
+ let transferred = 0, filesDone = 0, copied = 0, bytes = 0, sessionBytes = 0;
487
+ const started = Date.now();
488
+ const progress = (file) => callbacks.onProgress({
489
+ file,
490
+ transferred,
491
+ total,
492
+ filesDone,
493
+ filesTotal: files.length,
494
+ bytesPerSecond: sessionBytes * 1e3 / Math.max(1, Date.now() - started)
495
+ });
496
+ for (const file of files) {
497
+ assertNotAborted(signal);
498
+ const id = createHash("sha256").update(JSON.stringify([file.source, file.target])).digest("hex");
499
+ let saved = checkpoint.files[id];
500
+ let output = file.target;
501
+ if (saved && (targetPaths.dirname(saved.target) !== targetPaths.dirname(output) || !targetPaths.basename(saved.target).startsWith(targetPaths.basename(output))))
502
+ throw new Error("\u7EED\u4F20\u76EE\u6807\u4E0D\u5728\u539F\u4EFB\u52A1\u76EE\u5F55\u5185");
503
+ if (saved) output = saved.target;
504
+ let existing = await target.stat(output);
505
+ if (existing && existing.kind !== "file")
506
+ throw new Error(`\u76EE\u6807\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF0C\u672A\u8986\u76D6\uFF1A${output}`);
507
+ const unchanged = existing?.size === file.info.size && Math.floor(existing.mtime / 1e3) === Math.floor(file.info.mtime / 1e3);
508
+ let equal = unchanged;
509
+ if (existing && request.checksum && (request.sync || saved?.complete))
510
+ equal = existing.size === file.info.size && await digest(source, file.source, signal) === await digest(target, output, signal);
511
+ if ((request.sync || saved?.complete && saved.size === file.info.size && saved.mtime === file.info.mtime) && equal) {
512
+ transferred += file.info.size;
513
+ filesDone++;
514
+ skipped.push(file.source);
515
+ progress(file.source);
516
+ continue;
517
+ }
518
+ if (!request.sync && existing && !saved) {
519
+ const resolution = checkpoint.choice ? { choice: checkpoint.choice, applyToAll: true } : await callbacks.onConflict({
520
+ source: file.source,
521
+ destination: output,
522
+ sourceSize: file.info.size,
523
+ destinationSize: existing.size
524
+ });
525
+ assertNotAborted(signal);
526
+ if (resolution.applyToAll) {
527
+ checkpoint.choice = resolution.choice;
528
+ await save();
529
+ }
530
+ if (resolution.choice === "skip") {
531
+ skipped.push(file.source);
532
+ transferred += file.info.size;
533
+ filesDone++;
534
+ progress(file.source);
535
+ continue;
536
+ }
537
+ if (resolution.choice === "keep-both") {
538
+ let n = 1;
539
+ while (await target.stat(`${file.target} (${n})`)) n++;
540
+ output = `${file.target} (${n})`;
541
+ existing = null;
542
+ }
543
+ }
544
+ const temporary = targetPaths.join(
545
+ targetPaths.dirname(output),
546
+ `.autodl-${request.id}-${id.slice(0, 16)}.part`
547
+ );
548
+ await ensureDir(target, targetPaths.dirname(output), targetPaths.dirname);
549
+ let partial = await target.stat(temporary);
550
+ if (partial && partial.kind !== "file")
551
+ throw new Error(`\u7EED\u4F20\u4E34\u65F6\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF1A${temporary}`);
552
+ const versionMatches = saved?.size === file.info.size && saved.mtime === file.info.mtime;
553
+ let offset = versionMatches && partial ? partial.size : 0;
554
+ if (offset > file.info.size) offset = 0;
555
+ if (offset > 0 && await digest(source, file.source, signal, offset - 1) !== await digest(target, temporary, signal, offset - 1))
556
+ offset = 0;
557
+ if (partial && offset === 0) {
558
+ await target.unlink(temporary);
559
+ partial = null;
560
+ }
561
+ checkpoint.files[id] = {
562
+ size: file.info.size,
563
+ mtime: file.info.mtime,
564
+ target: output,
565
+ complete: false
566
+ };
567
+ saved = checkpoint.files[id];
568
+ await save();
569
+ transferred += offset;
570
+ progress(file.source);
571
+ if (offset < file.info.size || !partial) {
572
+ const meter = new Transform({
573
+ transform(chunk, _encoding, done) {
574
+ transferred += chunk.length;
575
+ sessionBytes += chunk.length;
576
+ progress(file.source);
577
+ done(null, chunk);
578
+ }
579
+ });
580
+ await pipeline(
581
+ source.read(file.source, offset),
582
+ meter,
583
+ target.write(temporary, offset, Boolean(partial)),
584
+ { signal }
585
+ );
586
+ }
587
+ assertNotAborted(signal);
588
+ if (!sameVersion(await source.stat(file.source), file.info))
589
+ throw new Error(`\u6765\u6E90\u5728\u4F20\u8F93\u4E2D\u53D1\u751F\u53D8\u5316\uFF0C\u672A\u8986\u76D6\u76EE\u6807\uFF1A${file.source}`);
590
+ if (!sameVersion(await target.stat(output), existing))
591
+ throw new Error(`\u76EE\u6807\u5728\u4F20\u8F93\u4E2D\u53D1\u751F\u53D8\u5316\uFF0C\u672A\u8986\u76D6\uFF1A${output}`);
592
+ await target.times(temporary, file.info.mtime);
593
+ await target.rename(temporary, output, Boolean(existing));
594
+ saved.complete = true;
595
+ await save();
596
+ copied++;
597
+ bytes += file.info.size;
598
+ filesDone++;
599
+ progress(file.source);
600
+ }
601
+ return { files: copied, bytes, skipped };
602
+ },
603
+ signal
604
+ );
605
+ }
606
+ };
607
+
608
+ // src/ssh/transfer-queue.ts
609
+ import { randomUUID } from "crypto";
610
+ import {
611
+ closeSync,
612
+ mkdirSync,
613
+ openSync,
614
+ readdirSync,
615
+ readFileSync,
616
+ renameSync,
617
+ rmSync,
618
+ writeFileSync,
619
+ writeSync
620
+ } from "fs";
621
+ import { join as join2 } from "path";
622
+ import { z as z2 } from "zod";
623
+ var RECORD_VERSION = 1;
624
+ var MAX_ATTEMPTS = 3;
625
+ var RETRY_DELAYS_MS = [2e3, 5e3];
626
+ var PROGRESS_EMIT_MS = 100;
627
+ var NAMESPACE_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
628
+ var OWNER_CLAIM_PREFIX = ".owner-";
629
+ var OWNER_CLAIM_SUFFIX = ".lock";
630
+ var liveOwners = /* @__PURE__ */ new Set();
631
+ var JOB_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
632
+ var JOB_FILE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
633
+ var OWNER_CLAIM_PATTERN = /^\.owner-(\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.lock$/i;
634
+ var jobIdSchema = z2.string().regex(JOB_ID_PATTERN, "\u4EFB\u52A1 id \u987B\u4E3A randomUUID");
635
+ var FileTransferRequestSchema = z2.object({
636
+ id: jobIdSchema,
637
+ uuid: z2.string().min(1),
638
+ direction: z2.union([z2.literal("upload"), z2.literal("download")]),
639
+ sources: z2.array(z2.string().min(1)).min(1),
640
+ destination: z2.string().min(1),
641
+ sync: z2.boolean(),
642
+ checksum: z2.boolean()
643
+ });
644
+ var EnqueueRequestSchema = FileTransferRequestSchema.omit({ id: true });
645
+ var PersistedJobSchema = z2.object({
646
+ version: z2.literal(1),
647
+ id: jobIdSchema,
648
+ enqueuedAt: z2.number(),
649
+ updatedAt: z2.number(),
650
+ request: FileTransferRequestSchema,
651
+ state: z2.union([
652
+ z2.literal("queued"),
653
+ z2.literal("running"),
654
+ z2.literal("conflict"),
655
+ z2.literal("paused"),
656
+ z2.literal("completed"),
657
+ z2.literal("cancelled")
658
+ ]),
659
+ error: z2.string().optional(),
660
+ result: z2.object({ files: z2.number(), bytes: z2.number(), skipped: z2.array(z2.string()) }).optional()
661
+ });
662
+ var QueueAbortError = class extends Error {
663
+ constructor() {
664
+ super("\u64CD\u4F5C\u5DF2\u53D6\u6D88");
665
+ this.name = "QueueAbortError";
666
+ }
667
+ };
668
+ function validateStoredRecord(raw, filename) {
669
+ const parsed = PersistedJobSchema.safeParse(raw);
670
+ if (!parsed.success) {
671
+ const detail = parsed.error.issues[0]?.message ?? "\u683C\u5F0F\u975E\u6CD5";
672
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u635F\u574F\uFF08${filename}\uFF09\uFF1A${detail}`);
673
+ }
674
+ const rec = parsed.data;
675
+ if (`${rec.id}.json` !== filename) {
676
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u635F\u574F\uFF08${filename}\uFF09\uFF1A\u6587\u4EF6\u540D\u4E0E\u8BB0\u5F55 id \u4E0D\u4E00\u81F4`);
677
+ }
678
+ if (rec.request.id !== rec.id) {
679
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u635F\u574F\uFF08${filename}\uFF09\uFF1A\u8BF7\u6C42 id \u4E0E\u8BB0\u5F55 id \u4E0D\u4E00\u81F4`);
680
+ }
681
+ return rec;
682
+ }
683
+ function toJobError(err) {
684
+ let message;
685
+ let hint;
686
+ if (err instanceof AutoDLError) {
687
+ message = err.message;
688
+ hint = err.hint;
689
+ } else if (err instanceof Error) {
690
+ message = err.message;
691
+ } else {
692
+ message = String(err);
693
+ }
694
+ const combined = hint ? `${message}\uFF08${hint}\uFF09` : message;
695
+ const clean = combined.replace(/(password|passwd|pwd|token|secret)\s*[:=]\s*\S+/gi, "$1=****").replace(/(-p|--password)\s+\S+/g, "$1 ****").trim();
696
+ return clean || "\u4F20\u8F93\u5931\u8D25";
697
+ }
698
+ var RETRYABLE_ERRNO = {
699
+ ECONNRESET: true,
700
+ ETIMEDOUT: true,
701
+ EPIPE: true,
702
+ ECONNREFUSED: true,
703
+ ENOTFOUND: true,
704
+ EHOSTUNREACH: true,
705
+ ENETUNREACH: true,
706
+ EAI_AGAIN: true
707
+ };
708
+ function isRecoverable(err) {
709
+ if (err instanceof QueueAbortError) return false;
710
+ if (err instanceof AutoDLError) {
711
+ if (err.code === "NETWORK" || err.code === "TIMEOUT") return true;
712
+ if (err.code === "SSH_FAILED") return hasRecoverableCause(err.cause);
713
+ return false;
714
+ }
715
+ return hasRecoverableCause(err);
716
+ }
717
+ function hasRecoverableCause(err, depth = 0) {
718
+ if (err === null || err === void 0) return false;
719
+ if (depth > 5) return false;
720
+ if (err instanceof AutoDLError) {
721
+ if (err.code === "NETWORK" || err.code === "TIMEOUT") return true;
722
+ if (err.code === "SSH_FAILED") return hasRecoverableCause(err.cause, depth + 1);
723
+ return false;
724
+ }
725
+ if (typeof err === "object" && "code" in err && typeof err.code === "string") {
726
+ if (RETRYABLE_ERRNO[err.code]) return true;
727
+ if (err instanceof Error && err.cause !== void 0) {
728
+ return hasRecoverableCause(err.cause, depth + 1);
729
+ }
730
+ return false;
731
+ }
732
+ if (typeof err === "object" && "level" in err && typeof err.level === "string") {
733
+ if (/auth/i.test(err.level)) return false;
734
+ }
735
+ const msg = err instanceof Error ? err.message : String(err);
736
+ if (isAuthLike(msg) || isLocalLike(msg)) return false;
737
+ if (isTransportLike(msg)) return true;
738
+ if (err instanceof Error && err.cause !== void 0) {
739
+ return hasRecoverableCause(err.cause, depth + 1);
740
+ }
741
+ return false;
742
+ }
743
+ function isAuthLike(msg) {
744
+ return /auth|password|passwd|credential|permission denied|unauthorized|forbidden|invalid token|expired|登录|密码|权限|认证|未授权|关机|已关机|开机|power|shutdown|offline|not running|无法建立|状态为/i.test(
745
+ msg
746
+ );
747
+ }
748
+ function isLocalLike(msg) {
749
+ return /not found|ENOENT|EACCES|EPERM|EROFS|ENOSPC|EEXIST/i.test(msg);
750
+ }
751
+ function isTransportLike(msg) {
752
+ return /disconnect|ECONNRESET|ETIMEDOUT|EPIPE|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|timed out|timeout|socket|closed|unreachable|temporar|connection lost|broken pipe|reset by peer|通道.{0,8}(中断|断开|重置|超时|关闭)|连接.{0,8}(中断|断开|重置|超时|关闭|丢失)/i.test(
753
+ msg
754
+ );
755
+ }
756
+ function isLivePid(pid) {
757
+ if (!Number.isInteger(pid) || pid <= 0) return false;
758
+ try {
759
+ process.kill(pid, 0);
760
+ return true;
761
+ } catch (err) {
762
+ if (typeof err === "object" && err !== null && "code" in err) {
763
+ if (err.code === "EPERM") return true;
764
+ }
765
+ return false;
766
+ }
767
+ }
768
+ function listOwnerClaims(dir) {
769
+ let files;
770
+ try {
771
+ files = readdirSync(dir);
772
+ } catch (err) {
773
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
774
+ return [];
775
+ }
776
+ throw err;
777
+ }
778
+ const claims = [];
779
+ for (const file of files) {
780
+ if (file.includes(".tmp.")) continue;
781
+ if (!file.startsWith(OWNER_CLAIM_PREFIX) || !file.endsWith(OWNER_CLAIM_SUFFIX)) continue;
782
+ const match = OWNER_CLAIM_PATTERN.exec(file);
783
+ if (!match?.[1] || !match[2]) {
784
+ claims.push({ file, pid: Number.NaN, nonce: "", ticket: void 0 });
785
+ continue;
786
+ }
787
+ let ticket;
788
+ try {
789
+ const raw = JSON.parse(readFileSync(join2(dir, file), "utf8"));
790
+ if (typeof raw === "object" && raw !== null && "ticket" in raw && typeof raw.ticket === "number" && Number.isInteger(raw.ticket) && raw.ticket >= 0) {
791
+ ticket = raw.ticket;
792
+ }
793
+ } catch {
794
+ ticket = void 0;
795
+ }
796
+ claims.push({ file, pid: Number(match[1]), nonce: match[2].toLowerCase(), ticket });
797
+ }
798
+ return claims;
799
+ }
800
+ function removeOwnClaim(dir, file) {
801
+ try {
802
+ rmSync(join2(dir, file), { force: true });
803
+ } catch {
804
+ }
805
+ }
806
+ function reapDeadClaims(dir, claims) {
807
+ for (const claim of claims) {
808
+ if (Number.isInteger(claim.pid) && !isLivePid(claim.pid)) {
809
+ removeOwnClaim(dir, claim.file);
810
+ }
811
+ }
812
+ }
813
+ function abortableDelay(ms, signal) {
814
+ return new Promise((resolve2, reject) => {
815
+ if (signal.aborted) {
816
+ reject(new QueueAbortError());
817
+ return;
818
+ }
819
+ const onAbort = () => {
820
+ clearTimeout(timer);
821
+ reject(new QueueAbortError());
822
+ };
823
+ const timer = setTimeout(() => {
824
+ signal.removeEventListener("abort", onAbort);
825
+ resolve2();
826
+ }, ms);
827
+ signal.addEventListener("abort", onAbort, { once: true });
828
+ });
829
+ }
830
+ function freezeJob(job) {
831
+ return Object.freeze({
832
+ request: Object.freeze({ ...job.request }),
833
+ state: job.state,
834
+ ...job.progress ? { progress: Object.freeze({ ...job.progress }) } : {},
835
+ ...job.conflict ? { conflict: Object.freeze({ ...job.conflict }) } : {},
836
+ ...job.result ? { result: Object.freeze({ ...job.result, skipped: [...job.result.skipped] }) } : {},
837
+ ...job.error !== void 0 ? { error: job.error } : {}
838
+ });
839
+ }
840
+ var TransferQueue = class {
841
+ client;
842
+ dir;
843
+ claimFile;
844
+ jobs = [];
845
+ listeners = /* @__PURE__ */ new Set();
846
+ conflicts = /* @__PURE__ */ new Map();
847
+ active = null;
848
+ cached = null;
849
+ pausing = false;
850
+ disposed = false;
851
+ constructor(client, namespace) {
852
+ if (!NAMESPACE_PATTERN.test(namespace)) {
853
+ throw new Error("\u4F20\u8F93\u961F\u5217\u547D\u540D\u7A7A\u95F4\u975E\u6CD5\uFF1A\u987B\u4E3A 8\u2013128 \u4F4D\u5B57\u6BCD/\u6570\u5B57/_/-");
854
+ }
855
+ this.client = client;
856
+ this.dir = join2(configDir(), "transfers", namespace);
857
+ for (const job of this.load()) {
858
+ this.jobs.push(job);
859
+ }
860
+ }
861
+ snapshot() {
862
+ if (!this.cached) {
863
+ this.cached = Object.freeze(this.jobs.map(freezeJob));
864
+ }
865
+ return this.cached;
866
+ }
867
+ subscribe(listener) {
868
+ this.assertUsable();
869
+ this.listeners.add(listener);
870
+ return () => {
871
+ this.listeners.delete(listener);
872
+ };
873
+ }
874
+ enqueue(request) {
875
+ this.assertUsable();
876
+ const parsed = EnqueueRequestSchema.safeParse(request);
877
+ if (!parsed.success) {
878
+ const detail = parsed.error.issues[0]?.message ?? "\u683C\u5F0F\u975E\u6CD5";
879
+ throw new Error(`\u4F20\u8F93\u8BF7\u6C42\u975E\u6CD5\uFF1A${detail}`);
880
+ }
881
+ this.claimOwnership();
882
+ const id = randomUUID();
883
+ const job = {
884
+ request: { ...parsed.data, id },
885
+ state: "queued",
886
+ enqueuedAt: Date.now(),
887
+ cancelRequested: false,
888
+ lastEmitAt: 0
889
+ };
890
+ this.jobs.push(job);
891
+ try {
892
+ this.save(job);
893
+ } catch (err) {
894
+ const index = this.jobs.indexOf(job);
895
+ if (index >= 0) this.jobs.splice(index, 1);
896
+ throw err;
897
+ }
898
+ this.emit();
899
+ this.pump();
900
+ return id;
901
+ }
902
+ resume(id) {
903
+ this.assertUsable();
904
+ const job = this.require(id);
905
+ if (job.state !== "paused" && job.state !== "cancelled") return;
906
+ try {
907
+ this.validateStored(job);
908
+ } catch (err) {
909
+ job.error = toJobError(err);
910
+ this.emit();
911
+ throw err;
912
+ }
913
+ this.claimOwnership();
914
+ const prev = { ...job };
915
+ job.state = "queued";
916
+ job.error = void 0;
917
+ job.result = void 0;
918
+ job.conflict = void 0;
919
+ try {
920
+ this.save(job);
921
+ } catch (err) {
922
+ Object.assign(job, prev);
923
+ this.emit();
924
+ throw err;
925
+ }
926
+ this.emit();
927
+ this.pump();
928
+ }
929
+ cancel(id) {
930
+ this.assertUsable();
931
+ const job = this.require(id);
932
+ if (job.state === "completed" || job.state === "cancelled") return;
933
+ this.claimOwnership();
934
+ if (job.state === "running" || job.state === "conflict") {
935
+ job.cancelRequested = true;
936
+ job.abort?.abort();
937
+ this.emit();
938
+ return;
939
+ }
940
+ const prevState = job.state;
941
+ const prevConflict = job.conflict;
942
+ job.state = "cancelled";
943
+ job.conflict = void 0;
944
+ try {
945
+ this.save(job);
946
+ } catch (err) {
947
+ job.state = prevState;
948
+ job.conflict = prevConflict;
949
+ this.emit();
950
+ throw err;
951
+ }
952
+ this.emit();
953
+ }
954
+ resolveConflict(id, resolution) {
955
+ this.assertUsable();
956
+ const job = this.require(id);
957
+ if (!resolution || resolution.choice !== "overwrite" && resolution.choice !== "skip" && resolution.choice !== "keep-both") {
958
+ throw new Error(`\u4EFB\u52A1 ${id}\uFF1A\u51B2\u7A81\u5904\u7406\u9009\u9879\u975E\u6CD5`);
959
+ }
960
+ const pending = this.conflicts.get(id);
961
+ if (!pending || job.state !== "conflict") {
962
+ throw new Error(`\u4EFB\u52A1 ${id} \u5F53\u524D\u6CA1\u6709\u5F85\u5904\u7406\u7684\u51B2\u7A81`);
963
+ }
964
+ this.claimOwnership();
965
+ pending.resolve({ choice: resolution.choice, applyToAll: resolution.applyToAll === true });
966
+ }
967
+ hasPending() {
968
+ return this.jobs.some(
969
+ (job) => job.state === "queued" || job.state === "running" || job.state === "conflict"
970
+ );
971
+ }
972
+ async pauseAll() {
973
+ if (this.disposed) return;
974
+ this.pausing = true;
975
+ let firstError;
976
+ try {
977
+ firstError = this.parkQueued() ?? firstError;
978
+ for (const job of this.jobs) {
979
+ job.abort?.abort();
980
+ }
981
+ const active = this.active;
982
+ if (active) await active.done;
983
+ firstError = this.parkQueued() ?? firstError;
984
+ this.emit();
985
+ if (firstError) throw firstError;
986
+ } finally {
987
+ this.pausing = false;
988
+ }
989
+ }
990
+ async dispose() {
991
+ if (this.disposed) return;
992
+ try {
993
+ await this.pauseAll();
994
+ } finally {
995
+ this.disposed = true;
996
+ this.listeners.clear();
997
+ this.conflicts.clear();
998
+ this.releaseOwnership();
999
+ }
1000
+ }
1001
+ /** Park every queued job as paused, persisting each record. */
1002
+ parkQueued() {
1003
+ let firstError;
1004
+ for (const job of this.jobs) {
1005
+ if (job.state === "queued") {
1006
+ job.state = "paused";
1007
+ try {
1008
+ this.save(job);
1009
+ } catch (err) {
1010
+ firstError = firstError ?? err;
1011
+ }
1012
+ }
1013
+ }
1014
+ return firstError;
1015
+ }
1016
+ assertUsable() {
1017
+ if (this.disposed) throw new Error("\u4F20\u8F93\u961F\u5217\u5DF2\u91CA\u653E");
1018
+ }
1019
+ require(id) {
1020
+ if (!JOB_ID_PATTERN.test(id)) throw new Error(`\u672A\u77E5\u4F20\u8F93\u4EFB\u52A1\uFF1A${id}`);
1021
+ const job = this.jobs.find((j) => j.request.id === id);
1022
+ if (!job) throw new Error(`\u672A\u77E5\u4F20\u8F93\u4EFB\u52A1\uFF1A${id}`);
1023
+ return job;
1024
+ }
1025
+ emit() {
1026
+ this.cached = null;
1027
+ for (const listener of [...this.listeners]) {
1028
+ try {
1029
+ listener();
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ }
1034
+ /** Invalidate the cached snapshot; notify subscribers at most ~10/s. */
1035
+ noteProgress() {
1036
+ this.cached = null;
1037
+ const now = Date.now();
1038
+ const running = this.jobs.find((j) => j.state === "running");
1039
+ if (running && now - running.lastEmitAt < PROGRESS_EMIT_MS) return;
1040
+ if (running) running.lastEmitAt = now;
1041
+ for (const listener of [...this.listeners]) {
1042
+ try {
1043
+ listener();
1044
+ } catch {
1045
+ }
1046
+ }
1047
+ }
1048
+ pump() {
1049
+ if (this.disposed || this.pausing || this.active) return;
1050
+ const next = this.jobs.find((job) => job.state === "queued");
1051
+ if (!next) return;
1052
+ const done = this.runJob(next).then(
1053
+ () => void 0,
1054
+ () => void 0
1055
+ );
1056
+ this.active = { id: next.request.id, done };
1057
+ void done.then(() => {
1058
+ if (this.active?.done === done) this.active = null;
1059
+ if (!this.disposed && !this.pausing) this.pump();
1060
+ });
1061
+ }
1062
+ async runJob(job) {
1063
+ job.state = "running";
1064
+ job.error = void 0;
1065
+ job.result = void 0;
1066
+ job.progress = void 0;
1067
+ job.conflict = void 0;
1068
+ job.cancelRequested = false;
1069
+ const aborter = new AbortController();
1070
+ job.abort = aborter;
1071
+ try {
1072
+ this.save(job);
1073
+ } catch (err) {
1074
+ job.state = "paused";
1075
+ job.error = toJobError(err);
1076
+ job.abort = void 0;
1077
+ this.emit();
1078
+ return;
1079
+ }
1080
+ this.emit();
1081
+ try {
1082
+ this.validateStored(job);
1083
+ } catch (err) {
1084
+ job.state = "paused";
1085
+ job.error = toJobError(err);
1086
+ job.abort = void 0;
1087
+ this.persistBestEffort(job);
1088
+ this.emit();
1089
+ return;
1090
+ }
1091
+ let workspace;
1092
+ try {
1093
+ workspace = new FileWorkspace(this.client, job.request.uuid);
1094
+ } catch (err) {
1095
+ job.state = "paused";
1096
+ job.error = toJobError(err);
1097
+ job.abort = void 0;
1098
+ this.persistBestEffort(job);
1099
+ this.emit();
1100
+ return;
1101
+ }
1102
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
1103
+ if (aborter.signal.aborted) break;
1104
+ try {
1105
+ const result = await workspace.transfer(
1106
+ job.request,
1107
+ {
1108
+ onProgress: (progress) => {
1109
+ job.progress = { ...progress };
1110
+ this.noteProgress();
1111
+ },
1112
+ onConflict: (conflict) => {
1113
+ job.state = "conflict";
1114
+ job.conflict = { ...conflict };
1115
+ try {
1116
+ this.save(job);
1117
+ } catch (err) {
1118
+ return Promise.reject(err);
1119
+ }
1120
+ this.emit();
1121
+ const conflictPromise = new Promise((resolve2, reject) => {
1122
+ const onAbort = () => {
1123
+ reject(new QueueAbortError());
1124
+ };
1125
+ if (aborter.signal.aborted) {
1126
+ onAbort();
1127
+ } else {
1128
+ this.conflicts.set(job.request.id, { resolve: resolve2, reject, onAbort });
1129
+ aborter.signal.addEventListener("abort", onAbort, { once: true });
1130
+ }
1131
+ });
1132
+ return conflictPromise.then((resolution) => {
1133
+ if (!aborter.signal.aborted && job.state === "conflict") {
1134
+ job.state = "running";
1135
+ job.conflict = void 0;
1136
+ this.emit();
1137
+ }
1138
+ return resolution;
1139
+ }).finally(() => {
1140
+ const pending = this.conflicts.get(job.request.id);
1141
+ if (pending) {
1142
+ aborter.signal.removeEventListener("abort", pending.onAbort);
1143
+ this.conflicts.delete(job.request.id);
1144
+ }
1145
+ });
1146
+ }
1147
+ },
1148
+ aborter.signal
1149
+ );
1150
+ job.state = "completed";
1151
+ job.result = result;
1152
+ job.conflict = void 0;
1153
+ job.abort = void 0;
1154
+ if (!this.persistBestEffort(job)) {
1155
+ job.error = job.error ?? "\u4F20\u8F93\u5B8C\u6210\uFF0C\u4F46\u961F\u5217\u6301\u4E45\u5316\u5931\u8D25\uFF0C\u91CD\u542F\u540E\u53EF\u80FD\u91CD\u590D\u4F20\u8F93";
1156
+ }
1157
+ this.emit();
1158
+ return;
1159
+ } catch (err) {
1160
+ if (aborter.signal.aborted || err instanceof QueueAbortError) {
1161
+ job.state = job.cancelRequested ? "cancelled" : "paused";
1162
+ job.conflict = void 0;
1163
+ job.abort = void 0;
1164
+ this.persistBestEffort(job);
1165
+ this.emit();
1166
+ return;
1167
+ }
1168
+ if (!isRecoverable(err) || attempt >= MAX_ATTEMPTS) {
1169
+ job.state = "paused";
1170
+ job.error = toJobError(err);
1171
+ job.conflict = void 0;
1172
+ job.abort = void 0;
1173
+ this.persistBestEffort(job);
1174
+ this.emit();
1175
+ return;
1176
+ }
1177
+ try {
1178
+ await abortableDelay(RETRY_DELAYS_MS[attempt - 1] ?? 5e3, aborter.signal);
1179
+ } catch {
1180
+ job.state = job.cancelRequested ? "cancelled" : "paused";
1181
+ job.conflict = void 0;
1182
+ job.abort = void 0;
1183
+ this.persistBestEffort(job);
1184
+ this.emit();
1185
+ return;
1186
+ }
1187
+ }
1188
+ }
1189
+ job.state = job.cancelRequested ? "cancelled" : "paused";
1190
+ job.conflict = void 0;
1191
+ job.abort = void 0;
1192
+ this.persistBestEffort(job);
1193
+ this.emit();
1194
+ }
1195
+ load() {
1196
+ let files;
1197
+ try {
1198
+ files = readdirSync(this.dir);
1199
+ } catch (err) {
1200
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
1201
+ return [];
1202
+ }
1203
+ throw new Error(`\u8BFB\u53D6\u4F20\u8F93\u961F\u5217\u5931\u8D25\uFF1A${toJobError(err)}`);
1204
+ }
1205
+ const loaded = [];
1206
+ for (const file of files) {
1207
+ if (!JOB_FILE_PATTERN.test(file)) continue;
1208
+ const full = join2(this.dir, file);
1209
+ let raw;
1210
+ try {
1211
+ raw = JSON.parse(readFileSync(full, "utf8"));
1212
+ } catch (err) {
1213
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u635F\u574F\uFF08${file}\uFF09\uFF1A${toJobError(err)}`);
1214
+ }
1215
+ const rec = validateStoredRecord(raw, file);
1216
+ loaded.push({
1217
+ request: rec.request,
1218
+ // Unfinished work always resumes paused; nothing autostarts.
1219
+ state: rec.state === "completed" || rec.state === "cancelled" || rec.state === "paused" ? rec.state : "paused",
1220
+ result: rec.result,
1221
+ error: rec.error,
1222
+ enqueuedAt: rec.enqueuedAt,
1223
+ cancelRequested: false,
1224
+ lastEmitAt: 0
1225
+ });
1226
+ }
1227
+ loaded.sort((a, b) => a.enqueuedAt - b.enqueuedAt);
1228
+ return loaded;
1229
+ }
1230
+ recordFor(job) {
1231
+ return {
1232
+ version: RECORD_VERSION,
1233
+ id: job.request.id,
1234
+ enqueuedAt: job.enqueuedAt,
1235
+ updatedAt: Date.now(),
1236
+ request: { ...job.request },
1237
+ state: job.state,
1238
+ ...job.error !== void 0 ? { error: job.error } : {},
1239
+ ...job.result ? { result: job.result } : {}
1240
+ };
1241
+ }
1242
+ save(job) {
1243
+ let payload;
1244
+ try {
1245
+ payload = JSON.stringify(this.recordFor(job));
1246
+ } catch (err) {
1247
+ throw new Error(`\u4F20\u8F93\u961F\u5217\u6301\u4E45\u5316\u5931\u8D25\uFF1A${toJobError(err)}`);
1248
+ }
1249
+ const tmp = join2(this.dir, `${job.request.id}.json.tmp.${process.pid}`);
1250
+ const final = join2(this.dir, `${job.request.id}.json`);
1251
+ try {
1252
+ mkdirSync(this.dir, { recursive: true, mode: 448 });
1253
+ writeFileSync(tmp, payload, { mode: 384 });
1254
+ renameSync(tmp, final);
1255
+ } catch (err) {
1256
+ try {
1257
+ rmSync(tmp, { force: true });
1258
+ } catch {
1259
+ }
1260
+ throw new Error(`\u4F20\u8F93\u961F\u5217\u6301\u4E45\u5316\u5931\u8D25\uFF1A${toJobError(err)}`);
1261
+ }
1262
+ }
1263
+ /** Background-transition persist: surface failures on the job, never throw. */
1264
+ persistBestEffort(job) {
1265
+ try {
1266
+ this.save(job);
1267
+ return true;
1268
+ } catch (err) {
1269
+ job.error = toJobError(err);
1270
+ return false;
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Claim exclusive mutation/run ownership for this namespace via fail-fast
1275
+ * bakery tickets over per-process unique claim files. Viewing, snapshotting
1276
+ * and subscribing never call this, so an empty view creates no files.
1277
+ *
1278
+ * Protocol: create `.owner-<pid>-<nonce>.lock` exclusively with ticket 0
1279
+ * (choosing), reap ONLY dead-pid unique names, fail busy if any live or
1280
+ * invalid claim exists, else publish max(ticket)+1 atomically and rescan:
1281
+ * win only when no live choosing claim remains and our (ticket, nonce) is
1282
+ * earliest; otherwise remove ONLY our own unique file and fail busy. A
1283
+ * crashed owner leaves an abandoned unique file its pid no longer resolves
1284
+ * for, so the next writer reaps just that immutable name without ever
1285
+ * deleting a live successor — no central stale rm race, no second reaper,
1286
+ * no external deps. Empty/partial writes parse fail-closed (invalid claim
1287
+ * blocks rather than being reaped). Same-process re-entry by one instance is
1288
+ * a no-op via `claimFile`; a second instance fails fast via liveOwners.
1289
+ */
1290
+ claimOwnership() {
1291
+ if (this.claimFile || this.disposed) return;
1292
+ if (liveOwners.has(this.dir)) {
1293
+ throw new Error(
1294
+ "\u540C\u4E00\u8FDB\u7A0B\u4E2D\u8BE5\u8D26\u53F7\u4F20\u8F93\u961F\u5217\u5DF2\u6709\u5199\u5165\u8005\uFF1A\u540C\u4E00\u65F6\u95F4\u53EA\u5141\u8BB8\u4E00\u4E2A\u961F\u5217\u5B9E\u4F8B\u5199\u5165\uFF0C\u907F\u514D\u5E76\u53D1\u6062\u590D\u540C\u4E00\u4EFB\u52A1\u635F\u574F\u4F20\u8F93\u76EE\u6807"
1295
+ );
1296
+ }
1297
+ const nonce = randomUUID();
1298
+ const file = `${OWNER_CLAIM_PREFIX}${process.pid}-${nonce}${OWNER_CLAIM_SUFFIX}`;
1299
+ const path = join2(this.dir, file);
1300
+ let fd;
1301
+ try {
1302
+ mkdirSync(this.dir, { recursive: true, mode: 448 });
1303
+ fd = openSync(path, "wx", 384);
1304
+ writeSync(fd, JSON.stringify({ pid: process.pid, nonce, ticket: 0, createdAt: Date.now() }));
1305
+ closeSync(fd);
1306
+ fd = void 0;
1307
+ } catch (err) {
1308
+ if (fd !== void 0) {
1309
+ try {
1310
+ closeSync(fd);
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ throw new Error(`\u4F20\u8F93\u961F\u5217\u52A0\u9501\u5931\u8D25\uFF1A${toJobError(err)}`);
1315
+ }
1316
+ reapDeadClaims(this.dir, listOwnerClaims(this.dir));
1317
+ const rivals = listOwnerClaims(this.dir).filter((claim) => claim.file !== file);
1318
+ const liveRival = rivals.find(
1319
+ (claim) => claim.ticket === void 0 || !Number.isInteger(claim.pid) || isLivePid(claim.pid)
1320
+ );
1321
+ if (liveRival) {
1322
+ removeOwnClaim(this.dir, file);
1323
+ throw new Error(
1324
+ Number.isInteger(liveRival.pid) ? `\u4F20\u8F93\u961F\u5217\u6B63\u7531\u8FDB\u7A0B ${liveRival.pid} \u72EC\u5360\u4F7F\u7528\uFF1A\u540C\u4E00\u8D26\u53F7\u961F\u5217\u540C\u4E00\u65F6\u95F4\u53EA\u5141\u8BB8\u4E00\u4E2A\u5199\u5165\u8005\uFF0C\u907F\u514D\u5E76\u53D1\u6062\u590D\u540C\u4E00\u4EFB\u52A1\u635F\u574F\u4F20\u8F93\u76EE\u6807` : "\u4F20\u8F93\u961F\u5217\u6B63\u7531\u53E6\u4E00\u5199\u5165\u8005\u72EC\u5360\u4F7F\u7528\uFF1A\u540C\u4E00\u8D26\u53F7\u961F\u5217\u540C\u4E00\u65F6\u95F4\u53EA\u5141\u8BB8\u4E00\u4E2A\u5199\u5165\u8005\uFF0C\u907F\u514D\u5E76\u53D1\u6062\u590D\u540C\u4E00\u4EFB\u52A1\u635F\u574F\u4F20\u8F93\u76EE\u6807"
1325
+ );
1326
+ }
1327
+ let ticket = 1;
1328
+ for (const claim of rivals) {
1329
+ if (claim.ticket !== void 0 && claim.ticket >= ticket) ticket = claim.ticket + 1;
1330
+ }
1331
+ const ticketPayload = JSON.stringify({
1332
+ pid: process.pid,
1333
+ nonce,
1334
+ ticket,
1335
+ createdAt: Date.now()
1336
+ });
1337
+ const ticketTmp = `${path}.tmp.${process.pid}`;
1338
+ try {
1339
+ writeFileSync(ticketTmp, ticketPayload, { mode: 384 });
1340
+ renameSync(ticketTmp, path);
1341
+ } catch (err) {
1342
+ try {
1343
+ rmSync(ticketTmp, { force: true });
1344
+ } catch {
1345
+ }
1346
+ removeOwnClaim(this.dir, file);
1347
+ throw new Error(`\u4F20\u8F93\u961F\u5217\u52A0\u9501\u5931\u8D25\uFF1A${toJobError(err)}`);
1348
+ }
1349
+ const after = listOwnerClaims(this.dir).filter(
1350
+ (claim) => Number.isInteger(claim.pid) && isLivePid(claim.pid)
1351
+ );
1352
+ if (after.some((claim) => claim.file !== file && claim.ticket === void 0)) {
1353
+ removeOwnClaim(this.dir, file);
1354
+ throw new Error(
1355
+ "\u4F20\u8F93\u961F\u5217\u6B63\u7531\u53E6\u4E00\u5199\u5165\u8005\u72EC\u5360\u4F7F\u7528\uFF1A\u540C\u4E00\u8D26\u53F7\u961F\u5217\u540C\u4E00\u65F6\u95F4\u53EA\u5141\u8BB8\u4E00\u4E2A\u5199\u5165\u8005\uFF0C\u907F\u514D\u5E76\u53D1\u6062\u590D\u540C\u4E00\u4EFB\u52A1\u635F\u574F\u4F20\u8F93\u76EE\u6807"
1356
+ );
1357
+ }
1358
+ const ranked = after.filter((claim) => claim.ticket !== void 0).sort(
1359
+ (a, b) => a.ticket - b.ticket || (a.nonce < b.nonce ? -1 : a.nonce > b.nonce ? 1 : 0)
1360
+ );
1361
+ const earliest = ranked[0];
1362
+ if (!earliest || earliest.file !== file) {
1363
+ removeOwnClaim(this.dir, file);
1364
+ const holder = earliest && Number.isInteger(earliest.pid) ? `\u8FDB\u7A0B ${earliest.pid}` : "\u53E6\u4E00\u5199\u5165\u8005";
1365
+ throw new Error(
1366
+ `\u4F20\u8F93\u961F\u5217\u6B63\u7531${holder}\u72EC\u5360\u4F7F\u7528\uFF1A\u540C\u4E00\u8D26\u53F7\u961F\u5217\u540C\u4E00\u65F6\u95F4\u53EA\u5141\u8BB8\u4E00\u4E2A\u5199\u5165\u8005\uFF0C\u907F\u514D\u5E76\u53D1\u6062\u590D\u540C\u4E00\u4EFB\u52A1\u635F\u574F\u4F20\u8F93\u76EE\u6807`
1367
+ );
1368
+ }
1369
+ this.claimFile = file;
1370
+ liveOwners.add(this.dir);
1371
+ }
1372
+ /** Release our unique claim; deletes ONLY our own immutable filename. */
1373
+ releaseOwnership() {
1374
+ if (!this.claimFile) return;
1375
+ const file = this.claimFile;
1376
+ this.claimFile = void 0;
1377
+ liveOwners.delete(this.dir);
1378
+ removeOwnClaim(this.dir, file);
1379
+ }
1380
+ /** Re-read and validate the disk record before any remote side effect. */
1381
+ validateStored(job) {
1382
+ const full = join2(this.dir, `${job.request.id}.json`);
1383
+ let raw;
1384
+ try {
1385
+ raw = JSON.parse(readFileSync(full, "utf8"));
1386
+ } catch (err) {
1387
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u4E0D\u53EF\u7528\uFF08${job.request.id}\uFF09\uFF1A${toJobError(err)}`);
1388
+ }
1389
+ const rec = validateStoredRecord(raw, `${job.request.id}.json`);
1390
+ const want = job.request;
1391
+ const got = rec.request;
1392
+ if (got.uuid !== want.uuid || got.direction !== want.direction || got.destination !== want.destination || got.sync !== want.sync || got.checksum !== want.checksum || got.sources.length !== want.sources.length || !got.sources.every((s, i) => s === want.sources[i])) {
1393
+ throw new Error(`\u4F20\u8F93\u8BB0\u5F55\u4E0E\u961F\u5217\u4E0D\u4E00\u81F4\uFF08${job.request.id}\uFF09\uFF0C\u5DF2\u505C\u6B62\u6267\u884C`);
1394
+ }
1395
+ }
1396
+ };
1397
+
1398
+ // src/tui/account.ts
1399
+ import { Buffer } from "buffer";
1400
+ function identityFromToken(token) {
1401
+ const empty = { uid: null, uuid: null, tenant: null };
1402
+ const parts = token.split(".");
1403
+ if (parts.length < 2) return empty;
1404
+ try {
1405
+ const segment = parts[1];
1406
+ const padded = segment + "=".repeat((4 - segment.length % 4) % 4);
1407
+ const payload = JSON.parse(Buffer.from(padded, "base64url").toString("utf8"));
1408
+ return {
1409
+ uid: typeof payload.uid === "number" ? payload.uid : null,
1410
+ uuid: typeof payload.uuid === "string" ? payload.uuid : null,
1411
+ tenant: typeof payload.tenant === "string" ? payload.tenant : null
1412
+ };
1413
+ } catch {
1414
+ return empty;
1415
+ }
1416
+ }
1417
+ function tokenOverrideNote(source) {
1418
+ if (source === "env")
1419
+ return "\u6CE8\u610F\uFF1A\u73AF\u5883\u53D8\u91CF AUTODL_TOKEN \u4ECD\u7136\u751F\u6548\uFF0C\u5176\u4F18\u5148\u7EA7\u9AD8\u4E8E\u8FD9\u91CC\u4FDD\u5B58\u7684 Token\u3002";
1420
+ if (source === "flag") return "\u6CE8\u610F\uFF1A\u672C\u6B21\u542F\u52A8\u5E26\u4E86 --token \u53C2\u6570\uFF0C\u5176\u4F18\u5148\u7EA7\u9AD8\u4E8E\u8FD9\u91CC\u4FDD\u5B58\u7684 Token\u3002";
1421
+ return null;
1422
+ }
1423
+
1424
+ // src/ssh/terminal.ts
1425
+ async function connectTerminal(client, uuid, options = {}) {
1426
+ const stdin = process.stdin;
1427
+ const stdout = process.stdout;
1428
+ if (!stdin.isTTY || !stdout.isTTY) {
1429
+ throw new UsageError("\u81EA\u52A8\u7EC8\u7AEF\u767B\u5F55\u9700\u8981\u771F\u5B9E\u4EA4\u4E92\u7EC8\u7AEF", {
1430
+ hint: "\u5728\u7BA1\u9053\u3001CI \u6216\u5176\u4ED6\u975E TTY \u73AF\u5883\u4E0B\u8BF7\u7528 `autodl exec` \u6267\u884C\u5355\u6761\u547D\u4EE4\u3002"
1431
+ });
1432
+ }
1433
+ assertNotAborted(options.signal);
1434
+ return withSSH(client, uuid, (conn) => runShellSession(conn, options.signal), options);
1435
+ }
1436
+ function runShellSession(conn, signal) {
1437
+ return new Promise((resolve2, reject) => {
1438
+ let settled = false;
1439
+ const cleanupPending = () => {
1440
+ signal?.removeEventListener("abort", onPendingAbort);
1441
+ conn.off("close", onConnClose);
1442
+ conn.off("error", onConnError);
1443
+ };
1444
+ const failPending = (err) => {
1445
+ if (settled) return;
1446
+ settled = true;
1447
+ cleanupPending();
1448
+ try {
1449
+ conn.end();
1450
+ } catch {
1451
+ }
1452
+ reject(err);
1453
+ };
1454
+ const onPendingAbort = () => {
1455
+ failPending(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1456
+ };
1457
+ const onConnClose = () => {
1458
+ failPending(
1459
+ new SSHError("SSH \u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u8FDC\u7A0B Shell \u672A\u80FD\u542F\u52A8", {
1460
+ hint: "\u5B9E\u4F8B\u53EF\u80FD\u6B63\u5728\u91CD\u542F\uFF0C\u6216\u7F51\u7EDC\u4E2D\u65AD\uFF1B\u7A0D\u540E\u91CD\u8BD5\u3002"
1461
+ })
1462
+ );
1463
+ };
1464
+ const onConnError = (err) => {
1465
+ failPending(new SSHError(`SSH \u8FDE\u63A5\u51FA\u9519\uFF1A${err.message}`, { cause: err }));
1466
+ };
1467
+ if (signal?.aborted) {
1468
+ failPending(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1469
+ return;
1470
+ }
1471
+ const stdout = process.stdout;
1472
+ const pty = {
1473
+ cols: stdout.columns ?? 80,
1474
+ rows: stdout.rows ?? 24,
1475
+ term: process.env.TERM || "xterm-256color"
1476
+ };
1477
+ signal?.addEventListener("abort", onPendingAbort, { once: true });
1478
+ conn.once("close", onConnClose);
1479
+ conn.once("error", onConnError);
1480
+ try {
1481
+ conn.shell(pty, (err, stream) => {
1482
+ if (settled) {
1483
+ try {
1484
+ stream?.close();
1485
+ } catch {
1486
+ }
1487
+ return;
1488
+ }
1489
+ cleanupPending();
1490
+ if (err || !stream) {
1491
+ settled = true;
1492
+ reject(
1493
+ new SSHError(
1494
+ `\u8FDC\u7A0B Shell \u542F\u52A8\u5931\u8D25\uFF1A${err?.message ?? "\u672A\u77E5\u9519\u8BEF"}`,
1495
+ {
1496
+ hint: "\u8FDE\u63A5\u672C\u8EAB\u5DF2\u5EFA\u7ACB\uFF0C\u4F46\u8FDC\u7AEF\u62D2\u7EDD\u5206\u914D Shell\uFF1B\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 `autodl exec` \u5148\u9A8C\u8BC1\u8FDE\u901A\u6027\u3002",
1497
+ cause: err
1498
+ }
1499
+ )
1500
+ );
1501
+ return;
1502
+ }
1503
+ if (signal?.aborted) {
1504
+ settled = true;
1505
+ try {
1506
+ stream.close();
1507
+ } catch {
1508
+ }
1509
+ reject(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1510
+ return;
1511
+ }
1512
+ driveSession(conn, stream, signal, resolve2, reject);
1513
+ });
1514
+ } catch (err) {
1515
+ failPending(
1516
+ new SSHError(`\u8FDC\u7A0B Shell \u8BF7\u6C42\u5931\u8D25\uFF1A${err?.message ?? String(err)}`, {
1517
+ cause: err
1518
+ })
1519
+ );
1520
+ }
1521
+ });
1522
+ }
1523
+ function driveSession(conn, stream, signal, resolve2, reject) {
1524
+ const stdin = process.stdin;
1525
+ const stdout = process.stdout;
1526
+ const wasRaw = stdin.isRaw;
1527
+ let settled = false;
1528
+ let exitCode = null;
1529
+ let exitSignal = null;
1530
+ let gotExit = false;
1531
+ let connCause;
1532
+ const cleanup = () => {
1533
+ stdin.off("data", onStdinData);
1534
+ stdin.off("end", onStdinGone);
1535
+ stdin.off("close", onStdinGone);
1536
+ stdin.off("error", onStdinError);
1537
+ stdin.pause();
1538
+ stream.off("data", onStdoutData);
1539
+ stream.stderr.off("data", onStdoutData);
1540
+ stream.off("error", onStreamError);
1541
+ stream.off("exit", onStreamExit);
1542
+ stream.off("close", onStreamClose);
1543
+ stream.off("drain", onStreamDrain);
1544
+ stdout.off("drain", onStdoutDrain);
1545
+ stdout.off("error", onStdoutError);
1546
+ stdout.off("close", onStdoutGone);
1547
+ stdout.off("resize", onResize);
1548
+ conn.off("error", onConnError);
1549
+ signal?.removeEventListener("abort", onAbort);
1550
+ if (stdin.isTTY) {
1551
+ try {
1552
+ stdin.setRawMode(wasRaw);
1553
+ } catch {
1554
+ }
1555
+ }
1556
+ };
1557
+ const done = (code) => {
1558
+ if (settled) return;
1559
+ settled = true;
1560
+ cleanup();
1561
+ resolve2(code);
1562
+ };
1563
+ const fail = (err) => {
1564
+ if (settled) return;
1565
+ settled = true;
1566
+ cleanup();
1567
+ try {
1568
+ stream.close();
1569
+ } catch {
1570
+ }
1571
+ reject(err);
1572
+ };
1573
+ const onStdinData = (chunk) => {
1574
+ if (settled) return;
1575
+ let ok = true;
1576
+ try {
1577
+ ok = stream.write(chunk);
1578
+ } catch (err) {
1579
+ fail(err instanceof Error ? err : new Error(String(err)));
1580
+ return;
1581
+ }
1582
+ if (!ok) stdin.pause();
1583
+ };
1584
+ const onStdinGone = () => {
1585
+ fail(
1586
+ new SSHError("\u672C\u5730\u7EC8\u7AEF\u8F93\u5165\u5DF2\u5173\u95ED\uFF0C\u8FDC\u7A0B Shell \u5DF2\u65AD\u5F00", {
1587
+ hint: "\u7EC8\u7AEF\u53EF\u80FD\u88AB\u5173\u95ED\uFF1B\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u540E\u518D\u8BD5\u3002"
1588
+ })
1589
+ );
1590
+ };
1591
+ const onStdinError = (err) => {
1592
+ fail(new SSHError(`\u672C\u5730\u7EC8\u7AEF\u8F93\u5165\u51FA\u9519\uFF1A${err.message}`, { cause: err }));
1593
+ };
1594
+ const onStreamDrain = () => {
1595
+ if (!settled) stdin.resume();
1596
+ };
1597
+ const onStdoutData = (chunk) => {
1598
+ if (settled) return;
1599
+ let ok = true;
1600
+ try {
1601
+ ok = stdout.write(chunk);
1602
+ } catch (err) {
1603
+ fail(err instanceof Error ? err : new Error(String(err)));
1604
+ return;
1605
+ }
1606
+ if (!ok) {
1607
+ stream.pause();
1608
+ stream.stderr.pause();
1609
+ }
1610
+ };
1611
+ const onStdoutDrain = () => {
1612
+ if (settled) return;
1613
+ stream.resume();
1614
+ stream.stderr.resume();
1615
+ };
1616
+ const onStdoutError = (err) => {
1617
+ fail(new SSHError(`\u672C\u5730\u7EC8\u7AEF\u8F93\u51FA\u51FA\u9519\uFF1A${err.message}`, { cause: err }));
1618
+ };
1619
+ const onStdoutGone = () => {
1620
+ fail(
1621
+ new SSHError("\u672C\u5730\u7EC8\u7AEF\u8F93\u51FA\u5DF2\u5173\u95ED\uFF0C\u8FDC\u7A0B Shell \u5DF2\u65AD\u5F00", {
1622
+ hint: "\u7EC8\u7AEF\u53EF\u80FD\u88AB\u5173\u95ED\uFF1B\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u540E\u518D\u8BD5\u3002"
1623
+ })
1624
+ );
1625
+ };
1626
+ const onResize = () => {
1627
+ if (settled) return;
1628
+ try {
1629
+ stream.setWindow(stdout.rows ?? 24, stdout.columns ?? 80, 0, 0);
1630
+ } catch {
1631
+ }
1632
+ };
1633
+ const onAbort = () => {
1634
+ fail(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1635
+ };
1636
+ const onConnError = (err) => {
1637
+ connCause = err;
1638
+ };
1639
+ const onStreamError = (err) => {
1640
+ fail(new SSHError(`\u8FDC\u7A0B Shell \u51FA\u9519\uFF1A${err.message}`, { cause: err }));
1641
+ };
1642
+ const onStreamExit = (code, signalName) => {
1643
+ gotExit = true;
1644
+ exitCode = typeof code === "number" ? code : null;
1645
+ exitSignal = signalName ?? null;
1646
+ };
1647
+ const onStreamClose = () => {
1648
+ if (gotExit) {
1649
+ done(exitCode ?? (exitSignal ? 128 : 0));
1650
+ return;
1651
+ }
1652
+ fail(
1653
+ new SSHError("\u8FDC\u7A0B\u8FDE\u63A5\u5DF2\u610F\u5916\u65AD\u5F00", {
1654
+ hint: "Shell \u672A\u6B63\u5E38\u9000\u51FA\uFF08\u5B9E\u4F8B\u91CD\u542F\u6216\u7F51\u7EDC\u4E2D\u65AD\u90FD\u53EF\u80FD\uFF09\uFF1B\u91CD\u8FDE\u540E\u68C0\u67E5\u8FDC\u7AEF\u8FDB\u7A0B\u72B6\u6001\u3002",
1655
+ cause: connCause
1656
+ })
1657
+ );
1658
+ };
1659
+ if (signal?.aborted) {
1660
+ try {
1661
+ stream.close();
1662
+ } catch {
1663
+ }
1664
+ reject(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1665
+ return;
1666
+ }
1667
+ stream.on("data", onStdoutData);
1668
+ stream.stderr.on("data", onStdoutData);
1669
+ stream.on("error", onStreamError);
1670
+ stream.once("exit", onStreamExit);
1671
+ stream.on("close", onStreamClose);
1672
+ stream.on("drain", onStreamDrain);
1673
+ conn.once("error", onConnError);
1674
+ stdout.on("drain", onStdoutDrain);
1675
+ stdout.on("error", onStdoutError);
1676
+ stdout.on("close", onStdoutGone);
1677
+ stdout.on("resize", onResize);
1678
+ stdin.on("error", onStdinError);
1679
+ stdin.on("end", onStdinGone);
1680
+ stdin.on("close", onStdinGone);
1681
+ stdin.on("data", onStdinData);
1682
+ signal?.addEventListener("abort", onAbort, { once: true });
1683
+ try {
1684
+ stdin.setRawMode(true);
1685
+ } catch (err) {
1686
+ fail(
1687
+ new SSHError(`\u65E0\u6CD5\u63A5\u7BA1\u672C\u5730\u7EC8\u7AEF\uFF1A${err?.message ?? String(err)}`, {
1688
+ hint: "\u7EC8\u7AEF\u53EF\u80FD\u5728\u8FDE\u63A5\u5EFA\u7ACB\u540E\u88AB\u5173\u95ED\uFF1B\u786E\u8BA4 stdin \u4ECD\u662F\u53EF\u7528 TTY \u540E\u91CD\u8BD5\u3002",
1689
+ cause: err
1690
+ })
1691
+ );
1692
+ return;
1693
+ }
1694
+ stdin.resume();
1695
+ }
1696
+
1697
+ export {
1698
+ FileWorkspace,
1699
+ TransferQueue,
1700
+ identityFromToken,
1701
+ tokenOverrideNote,
1702
+ connectTerminal
1703
+ };
1704
+ //# sourceMappingURL=chunk-2E2Y5GFH.js.map