@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.
@@ -976,6 +976,7 @@ var waitForRunning = (client, uuid, options2) => waitForStatus(client, uuid, ["r
976
976
  var waitForShutdown = (client, uuid, options2) => waitForStatus(client, uuid, ["shutdown"], options2);
977
977
 
978
978
  // src/ssh/credentials.ts
979
+ import { setTimeout as delay } from "timers/promises";
979
980
  import { Client } from "ssh2";
980
981
 
981
982
  // src/output/i18n.ts
@@ -1065,7 +1066,9 @@ function t(key) {
1065
1066
 
1066
1067
  // src/ssh/credentials.ts
1067
1068
  async function getCredentials(client, uuid, options2 = {}) {
1069
+ assertNotAborted(options2.signal);
1068
1070
  let status = await getInstanceStatus(client, uuid);
1071
+ assertNotAborted(options2.signal);
1069
1072
  if (status !== "running") {
1070
1073
  if (!options2.autoStart) {
1071
1074
  throw new SSHError(`\u5B9E\u4F8B ${uuid} \u5F53\u524D\u72B6\u6001\u4E3A "${status}"\uFF0C\u65E0\u6CD5\u5EFA\u7ACB SSH \u8FDE\u63A5`, {
@@ -1082,6 +1085,7 @@ async function getCredentials(client, uuid, options2 = {}) {
1082
1085
  });
1083
1086
  }
1084
1087
  if (status === "shutdown") {
1088
+ assertNotAborted(options2.signal);
1085
1089
  note(t("instance.poweringOn"));
1086
1090
  await powerOnInstance(client, uuid);
1087
1091
  }
@@ -1093,6 +1097,7 @@ async function getCredentials(client, uuid, options2 = {}) {
1093
1097
  });
1094
1098
  }
1095
1099
  const snapshot = await getInstanceSnapshot(client, uuid);
1100
+ assertNotAborted(options2.signal);
1096
1101
  const { host, port, password } = snapshot.ssh;
1097
1102
  if (!host || !port || !password) {
1098
1103
  throw new SSHError(`\u5B9E\u4F8B ${uuid} \u5C1A\u672A\u8FD4\u56DE\u5B8C\u6574\u7684 SSH \u4FE1\u606F`, {
@@ -1103,57 +1108,92 @@ async function getCredentials(client, uuid, options2 = {}) {
1103
1108
  return { uuid, host, port, user: "root", password };
1104
1109
  }
1105
1110
  var RETRY_DELAYS_MS = [2e3, 5e3, 8e3];
1106
- function delay(ms) {
1107
- return new Promise((resolve) => setTimeout(resolve, ms));
1108
- }
1109
- function connectOnce(creds, options2) {
1110
- return new Promise((resolve, reject) => {
1111
- const conn = new Client();
1112
- const settle = (err) => {
1113
- conn.removeAllListeners("ready");
1114
- conn.removeAllListeners("error");
1115
- if (err) reject(err);
1116
- else resolve(conn);
1117
- };
1118
- conn.once("ready", () => settle());
1119
- conn.once("error", (err) => {
1120
- conn.end();
1121
- settle(err);
1122
- });
1111
+ async function connectOnce(creds, options2) {
1112
+ assertNotAborted(options2.signal);
1113
+ const { promise, resolve, reject } = Promise.withResolvers();
1114
+ const conn = new Client();
1115
+ let settled = false;
1116
+ const settle = (err) => {
1117
+ if (settled) return;
1118
+ settled = true;
1119
+ conn.off("ready", onReady);
1120
+ if (err) {
1121
+ conn.once("close", () => conn.off("error", onError));
1122
+ } else {
1123
+ conn.off("error", onError);
1124
+ }
1125
+ conn.off("close", onClose);
1126
+ options2.signal?.removeEventListener("abort", onAbort);
1127
+ if (err) reject(err);
1128
+ else resolve(conn);
1129
+ };
1130
+ const onReady = () => settle();
1131
+ const onError = (err) => {
1132
+ if (settled) return;
1133
+ settle(err);
1134
+ conn.end();
1135
+ };
1136
+ const onClose = () => {
1137
+ settle(new SSHError("SSH \u8FDE\u63A5\u5728\u63E1\u624B\u5B8C\u6210\u524D\u5173\u95ED"));
1138
+ conn.off("error", onError);
1139
+ };
1140
+ const onAbort = () => {
1141
+ settle(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
1142
+ conn.destroy();
1143
+ };
1144
+ conn.once("ready", onReady);
1145
+ conn.on("error", onError);
1146
+ conn.once("close", onClose);
1147
+ options2.signal?.addEventListener("abort", onAbort, { once: true });
1148
+ try {
1123
1149
  conn.connect({
1124
1150
  host: creds.host,
1125
1151
  port: creds.port,
1126
1152
  username: creds.user,
1127
1153
  password: creds.password,
1128
1154
  readyTimeout: options2.connectTimeoutMs ?? 3e4,
1129
- keepaliveInterval: options2.keepaliveIntervalMs ?? 15e3,
1130
- // AutoDL's proxy hosts reuse addresses across instances, so a known_hosts
1131
- // check would fail constantly and teach users to ignore warnings.
1132
- algorithms: void 0
1155
+ keepaliveInterval: options2.keepaliveIntervalMs ?? 15e3
1133
1156
  });
1134
- });
1157
+ } catch (err) {
1158
+ onError(err instanceof Error ? err : new Error(String(err)));
1159
+ }
1160
+ return promise;
1135
1161
  }
1136
1162
  async function withSSH(client, uuid, fn, options2 = {}) {
1163
+ const conn = await connectSSH(client, uuid, options2);
1164
+ const creds = conn._creds;
1165
+ try {
1166
+ if (!creds) throw new SSHError("SSH \u8FDE\u63A5\u7F3A\u5C11\u51ED\u8BC1\u4E0A\u4E0B\u6587", { details: { uuid } });
1167
+ return await fn(conn, creds);
1168
+ } finally {
1169
+ delete conn._creds;
1170
+ conn.end();
1171
+ }
1172
+ }
1173
+ async function connectSSH(client, uuid, options2 = {}) {
1137
1174
  let lastError;
1138
1175
  const attempts = Math.max(1, options2.connectAttempts ?? 3);
1139
1176
  for (let attempt = 0; attempt < attempts; attempt++) {
1177
+ assertNotAborted(options2.signal);
1140
1178
  if (attempt > 0) {
1141
1179
  note(t("ssh.refreshing"));
1142
- await delay(RETRY_DELAYS_MS[attempt - 1] ?? RETRY_DELAYS_MS.at(-1) ?? 5e3);
1180
+ await delay(RETRY_DELAYS_MS[attempt - 1] ?? RETRY_DELAYS_MS.at(-1) ?? 5e3, void 0, {
1181
+ signal: options2.signal
1182
+ });
1143
1183
  }
1144
1184
  const creds = await getCredentials(client, uuid, options2);
1145
- let conn;
1185
+ assertNotAborted(options2.signal);
1146
1186
  try {
1147
- conn = await connectOnce(creds, options2);
1187
+ const conn = await connectOnce(creds, options2);
1188
+ conn._creds = creds;
1189
+ return conn;
1148
1190
  } catch (err) {
1191
+ assertNotAborted(options2.signal);
1192
+ if (err instanceof Error && "level" in err && err.level === "client-authentication") {
1193
+ throw new SSHError("SSH \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u83B7\u53D6\u5B9E\u4F8B\u51ED\u8BC1", { cause: err });
1194
+ }
1149
1195
  lastError = err;
1150
1196
  debug(`SSH \u8FDE\u63A5 ${creds.host}:${creds.port} \u5931\u8D25\uFF1A${err.message}`);
1151
- continue;
1152
- }
1153
- try {
1154
- return await fn(conn, creds);
1155
- } finally {
1156
- conn.end();
1157
1197
  }
1158
1198
  }
1159
1199
  throw new SSHError(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u5B9E\u4F8B ${uuid}\uFF08\u5DF2\u5C1D\u8BD5 ${attempts} \u6B21\uFF09`, {
@@ -1164,6 +1204,9 @@ async function withSSH(client, uuid, fn, options2 = {}) {
1164
1204
  function shellQuote(value) {
1165
1205
  return `'${value.replace(/'/g, `'\\''`)}'`;
1166
1206
  }
1207
+ function assertNotAborted(signal) {
1208
+ if (signal?.aborted) throw new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88");
1209
+ }
1167
1210
 
1168
1211
  // src/ssh/exec.ts
1169
1212
  function buildCommand(command, options2) {
@@ -1362,6 +1405,7 @@ export {
1362
1405
  SSHError,
1363
1406
  toAutoDLError,
1364
1407
  isAuthError,
1408
+ configDir,
1365
1409
  configPath,
1366
1410
  readConfig,
1367
1411
  writeConfig,
@@ -1420,6 +1464,8 @@ export {
1420
1464
  waitForShutdown,
1421
1465
  getCredentials,
1422
1466
  withSSH,
1467
+ connectSSH,
1468
+ assertNotAborted,
1423
1469
  execOnConnection,
1424
1470
  execCommand,
1425
1471
  buildTTLSnippet,
@@ -1429,4 +1475,4 @@ export {
1429
1475
  recordTTL,
1430
1476
  sweepExpired
1431
1477
  };
1432
- //# sourceMappingURL=chunk-6QR4ZYQR.js.map
1478
+ //# sourceMappingURL=chunk-GHJVQKKC.js.map
@@ -4,7 +4,7 @@ const require = __nodeCreateRequire(import.meta.url);
4
4
  import {
5
5
  createContext,
6
6
  runOpportunisticSweep
7
- } from "./chunk-NBWWTYSL.js";
7
+ } from "./chunk-VEDMEYCW.js";
8
8
  import {
9
9
  ExitCode,
10
10
  UsageError,
@@ -14,7 +14,7 @@ import {
14
14
  resolveLang,
15
15
  setLang,
16
16
  toAutoDLError
17
- } from "./chunk-6QR4ZYQR.js";
17
+ } from "./chunk-GHJVQKKC.js";
18
18
 
19
19
  // src/commands/helpers.ts
20
20
  import { confirm, isCancel } from "@clack/prompts";
@@ -112,7 +112,7 @@ function assertInteractive(json) {
112
112
  }
113
113
  async function launchTui(globals) {
114
114
  assertInteractive(globals.json === true);
115
- const { runTui } = await import("./app-3QXI65BE.js");
115
+ const { runTui } = await import("./app-V5GCG3MC.js");
116
116
  await runTui({
117
117
  ...globals.token ? { token: globals.token } : {},
118
118
  ...globals.baseUrl ? { baseUrl: globals.baseUrl } : {}
@@ -137,4 +137,4 @@ export {
137
137
  launchTui,
138
138
  registerTuiCommand
139
139
  };
140
- //# sourceMappingURL=chunk-DLCDJE6I.js.map
140
+ //# sourceMappingURL=chunk-JOOIUQLJ.js.map
@@ -15,10 +15,10 @@ import {
15
15
  parseCudaVersion,
16
16
  resolveGpuSpec,
17
17
  resolveRegion
18
- } from "./chunk-GJGHX55T.js";
18
+ } from "./chunk-WN2CXPD2.js";
19
19
  import {
20
20
  createContext
21
- } from "./chunk-NBWWTYSL.js";
21
+ } from "./chunk-VEDMEYCW.js";
22
22
  import {
23
23
  AutoDLError,
24
24
  BudgetError,
@@ -56,7 +56,7 @@ import {
56
56
  waitForShutdown,
57
57
  warn,
58
58
  withSSH
59
- } from "./chunk-6QR4ZYQR.js";
59
+ } from "./chunk-GHJVQKKC.js";
60
60
 
61
61
  // src/core/endpoints/image.ts
62
62
  async function saveImage(client, instanceUuid, imageName) {
@@ -1455,4 +1455,4 @@ export {
1455
1455
  connectInteractive,
1456
1456
  formatSSHCommand
1457
1457
  };
1458
- //# sourceMappingURL=chunk-BMKVXRT7.js.map
1458
+ //# sourceMappingURL=chunk-MWYYK755.js.map
@@ -10,7 +10,7 @@ import {
10
10
  resolveToken,
11
11
  setLang,
12
12
  sweepExpired
13
- } from "./chunk-6QR4ZYQR.js";
13
+ } from "./chunk-GHJVQKKC.js";
14
14
 
15
15
  // src/context.ts
16
16
  function createContext(options = {}) {
@@ -43,4 +43,4 @@ export {
43
43
  createContext,
44
44
  runOpportunisticSweep
45
45
  };
46
- //# sourceMappingURL=chunk-NBWWTYSL.js.map
46
+ //# sourceMappingURL=chunk-VEDMEYCW.js.map
@@ -9,7 +9,7 @@ import {
9
9
  note,
10
10
  warn,
11
11
  yuanToMilli
12
- } from "./chunk-6QR4ZYQR.js";
12
+ } from "./chunk-GHJVQKKC.js";
13
13
 
14
14
  // src/core/catalog.ts
15
15
  var GPU_SPECS = [
@@ -435,4 +435,4 @@ export {
435
435
  chooseRegions,
436
436
  VERSION
437
437
  };
438
- //# sourceMappingURL=chunk-GJGHX55T.js.map
438
+ //# sourceMappingURL=chunk-WN2CXPD2.js.map
package/dist/cli.js CHANGED
@@ -13,7 +13,13 @@ import {
13
13
  saveImage,
14
14
  startMcpServer,
15
15
  watchIdle
16
- } from "./chunk-BMKVXRT7.js";
16
+ } from "./chunk-MWYYK755.js";
17
+ import {
18
+ FileWorkspace,
19
+ TransferQueue,
20
+ connectTerminal,
21
+ identityFromToken
22
+ } from "./chunk-2E2Y5GFH.js";
17
23
  import {
18
24
  BASE_IMAGES,
19
25
  DEFAULT_BASE_IMAGE,
@@ -29,7 +35,7 @@ import {
29
35
  parseCudaVersion,
30
36
  resolveGpuSpec,
31
37
  resolveRegion
32
- } from "./chunk-GJGHX55T.js";
38
+ } from "./chunk-WN2CXPD2.js";
33
39
  import {
34
40
  action,
35
41
  bareAction,
@@ -37,10 +43,11 @@ import {
37
43
  globalsOf,
38
44
  lazyAction,
39
45
  registerTuiCommand
40
- } from "./chunk-DLCDJE6I.js";
41
- import "./chunk-NBWWTYSL.js";
46
+ } from "./chunk-JOOIUQLJ.js";
47
+ import "./chunk-VEDMEYCW.js";
42
48
  import {
43
49
  AutoDLClient,
50
+ DEFAULT_BASE_URL,
44
51
  ExitCode,
45
52
  UsageError,
46
53
  armTTLOverSSH,
@@ -76,7 +83,9 @@ import {
76
83
  recordTTL,
77
84
  redactToken,
78
85
  releaseInstance,
86
+ resolveBaseUrl,
79
87
  resolveLang,
88
+ resolveToken,
80
89
  setLang,
81
90
  success,
82
91
  sweepExpired,
@@ -87,7 +96,7 @@ import {
87
96
  waitForRunning,
88
97
  waitForShutdown,
89
98
  warn
90
- } from "./chunk-6QR4ZYQR.js";
99
+ } from "./chunk-GHJVQKKC.js";
91
100
 
92
101
  // src/cli.ts
93
102
  import { Command } from "commander";
@@ -356,6 +365,250 @@ function registerDeployCommand(program) {
356
365
  );
357
366
  }
358
367
 
368
+ // src/commands/files.ts
369
+ import { createHash } from "crypto";
370
+ function queueNamespace(token, baseUrl) {
371
+ const identity = identityFromToken(token);
372
+ const account = [identity.tenant, identity.uid ?? identity.uuid ?? token];
373
+ return createHash("sha256").update(JSON.stringify([(baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""), account])).digest("hex");
374
+ }
375
+ function queueFor(context, command) {
376
+ const globals = globalsOf(command);
377
+ const resolved = resolveToken(globals.token);
378
+ const baseUrl = resolveBaseUrl(globals.baseUrl);
379
+ const queue = new TransferQueue(context.client, queueNamespace(resolved.token, baseUrl));
380
+ let released = false;
381
+ return {
382
+ queue,
383
+ release: async () => {
384
+ if (!released) {
385
+ released = true;
386
+ await queue.dispose();
387
+ }
388
+ }
389
+ };
390
+ }
391
+ async function withWorkspace(context, id, fn) {
392
+ const workspace = new FileWorkspace(context.client, id);
393
+ try {
394
+ return await fn(workspace);
395
+ } finally {
396
+ workspace.dispose();
397
+ }
398
+ }
399
+ async function ensureStarted(context, id, start) {
400
+ if (start) await getCredentials(context.client, id, { autoStart: true });
401
+ }
402
+ var STATE_LABEL = {
403
+ queued: "\u6392\u961F",
404
+ running: "\u4F20\u8F93\u4E2D",
405
+ conflict: "\u5F85\u786E\u8BA4",
406
+ paused: "\u5DF2\u6682\u505C",
407
+ completed: "\u5B8C\u6210",
408
+ cancelled: "\u5DF2\u53D6\u6D88"
409
+ };
410
+ function registerFileCommands(program) {
411
+ const files = program.command("files").description("\u6D4F\u89C8\u4E0E\u7BA1\u7406\u5B9E\u4F8B\u6587\u4EF6\uFF08SFTP\uFF0C\u8FDE\u63A5\u590D\u7528\uFF09");
412
+ files.command("ls <id> [remote]").description("\u5217\u51FA\u8FDC\u7A0B\u76EE\u5F55\uFF08\u5F53\u524D\u5C42\uFF0C\u65E0\u9012\u5F52\uFF09").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).action(
413
+ action(
414
+ async (context, id, remote, options) => {
415
+ const dir = remote ?? "/root/autodl-tmp";
416
+ const entries = await withWorkspace(context, id, async (workspace) => {
417
+ await ensureStarted(context, id, options.start);
418
+ return workspace.list("remote", dir);
419
+ });
420
+ emit({ uuid: id, path: dir, entries }, () => {
421
+ for (const entry of entries) {
422
+ printKeyValues([[entry.name, `${entry.kind} ${entry.size} ${entry.path}`]]);
423
+ }
424
+ if (entries.length === 0) note("\u7A7A\u76EE\u5F55");
425
+ });
426
+ return 0;
427
+ }
428
+ )
429
+ );
430
+ files.command("mkdir <id> <remote>").description("\u5728\u5B9E\u4F8B\u4E0A\u521B\u5EFA\u76EE\u5F55").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).action(
431
+ action(async (context, id, remote, options) => {
432
+ await withWorkspace(context, id, async (workspace) => {
433
+ await ensureStarted(context, id, options.start);
434
+ return workspace.mkdir("remote", remote);
435
+ });
436
+ emit({ uuid: id, path: remote, created: true }, () => success(`\u5DF2\u521B\u5EFA ${remote}`));
437
+ return 0;
438
+ })
439
+ );
440
+ files.command("mv <id> <from> <to>").description("\u5728\u5B9E\u4F8B\u4E0A\u6539\u540D\u6216\u79FB\u52A8\uFF08\u4E0D\u8986\u76D6\u5DF2\u5B58\u5728\u76EE\u6807\uFF09").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).action(
441
+ action(async (context, id, from, to, options) => {
442
+ await withWorkspace(context, id, async (workspace) => {
443
+ await ensureStarted(context, id, options.start);
444
+ return workspace.rename("remote", from, to);
445
+ });
446
+ emit({ uuid: id, from, to, moved: true }, () => success(`\u5DF2\u79FB\u52A8 ${from} \u2192 ${to}`));
447
+ return 0;
448
+ })
449
+ );
450
+ files.command("rm <id> <remote>").description("\u5220\u9664\u5B9E\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55\uFF08\u9012\u5F52\uFF0C\u4E0D\u53EF\u6062\u590D\uFF09").option("--yes", "\u786E\u8BA4\u5220\u9664", false).option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).action(
451
+ action(
452
+ async (context, id, remote, options) => {
453
+ const confirmed = await confirmDestructive(
454
+ `\u6C38\u4E45\u5220\u9664\u5B9E\u4F8B\u6587\u4EF6 ${id}:${remote}`,
455
+ options.yes
456
+ );
457
+ if (!confirmed) {
458
+ emit({ uuid: id, path: remote, removed: false, cancelled: true }, () => note("\u5DF2\u53D6\u6D88"));
459
+ return 0;
460
+ }
461
+ await withWorkspace(context, id, async (workspace) => {
462
+ await ensureStarted(context, id, options.start);
463
+ return workspace.remove("remote", remote);
464
+ });
465
+ emit({ uuid: id, path: remote, removed: true }, () => success(`\u5DF2\u5220\u9664 ${remote}`));
466
+ return 0;
467
+ }
468
+ )
469
+ );
470
+ const queue = program.command("queue").description("\u4E32\u884C\u4F20\u8F93\u961F\u5217\uFF08\u4E0E\u770B\u677F\u5171\u7528\u540C\u4E00\u961F\u5217\uFF09");
471
+ queue.command("add <id> <source> <destination>").description("\u52A0\u5165\u4E0A\u4F20/\u4E0B\u8F7D\u4EFB\u52A1\uFF08\u6765\u6E90\u5728\u672C\u5730\u5219\u4E0A\u4F20\uFF0C\u5728\u8FDC\u7AEF\u9700 --download\uFF09").option("--download", "\u4ECE\u5B9E\u4F8B\u4E0B\u8F7D\u5230\u672C\u5730", false).option("--sync", "\u5355\u5411\u589E\u91CF\u540C\u6B65\uFF0C\u4E0D\u5220\u9664\u76EE\u6807\u72EC\u6709\u6587\u4EF6", false).option("--checksum", "SHA-256 \u5185\u5BB9\u6821\u9A8C", false).option("--wait", "\u7B49\u5F85\u4EFB\u52A1\u5B8C\u6210", false).action(
472
+ action(
473
+ async (context, id, source, destination, options, command) => {
474
+ const { queue: transferQueue, release } = queueFor(context, command);
475
+ try {
476
+ const jobId = transferQueue.enqueue({
477
+ uuid: id,
478
+ direction: options.download ? "download" : "upload",
479
+ sources: [source],
480
+ destination,
481
+ sync: options.sync,
482
+ checksum: options.checksum
483
+ });
484
+ if (options.wait) {
485
+ const done = await waitForJob(transferQueue, jobId);
486
+ emit(
487
+ { jobId, ...done },
488
+ () => success(`\u4EFB\u52A1 ${jobId}\uFF1A${STATE_LABEL[done.state] ?? done.state}`)
489
+ );
490
+ return 0;
491
+ }
492
+ emit({ jobId, uuid: id }, () => success(`\u5DF2\u52A0\u5165\u961F\u5217\uFF1A${jobId}`));
493
+ return 0;
494
+ } finally {
495
+ await release();
496
+ }
497
+ }
498
+ )
499
+ );
500
+ queue.command("ls").description("\u5217\u51FA\u5F53\u524D\u8D26\u53F7\u7684\u4F20\u8F93\u4EFB\u52A1").action(
501
+ action(async (context, _options, command) => {
502
+ const { queue: transferQueue, release } = queueFor(context, command);
503
+ try {
504
+ const jobs = transferQueue.snapshot().map((job) => ({
505
+ id: job.request.id,
506
+ uuid: job.request.uuid,
507
+ direction: job.request.direction,
508
+ sources: job.request.sources,
509
+ destination: job.request.destination,
510
+ sync: job.request.sync,
511
+ checksum: job.request.checksum,
512
+ state: job.state,
513
+ ...job.error !== void 0 ? { error: job.error } : {},
514
+ ...job.result ? { result: job.result } : {}
515
+ }));
516
+ emit({ jobs }, () => {
517
+ if (jobs.length === 0) {
518
+ note("\u961F\u5217\u4E3A\u7A7A");
519
+ return;
520
+ }
521
+ for (const job of jobs) {
522
+ printKeyValues([
523
+ [
524
+ job.id,
525
+ `${job.direction} ${STATE_LABEL[job.state] ?? job.state} \u2192 ${job.destination}`
526
+ ]
527
+ ]);
528
+ }
529
+ });
530
+ return 0;
531
+ } finally {
532
+ await release();
533
+ }
534
+ })
535
+ );
536
+ queue.command("resume <jobId>").description("\u6062\u590D\u5DF2\u6682\u505C/\u5DF2\u53D6\u6D88\u7684\u4EFB\u52A1\uFF08\u9700\u5B9E\u4F8B\u8FD0\u884C\u4E2D\uFF0C\u4E0D\u81EA\u52A8\u5F00\u673A\uFF09").action(
537
+ action(async (context, jobId, _options, command) => {
538
+ const { queue: transferQueue, release } = queueFor(context, command);
539
+ try {
540
+ transferQueue.resume(jobId);
541
+ emit({ jobId, resumed: true }, () => success(`\u5DF2\u6062\u590D ${jobId}`));
542
+ return 0;
543
+ } finally {
544
+ await release();
545
+ }
546
+ })
547
+ );
548
+ queue.command("cancel <jobId>").description("\u53D6\u6D88\u6392\u961F/\u4F20\u8F93\u4E2D\u7684\u4EFB\u52A1\uFF08\u4FDD\u7559\u7EED\u4F20\u6570\u636E\uFF09").action(
549
+ action(async (context, jobId, _options, command) => {
550
+ const { queue: transferQueue, release } = queueFor(context, command);
551
+ try {
552
+ transferQueue.cancel(jobId);
553
+ emit({ jobId, cancelled: true }, () => success(`\u5DF2\u53D6\u6D88 ${jobId}`));
554
+ return 0;
555
+ } finally {
556
+ await release();
557
+ }
558
+ })
559
+ );
560
+ queue.command("resolve <jobId> <choice>").description("\u5904\u7406\u4EFB\u52A1\u7684\u6587\u4EF6\u51B2\u7A81\uFF1Aoverwrite / skip / keep-both").option("--all", "\u5E94\u7528\u4E8E\u672C\u4EFB\u52A1\u5176\u4F59\u51B2\u7A81", false).action(
561
+ action(
562
+ async (context, jobId, choice, options, command) => {
563
+ if (choice !== "overwrite" && choice !== "skip" && choice !== "keep-both") {
564
+ throw new UsageError("choice \u5FC5\u987B\u662F overwrite / skip / keep-both");
565
+ }
566
+ const { queue: transferQueue, release } = queueFor(context, command);
567
+ try {
568
+ transferQueue.resolveConflict(jobId, { choice, applyToAll: options.all });
569
+ emit(
570
+ { jobId, choice, applyToAll: options.all },
571
+ () => success(`\u5DF2\u5904\u7406\u51B2\u7A81\uFF1A${choice}`)
572
+ );
573
+ return 0;
574
+ } finally {
575
+ await release();
576
+ }
577
+ }
578
+ )
579
+ );
580
+ }
581
+ async function waitForJob(queue, jobId) {
582
+ const { promise, resolve } = Promise.withResolvers();
583
+ const done = (state, error) => resolve(error === void 0 ? { state } : { state, error });
584
+ const check = () => {
585
+ const job = queue.snapshot().find((entry) => entry.request.id === jobId);
586
+ if (!job) {
587
+ done("missing", "\u4EFB\u52A1\u4E0D\u5B58\u5728");
588
+ return true;
589
+ }
590
+ if (job.state === "completed" || job.state === "cancelled") {
591
+ done(job.state, job.error);
592
+ return true;
593
+ }
594
+ if (job.state === "paused") {
595
+ done(job.state, job.error ?? "\u4EFB\u52A1\u5DF2\u6682\u505C");
596
+ return true;
597
+ }
598
+ if (job.state === "conflict" && isJson()) {
599
+ done(job.state, job.error ?? "\u4EFB\u52A1\u7B49\u5F85\u51B2\u7A81\u786E\u8BA4\uFF08\u975E\u4EA4\u4E92\u6A21\u5F0F\u65E0\u6CD5\u56DE\u7B54\uFF09");
600
+ return true;
601
+ }
602
+ return false;
603
+ };
604
+ if (check()) return promise;
605
+ const unsubscribe = queue.subscribe(() => {
606
+ if (check()) unsubscribe();
607
+ });
608
+ if (check()) unsubscribe();
609
+ return promise;
610
+ }
611
+
359
612
  // src/commands/guard.ts
360
613
  import pc4 from "picocolors";
361
614
  function registerGuardCommands(program) {
@@ -803,24 +1056,37 @@ function registerRunCommand(program) {
803
1056
  // src/commands/ssh.ts
804
1057
  import pc6 from "picocolors";
805
1058
  function registerSSHCommands(program) {
806
- program.command("ssh <id>").description("\u4EA4\u4E92\u5F0F SSH \u767B\u5F55\u5B9E\u4F8B").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).option("--print", "\u53EA\u6253\u5370\u8FDE\u63A5\u4FE1\u606F\uFF0C\u4E0D\u5EFA\u7ACB\u8FDE\u63A5", false).allowUnknownOption().action(
807
- action(async (context, id, options, command) => {
808
- if (options.print) {
809
- const creds = await getCredentials(context.client, id, { autoStart: options.start });
810
- emit(creds, () => {
811
- printKeyValues([
812
- ["\u547D\u4EE4", `ssh -p ${creds.port} ${creds.user}@${creds.host}`],
813
- ["\u5BC6\u7801", creds.password]
814
- ]);
1059
+ program.command("ssh <id>").description("\u4EA4\u4E92\u5F0F SSH \u767B\u5F55\u5B9E\u4F8B").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).option("--print", "\u53EA\u6253\u5370\u8FDE\u63A5\u4FE1\u606F\uFF0C\u4E0D\u5EFA\u7ACB\u8FDE\u63A5", false).option("--auto-auth", "\u4F7F\u7528\u5B9E\u4F8B\u5BC6\u7801\u81EA\u52A8\u8BA4\u8BC1\uFF08\u72EC\u5360\u5F53\u524D\u7EC8\u7AEF\uFF0C\u4E0D\u8BFB\u53D6 OpenSSH \u914D\u7F6E\uFF09", false).allowUnknownOption().action(
1060
+ action(
1061
+ async (context, id, options, command) => {
1062
+ if (options.print) {
1063
+ const creds = await getCredentials(context.client, id, { autoStart: options.start });
1064
+ emit(creds, () => {
1065
+ printKeyValues([
1066
+ ["\u547D\u4EE4", `ssh -p ${creds.port} ${creds.user}@${creds.host}`],
1067
+ ["\u5BC6\u7801", creds.password]
1068
+ ]);
1069
+ });
1070
+ return 0;
1071
+ }
1072
+ const extraArgs = command.args.slice(1);
1073
+ if (options.autoAuth) {
1074
+ if (isJson() || !process.stdin.isTTY || !process.stdout.isTTY) {
1075
+ throw new UsageError("--auto-auth \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u4E0D\u652F\u6301 --json");
1076
+ }
1077
+ if (extraArgs.length) {
1078
+ throw new UsageError(
1079
+ "--auto-auth \u4E0D\u63A5\u53D7 OpenSSH \u53C2\u6570\uFF1B\u7AEF\u53E3\u8F6C\u53D1\u7B49\u573A\u666F\u8BF7\u4F7F\u7528\u666E\u901A autodl ssh"
1080
+ );
1081
+ }
1082
+ return connectTerminal(context.client, id, { autoStart: options.start });
1083
+ }
1084
+ return connectInteractive(context.client, id, {
1085
+ autoStart: options.start,
1086
+ extraArgs
815
1087
  });
816
- return 0;
817
1088
  }
818
- const extraArgs = command.args.slice(1);
819
- return connectInteractive(context.client, id, {
820
- autoStart: options.start,
821
- extraArgs
822
- });
823
- })
1089
+ )
824
1090
  );
825
1091
  program.command("exec <id> <command...>").description("\u5728\u5B9E\u4F8B\u4E0A\u6267\u884C\u547D\u4EE4\uFF0C\u6D41\u5F0F\u56DE\u4F20\u8F93\u51FA\uFF0C\u900F\u4F20\u8FDC\u7A0B\u9000\u51FA\u7801").option("--start", "\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u81EA\u52A8\u5F00\u673A", false).option("--cwd <dir>", "\u8FDC\u7A0B\u5DE5\u4F5C\u76EE\u5F55").option("--timeout <duration>", "\u547D\u4EE4\u8D85\u65F6\u65F6\u95F4\uFF0C\u5982 30m").option("--env <key=value...>", "\u6CE8\u5165\u73AF\u5883\u53D8\u91CF").option("--pty", "\u5206\u914D\u4F2A\u7EC8\u7AEF\uFF08\u9700\u8981 isatty \u7684\u7A0B\u5E8F\u7528\u5F97\u4E0A\uFF09", false).action(
826
1092
  action(
@@ -983,6 +1249,7 @@ function buildProgram() {
983
1249
  registerAccountCommands(program);
984
1250
  registerInstanceCommands(program);
985
1251
  registerSSHCommands(program);
1252
+ registerFileCommands(program);
986
1253
  registerRunCommand(program);
987
1254
  registerDeployCommand(program);
988
1255
  registerGuardCommands(program);
@@ -1021,7 +1288,7 @@ function shouldLaunchBareTui(argv) {
1021
1288
  async function main(argv = process.argv) {
1022
1289
  if (shouldLaunchBareTui(argv)) {
1023
1290
  try {
1024
- const { launchTui } = await import("./tui-2JGNYYRA.js");
1291
+ const { launchTui } = await import("./tui-ZT5ZBVSQ.js");
1025
1292
  await launchTui({});
1026
1293
  return;
1027
1294
  } catch (err) {