scp-next 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1354 +1,1306 @@
1
- // src/client/client.ts
2
- import { access as access4, mkdir as mkdir2, stat as stat3 } from "fs/promises";
3
- import path4 from "path";
1
+ import { stat, access, mkdir, readFile, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import SftpClient from 'ssh2-sftp-client';
4
+ import { createHash } from 'node:crypto';
5
+ import { constants } from 'node:fs';
6
+ import os from 'node:os';
7
+ import { Client } from 'ssh2';
4
8
 
5
- // src/client/transport.ts
6
- import SftpClient from "ssh2-sftp-client";
7
-
8
- // src/security/redact.ts
9
- var SENSITIVE_KEYS = [
10
- "password",
11
- "passphrase",
12
- "privatekey",
13
- "privatekeyfile",
14
- "token",
15
- "authorization",
16
- "credential",
17
- "secret"
9
+ const SENSITIVE_KEYS = [
10
+ "password",
11
+ "passphrase",
12
+ "privatekey",
13
+ "privatekeyfile",
14
+ "token",
15
+ "authorization",
16
+ "credential",
17
+ "secret"
18
18
  ];
19
- var PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
20
- var SECRET_OPTION_PATTERN = /(--(?:password|passphrase|token|secret|authorization|credential)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi;
21
- var SECRET_ASSIGNMENT_PATTERN = /(\b(?:password|passphrase|token|secret|authorization|credential)[A-Z0-9_-]*\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
19
+ const PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
20
+ const SECRET_OPTION_PATTERN = /(--(?:password|passphrase|token|secret|authorization|credential)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi;
21
+ const SECRET_ASSIGNMENT_PATTERN = /(\b(?:password|passphrase|token|secret|authorization|credential)[A-Z0-9_-]*\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
22
22
  function isSensitiveKey(key) {
23
- const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
24
- return SENSITIVE_KEYS.some((sensitive) => normalized.includes(sensitive));
23
+ const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
24
+ return SENSITIVE_KEYS.some((sensitive) => normalized.includes(sensitive));
25
25
  }
26
26
  function redactSensitiveValues(value) {
27
- if (typeof value === "string") {
28
- return value.replace(PRIVATE_KEY_PATTERN, "[REDACTED]");
29
- }
30
- if (Array.isArray(value)) {
31
- const redacted = value.map((item) => redactSensitiveValues(item));
32
- return redacted;
33
- }
34
- if (value && typeof value === "object") {
35
- const redacted = {};
36
- for (const [key, nestedValue] of Object.entries(value)) {
37
- redacted[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveValues(nestedValue);
38
- }
39
- return redacted;
40
- }
41
- return value;
27
+ if (typeof value === "string") {
28
+ return value.replace(PRIVATE_KEY_PATTERN, "[REDACTED]");
29
+ }
30
+ if (Array.isArray(value)) {
31
+ const redacted = value.map((item) => redactSensitiveValues(item));
32
+ return redacted;
33
+ }
34
+ if (value && typeof value === "object") {
35
+ const redacted = {};
36
+ for (const [key, nestedValue] of Object.entries(value)) {
37
+ redacted[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveValues(nestedValue);
38
+ }
39
+ return redacted;
40
+ }
41
+ return value;
42
42
  }
43
43
  function formatErrorMessage(error) {
44
- if (error instanceof Error) {
45
- return redactKnownSensitiveValues(error.message, {});
46
- }
47
- return redactKnownSensitiveValues(String(error), {});
44
+ if (error instanceof Error) {
45
+ return redactKnownSensitiveValues(error.message, {});
46
+ }
47
+ return redactKnownSensitiveValues(String(error), {});
48
48
  }
49
49
  function redactKnownSensitiveValues(text, source) {
50
- const sensitiveValues = collectSensitiveValues(source);
51
- const redactedKnownValues = sensitiveValues.reduce(
52
- (redacted, value) => redacted.split(value).join("[REDACTED]"),
53
- String(redactSensitiveValues(text))
54
- );
55
- return redactedKnownValues.replace(SECRET_OPTION_PATTERN, "$1[REDACTED]").replace(SECRET_ASSIGNMENT_PATTERN, "$1[REDACTED]");
50
+ const sensitiveValues = collectSensitiveValues(source);
51
+ const redactedKnownValues = sensitiveValues.reduce((redacted, value) => redacted.split(value).join("[REDACTED]"), String(redactSensitiveValues(text)));
52
+ return redactedKnownValues
53
+ .replace(SECRET_OPTION_PATTERN, "$1[REDACTED]")
54
+ .replace(SECRET_ASSIGNMENT_PATTERN, "$1[REDACTED]");
56
55
  }
57
56
  function collectSensitiveValues(value, key = "") {
58
- if (isSensitiveKey(key)) {
59
- if (typeof value === "string" && value) {
60
- return [value];
61
- }
62
- if (Buffer.isBuffer(value) && value.length > 0) {
63
- return [value.toString("utf8")];
64
- }
65
- return [];
66
- }
67
- if (!value || typeof value !== "object") {
68
- return [];
69
- }
70
- return Object.entries(value).flatMap(
71
- ([nestedKey, nestedValue]) => collectSensitiveValues(nestedValue, nestedKey)
72
- );
57
+ if (isSensitiveKey(key)) {
58
+ if (typeof value === "string" && value) {
59
+ return [value];
60
+ }
61
+ if (Buffer.isBuffer(value) && value.length > 0) {
62
+ return [value.toString("utf8")];
63
+ }
64
+ return [];
65
+ }
66
+ if (!value || typeof value !== "object") {
67
+ return [];
68
+ }
69
+ return Object.entries(value).flatMap(([nestedKey, nestedValue]) => collectSensitiveValues(nestedValue, nestedKey));
73
70
  }
74
71
 
75
- // src/errors/index.ts
76
- var ScpNextError = class extends Error {
77
- code;
78
- context;
79
- constructor(message, options = {}) {
80
- super(message, { cause: options.cause });
81
- this.name = new.target.name;
82
- this.code = options.code ?? "SCP_NEXT_ERROR";
83
- this.context = redactSensitiveValues(options.context ?? {});
84
- }
85
- };
86
- var ConfigurationError = class extends ScpNextError {
87
- constructor(message, options = {}) {
88
- super(message, { ...options, code: "SCP_NEXT_CONFIGURATION_ERROR" });
89
- }
90
- };
91
- var ValidationError = class extends ScpNextError {
92
- constructor(message, options = {}) {
93
- super(message, { ...options, code: "SCP_NEXT_VALIDATION_ERROR" });
94
- }
95
- };
96
- var AuthenticationError = class extends ScpNextError {
97
- constructor(message, options = {}) {
98
- super(message, { ...options, code: "SCP_NEXT_AUTHENTICATION_ERROR" });
99
- }
100
- };
101
- var ConnectionError = class extends ScpNextError {
102
- constructor(message, options = {}) {
103
- super(message, { ...options, code: "SCP_NEXT_CONNECTION_ERROR" });
104
- }
105
- };
106
- var TransferError = class extends ScpNextError {
107
- constructor(message, options = {}) {
108
- super(message, { ...options, code: "SCP_NEXT_TRANSFER_ERROR" });
109
- }
110
- };
111
- var RemoteCommandError = class extends ScpNextError {
112
- exitCode;
113
- signal;
114
- stdout;
115
- stderr;
116
- constructor(message, options) {
117
- super(message, {
118
- code: "SCP_NEXT_REMOTE_COMMAND_ERROR",
119
- ...options.cause !== void 0 ? { cause: options.cause } : {},
120
- ...options.context !== void 0 ? { context: options.context } : {}
121
- });
122
- this.exitCode = options.exitCode;
123
- this.signal = options.signal;
124
- this.stdout = options.stdout ?? "";
125
- this.stderr = options.stderr ?? "";
126
- }
127
- };
128
- var FileSystemError = class extends ScpNextError {
129
- constructor(message, options = {}) {
130
- super(message, { ...options, code: "SCP_NEXT_FILESYSTEM_ERROR" });
131
- }
132
- };
133
- var HostVerificationError = class extends ScpNextError {
134
- constructor(message, options = {}) {
135
- super(message, { ...options, code: "SCP_NEXT_HOST_VERIFICATION_ERROR" });
136
- }
137
- };
72
+ class ScpNextError extends Error {
73
+ code;
74
+ context;
75
+ constructor(message, options = {}) {
76
+ super(message, { cause: options.cause });
77
+ this.name = new.target.name;
78
+ this.code = options.code ?? "SCP_NEXT_ERROR";
79
+ this.context = redactSensitiveValues(options.context ?? {});
80
+ }
81
+ }
82
+ class ConfigurationError extends ScpNextError {
83
+ constructor(message, options = {}) {
84
+ super(message, { ...options, code: "SCP_NEXT_CONFIGURATION_ERROR" });
85
+ }
86
+ }
87
+ class ValidationError extends ScpNextError {
88
+ constructor(message, options = {}) {
89
+ super(message, { ...options, code: "SCP_NEXT_VALIDATION_ERROR" });
90
+ }
91
+ }
92
+ class AuthenticationError extends ScpNextError {
93
+ constructor(message, options = {}) {
94
+ super(message, { ...options, code: "SCP_NEXT_AUTHENTICATION_ERROR" });
95
+ }
96
+ }
97
+ class ConnectionError extends ScpNextError {
98
+ constructor(message, options = {}) {
99
+ super(message, { ...options, code: "SCP_NEXT_CONNECTION_ERROR" });
100
+ }
101
+ }
102
+ class TransferError extends ScpNextError {
103
+ constructor(message, options = {}) {
104
+ super(message, { ...options, code: "SCP_NEXT_TRANSFER_ERROR" });
105
+ }
106
+ }
107
+ class RemoteCommandError extends ScpNextError {
108
+ exitCode;
109
+ signal;
110
+ stdout;
111
+ stderr;
112
+ constructor(message, options) {
113
+ super(message, {
114
+ code: "SCP_NEXT_REMOTE_COMMAND_ERROR",
115
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
116
+ ...(options.context !== undefined ? { context: options.context } : {})
117
+ });
118
+ this.exitCode = options.exitCode;
119
+ this.signal = options.signal;
120
+ this.stdout = options.stdout ?? "";
121
+ this.stderr = options.stderr ?? "";
122
+ }
123
+ }
124
+ class FileSystemError extends ScpNextError {
125
+ constructor(message, options = {}) {
126
+ super(message, { ...options, code: "SCP_NEXT_FILESYSTEM_ERROR" });
127
+ }
128
+ }
129
+ class HostVerificationError extends ScpNextError {
130
+ constructor(message, options = {}) {
131
+ super(message, { ...options, code: "SCP_NEXT_HOST_VERIFICATION_ERROR" });
132
+ }
133
+ }
138
134
  function toScpNextError(error) {
139
- if (error instanceof ScpNextError) {
140
- return error;
141
- }
142
- const message = error instanceof Error ? error.message : "Unexpected scp-next error.";
143
- const lowerMessage = message.toLowerCase();
144
- if (lowerMessage.includes("authentication") || lowerMessage.includes("permission denied")) {
145
- return new AuthenticationError("SSH authentication failed.", { cause: error });
146
- }
147
- if (lowerMessage.includes("host verification") || lowerMessage.includes("host key") || lowerMessage.includes("fingerprint") || lowerMessage.includes("host") && lowerMessage.includes("denied")) {
148
- return new HostVerificationError("SSH host verification failed.", { cause: error });
149
- }
150
- if (lowerMessage.includes("timed out") || lowerMessage.includes("connection") || lowerMessage.includes("econn")) {
151
- return new ConnectionError("SSH connection failed.", { cause: error });
152
- }
153
- return new TransferError(String(redactSensitiveValues(message)), { cause: error });
135
+ if (error instanceof ScpNextError) {
136
+ return error;
137
+ }
138
+ const message = error instanceof Error ? error.message : "Unexpected scp-next error.";
139
+ const lowerMessage = message.toLowerCase();
140
+ if (lowerMessage.includes("authentication") || lowerMessage.includes("permission denied")) {
141
+ return new AuthenticationError("SSH authentication failed.", { cause: error });
142
+ }
143
+ if (lowerMessage.includes("host verification") ||
144
+ lowerMessage.includes("host key") ||
145
+ lowerMessage.includes("fingerprint") ||
146
+ (lowerMessage.includes("host") && lowerMessage.includes("denied"))) {
147
+ return new HostVerificationError("SSH host verification failed.", { cause: error });
148
+ }
149
+ if (lowerMessage.includes("timed out") ||
150
+ lowerMessage.includes("connection") ||
151
+ lowerMessage.includes("econn")) {
152
+ return new ConnectionError("SSH connection failed.", { cause: error });
153
+ }
154
+ return new TransferError(String(redactSensitiveValues(message)), { cause: error });
154
155
  }
155
156
 
156
- // src/client/ssh-options.ts
157
- import { readFile as readFile2 } from "fs/promises";
158
-
159
- // src/security/host-verification.ts
160
- import { createHash } from "crypto";
161
- import { access as access2, readFile } from "fs/promises";
162
-
163
- // src/paths/local-path.ts
164
- import { constants } from "fs";
165
- import { access, mkdir, stat } from "fs/promises";
166
- import os from "os";
167
- import path from "path";
168
157
  function expandHome(inputPath) {
169
- if (inputPath === "~") {
170
- return os.homedir();
171
- }
172
- if (inputPath.startsWith(`~${path.sep}`) || inputPath.startsWith("~/")) {
173
- return path.join(os.homedir(), inputPath.slice(2));
174
- }
175
- return inputPath;
158
+ if (inputPath === "~") {
159
+ return os.homedir();
160
+ }
161
+ if (inputPath.startsWith(`~${path.sep}`) || inputPath.startsWith("~/")) {
162
+ return path.join(os.homedir(), inputPath.slice(2));
163
+ }
164
+ return inputPath;
176
165
  }
177
166
  function resolveLocalPath(inputPath, baseDir = process.cwd()) {
178
- const expanded = expandHome(inputPath);
179
- return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(baseDir, expanded);
167
+ const expanded = expandHome(inputPath);
168
+ return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(baseDir, expanded);
180
169
  }
181
170
  async function assertLocalPathExists(inputPath) {
182
- try {
183
- await access(resolveLocalPath(inputPath), constants.F_OK);
184
- } catch (error) {
185
- throw new FileSystemError(`Local path does not exist: ${inputPath}`, {
186
- cause: error,
187
- context: { path: inputPath }
188
- });
189
- }
171
+ try {
172
+ await access(resolveLocalPath(inputPath), constants.F_OK);
173
+ }
174
+ catch (error) {
175
+ throw new FileSystemError(`Local path does not exist: ${inputPath}`, {
176
+ cause: error,
177
+ context: { path: inputPath }
178
+ });
179
+ }
190
180
  }
191
181
  async function assertDownloadDestination(inputPath, createDirectories = false) {
192
- const resolved = resolveLocalPath(inputPath);
193
- const parent = path.dirname(resolved);
194
- if (createDirectories) {
195
- await mkdir(parent, { recursive: true });
196
- return;
197
- }
198
- try {
199
- await access(parent, constants.W_OK);
200
- } catch (error) {
201
- throw new FileSystemError(`Local destination parent does not exist or is not writable: ${parent}`, {
202
- cause: error,
203
- context: { path: inputPath }
204
- });
205
- }
182
+ const resolved = resolveLocalPath(inputPath);
183
+ const parent = path.dirname(resolved);
184
+ if (createDirectories) {
185
+ await mkdir(parent, { recursive: true });
186
+ return;
187
+ }
188
+ try {
189
+ await access(parent, constants.W_OK);
190
+ }
191
+ catch (error) {
192
+ throw new FileSystemError(`Local destination parent does not exist or is not writable: ${parent}`, {
193
+ cause: error,
194
+ context: { path: inputPath }
195
+ });
196
+ }
206
197
  }
207
198
  async function getLocalPathKind(inputPath) {
208
- const stats = await stat(resolveLocalPath(inputPath));
209
- if (stats.isDirectory()) {
210
- return "directory";
211
- }
212
- if (stats.isFile()) {
213
- return "file";
214
- }
215
- throw new ValidationError(`Local path must be a file or directory: ${inputPath}`, {
216
- context: { path: inputPath }
217
- });
199
+ const stats = await stat(resolveLocalPath(inputPath));
200
+ if (stats.isDirectory()) {
201
+ return "directory";
202
+ }
203
+ if (stats.isFile()) {
204
+ return "file";
205
+ }
206
+ throw new ValidationError(`Local path must be a file or directory: ${inputPath}`, {
207
+ context: { path: inputPath }
208
+ });
218
209
  }
219
210
 
220
- // src/security/host-verification.ts
221
211
  function normalizeFingerprint(fingerprint) {
222
- return fingerprint.trim().replace(/^SHA256:/i, "").replace(/^sha256:/i, "").replace(/=+$/g, "");
212
+ return fingerprint
213
+ .trim()
214
+ .replace(/^SHA256:/i, "")
215
+ .replace(/^sha256:/i, "")
216
+ .replace(/=+$/g, "");
223
217
  }
224
218
  function isHexFingerprint(fingerprint) {
225
- return /^[0-9a-f]+$/i.test(fingerprint);
219
+ return /^[0-9a-f]+$/i.test(fingerprint);
226
220
  }
227
221
  function fingerprintCandidates(fingerprint) {
228
- const normalized = normalizeFingerprint(fingerprint);
229
- const candidates = /* @__PURE__ */ new Set([normalized]);
230
- if (isHexFingerprint(normalized)) {
231
- candidates.add(normalized.toLowerCase());
232
- } else {
233
- try {
234
- candidates.add(Buffer.from(normalized, "base64").toString("hex"));
235
- } catch {
222
+ const normalized = normalizeFingerprint(fingerprint);
223
+ const candidates = new Set([normalized]);
224
+ if (isHexFingerprint(normalized)) {
225
+ candidates.add(normalized.toLowerCase());
236
226
  }
237
- }
238
- return [...candidates];
227
+ else {
228
+ try {
229
+ candidates.add(Buffer.from(normalized, "base64").toString("hex"));
230
+ }
231
+ catch {
232
+ // Keep the original normalized value when it is not valid base64.
233
+ }
234
+ }
235
+ return [...candidates];
239
236
  }
240
237
  function isFingerprintMatch(actual, expected) {
241
- const normalizedActual = normalizeFingerprint(actual);
242
- const normalizedExpected = normalizeFingerprint(expected);
243
- if (normalizedActual === normalizedExpected) {
244
- return true;
245
- }
246
- return isHexFingerprint(normalizedActual) && isHexFingerprint(normalizedExpected) && normalizedActual.toLowerCase() === normalizedExpected.toLowerCase();
238
+ const normalizedActual = normalizeFingerprint(actual);
239
+ const normalizedExpected = normalizeFingerprint(expected);
240
+ if (normalizedActual === normalizedExpected) {
241
+ return true;
242
+ }
243
+ return (isHexFingerprint(normalizedActual) &&
244
+ isHexFingerprint(normalizedExpected) &&
245
+ normalizedActual.toLowerCase() === normalizedExpected.toLowerCase());
247
246
  }
248
247
  function hostVerificationHint(host) {
249
- return [
250
- "",
251
- "Typical fix:",
252
- ` ssh ${host}`,
253
- "",
254
- "If you trust the host key, accept it and retry the scp-next command.",
255
- "Alternatively configure hostFingerprint or knownHostsFile explicitly."
256
- ].join("\n");
248
+ return [
249
+ "",
250
+ "Typical fix:",
251
+ ` ssh ${host}`,
252
+ "",
253
+ "If you trust the host key, accept it and retry the scp-next command.",
254
+ "Alternatively configure hostFingerprint or knownHostsFile explicitly."
255
+ ].join("\n");
257
256
  }
258
257
  async function resolveAllowedFingerprints(options) {
259
- const fingerprints = /* @__PURE__ */ new Set();
260
- const knownHostsFile = options.knownHostsFile ?? "~/.ssh/known_hosts";
261
- if (!options.host) {
262
- throw new HostVerificationError("A host is required for SSH host verification.");
263
- }
264
- if (options.hostFingerprint) {
265
- for (const fingerprint of fingerprintCandidates(options.hostFingerprint)) {
266
- fingerprints.add(fingerprint);
267
- }
268
- }
269
- try {
270
- await access2(expandHome(knownHostsFile));
271
- const content = await readFile(expandHome(knownHostsFile), "utf8");
272
- for (const line of content.split(/\r?\n/)) {
273
- const trimmed = line.trim();
274
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("@")) {
275
- continue;
276
- }
277
- const [hosts, algorithm, key] = trimmed.split(/\s+/);
278
- const hostPatterns = hosts?.split(",") ?? [];
279
- const portHost = options.port ? `[${options.host}]:${options.port}` : void 0;
280
- const matchesHost = hostPatterns.includes(options.host) || Boolean(portHost && hostPatterns.includes(portHost));
281
- if (!hosts || !algorithm || !key || !matchesHost) {
282
- continue;
283
- }
284
- const keyBuffer = Buffer.from(key, "base64");
285
- fingerprints.add(createHash("sha256").update(keyBuffer).digest("base64").replace(/=+$/g, ""));
286
- fingerprints.add(createHash("sha256").update(keyBuffer).digest("hex"));
287
- }
288
- } catch (error) {
289
- if (options.knownHostsFile || !options.hostFingerprint) {
290
- throw new HostVerificationError(
291
- "SSH host verification requires hostFingerprint or an accessible knownHostsFile.",
292
- { cause: error, context: { knownHostsFile } }
293
- );
294
- }
295
- }
296
- if (fingerprints.size === 0) {
297
- throw new HostVerificationError(
298
- `SSH host verification failed before connecting: no matching host fingerprint was configured.${hostVerificationHint(
299
- options.host
300
- )}`
301
- );
302
- }
303
- return fingerprints;
258
+ const fingerprints = new Set();
259
+ const knownHostsFile = options.knownHostsFile ?? "~/.ssh/known_hosts";
260
+ if (!options.host) {
261
+ throw new HostVerificationError("A host is required for SSH host verification.");
262
+ }
263
+ if (options.hostFingerprint) {
264
+ for (const fingerprint of fingerprintCandidates(options.hostFingerprint)) {
265
+ fingerprints.add(fingerprint);
266
+ }
267
+ }
268
+ try {
269
+ await access(expandHome(knownHostsFile));
270
+ const content = await readFile(expandHome(knownHostsFile), "utf8");
271
+ for (const line of content.split(/\r?\n/)) {
272
+ const trimmed = line.trim();
273
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("@")) {
274
+ continue;
275
+ }
276
+ const [hosts, algorithm, key] = trimmed.split(/\s+/);
277
+ const hostPatterns = hosts?.split(",") ?? [];
278
+ const portHost = options.port ? `[${options.host}]:${options.port}` : undefined;
279
+ const matchesHost = hostPatterns.includes(options.host) || Boolean(portHost && hostPatterns.includes(portHost));
280
+ if (!hosts || !algorithm || !key || !matchesHost) {
281
+ continue;
282
+ }
283
+ const keyBuffer = Buffer.from(key, "base64");
284
+ fingerprints.add(createHash("sha256").update(keyBuffer).digest("base64").replace(/=+$/g, ""));
285
+ fingerprints.add(createHash("sha256").update(keyBuffer).digest("hex"));
286
+ }
287
+ }
288
+ catch (error) {
289
+ if (options.knownHostsFile || !options.hostFingerprint) {
290
+ throw new HostVerificationError("SSH host verification requires hostFingerprint or an accessible knownHostsFile.", { cause: error, context: { knownHostsFile } });
291
+ }
292
+ }
293
+ if (fingerprints.size === 0) {
294
+ throw new HostVerificationError(`SSH host verification failed before connecting: no matching host fingerprint was configured.${hostVerificationHint(options.host)}`);
295
+ }
296
+ return fingerprints;
304
297
  }
305
298
  function createHostVerifier(allowedFingerprints) {
306
- return (hashedKey) => {
307
- for (const expected of allowedFingerprints) {
308
- if (isFingerprintMatch(hashedKey, expected)) {
309
- return true;
310
- }
311
- }
312
- return false;
313
- };
299
+ return (hashedKey) => {
300
+ for (const expected of allowedFingerprints) {
301
+ if (isFingerprintMatch(hashedKey, expected)) {
302
+ return true;
303
+ }
304
+ }
305
+ return false;
306
+ };
314
307
  }
315
308
 
316
- // src/client/ssh-options.ts
317
309
  async function createSshConnectOptions(options) {
318
- const connectOptions = {};
319
- if (options.host !== void 0) connectOptions.host = options.host;
320
- if (options.port !== void 0) connectOptions.port = options.port;
321
- if (options.username !== void 0) connectOptions.username = options.username;
322
- if (options.password !== void 0) connectOptions.password = options.password;
323
- if (options.privateKey !== void 0) {
324
- connectOptions.privateKey = options.privateKey;
325
- } else if (options.privateKeyFile) {
326
- connectOptions.privateKey = await readFile2(
327
- expandHome(options.privateKeyFile),
328
- "utf8"
329
- );
330
- }
331
- if (options.passphrase !== void 0) connectOptions.passphrase = options.passphrase;
332
- const agent = options.agent ?? process.env.SSH_AUTH_SOCK;
333
- if (agent !== void 0) connectOptions.agent = agent;
334
- if (options.timeout !== void 0) connectOptions.readyTimeout = options.timeout;
335
- const allowedFingerprints = await resolveAllowedFingerprints(options);
336
- const hostVerifier = createHostVerifier(allowedFingerprints);
337
- if (hostVerifier) {
338
- connectOptions.hostHash = "sha256";
339
- connectOptions.hostVerifier = hostVerifier;
340
- }
341
- return connectOptions;
310
+ const connectOptions = {};
311
+ if (options.host !== undefined)
312
+ connectOptions.host = options.host;
313
+ if (options.port !== undefined)
314
+ connectOptions.port = options.port;
315
+ if (options.username !== undefined)
316
+ connectOptions.username = options.username;
317
+ if (options.password !== undefined)
318
+ connectOptions.password = options.password;
319
+ if (options.privateKey !== undefined) {
320
+ connectOptions.privateKey = options.privateKey;
321
+ }
322
+ else if (options.privateKeyFile) {
323
+ connectOptions.privateKey = await readFile(expandHome(options.privateKeyFile), "utf8");
324
+ }
325
+ if (options.passphrase !== undefined)
326
+ connectOptions.passphrase = options.passphrase;
327
+ const agent = options.agent ?? process.env.SSH_AUTH_SOCK;
328
+ if (agent !== undefined)
329
+ connectOptions.agent = agent;
330
+ if (options.timeout !== undefined)
331
+ connectOptions.readyTimeout = options.timeout;
332
+ const allowedFingerprints = await resolveAllowedFingerprints(options);
333
+ const hostVerifier = createHostVerifier(allowedFingerprints);
334
+ if (hostVerifier) {
335
+ connectOptions.hostHash = "sha256";
336
+ connectOptions.hostVerifier = hostVerifier;
337
+ }
338
+ return connectOptions;
342
339
  }
343
340
 
344
- // src/client/transport.ts
345
341
  function toRemoteFileInfo(info) {
346
- return {
347
- type: info.type,
348
- name: info.name,
349
- size: info.size
350
- };
342
+ return {
343
+ type: info.type,
344
+ name: info.name,
345
+ size: info.size
346
+ };
351
347
  }
352
- var Ssh2SftpTransport = class {
353
- client;
354
- constructor(name = "scp-next") {
355
- this.client = new SftpClient(name, {
356
- error: () => void 0,
357
- end: () => void 0,
358
- close: () => void 0
359
- });
360
- }
361
- async connect(options) {
362
- const connectOptions = await createSshConnectOptions(options);
363
- try {
364
- await this.client.connect(connectOptions);
365
- } catch (error) {
366
- const converted = toScpNextError(error);
367
- if (converted instanceof HostVerificationError) {
368
- throw new HostVerificationError(
369
- `SSH host verification failed during connection: ${formatErrorMessage(
370
- error
371
- )}.${hostVerificationTroubleshooting(options.host)}`,
372
- { cause: error, context: connectionContext(options) }
373
- );
374
- }
375
- if (isLikelyHostVerificationFailure(error)) {
376
- throw new HostVerificationError(
377
- `SSH host verification failed during connection.${hostVerificationTroubleshooting(
378
- options.host
379
- )}`,
380
- { cause: error, context: connectionContext(options) }
381
- );
382
- }
383
- throw new ConnectionError(`Unable to connect to SSH server: ${formatErrorMessage(error)}`, {
384
- cause: error,
385
- context: connectionContext(options)
386
- });
387
- }
388
- }
389
- async close() {
390
- await this.client.end();
391
- }
392
- async exists(remotePath) {
393
- return this.client.exists(remotePath);
394
- }
395
- async mkdir(remotePath, recursive = true) {
396
- await this.client.mkdir(remotePath, recursive);
397
- }
398
- async list(remotePath) {
399
- const files = await this.client.list(remotePath);
400
- return files.map(toRemoteFileInfo);
401
- }
402
- async uploadFile(localPath, remotePath, onStep) {
403
- try {
404
- await this.client.fastPut(localPath, remotePath, this.transferOptions(onStep));
405
- } catch (error) {
406
- throw new TransferError(`Upload failed for ${localPath}: ${formatErrorMessage(error)}`, {
407
- cause: error,
408
- context: { localPath, remotePath }
409
- });
410
- }
411
- }
412
- async downloadFile(remotePath, localPath, onStep) {
413
- try {
414
- await this.client.fastGet(remotePath, localPath, this.transferOptions(onStep));
415
- } catch (error) {
416
- throw new TransferError(`Download failed for ${remotePath}: ${formatErrorMessage(error)}`, {
417
- cause: error,
418
- context: { remotePath, localPath }
419
- });
420
- }
421
- }
422
- transferOptions(onStep) {
423
- if (!onStep) {
424
- return void 0;
348
+ class Ssh2SftpTransport {
349
+ client;
350
+ constructor(name = "scp-next") {
351
+ this.client = new SftpClient(name, {
352
+ error: () => undefined,
353
+ end: () => undefined,
354
+ close: () => undefined
355
+ });
356
+ }
357
+ async connect(options) {
358
+ const connectOptions = await createSshConnectOptions(options);
359
+ try {
360
+ await this.client.connect(connectOptions);
361
+ }
362
+ catch (error) {
363
+ const converted = toScpNextError(error);
364
+ if (converted instanceof HostVerificationError) {
365
+ throw new HostVerificationError(`SSH host verification failed during connection: ${formatErrorMessage(error)}.${hostVerificationTroubleshooting(options.host)}`, { cause: error, context: connectionContext(options) });
366
+ }
367
+ if (isLikelyHostVerificationFailure(error)) {
368
+ throw new HostVerificationError(`SSH host verification failed during connection.${hostVerificationTroubleshooting(options.host)}`, { cause: error, context: connectionContext(options) });
369
+ }
370
+ throw new ConnectionError(`Unable to connect to SSH server: ${formatErrorMessage(error)}`, {
371
+ cause: error,
372
+ context: connectionContext(options)
373
+ });
374
+ }
375
+ }
376
+ async close() {
377
+ await this.client.end();
378
+ }
379
+ async exists(remotePath) {
380
+ return this.client.exists(remotePath);
381
+ }
382
+ async mkdir(remotePath, recursive = true) {
383
+ await this.client.mkdir(remotePath, recursive);
384
+ }
385
+ async list(remotePath) {
386
+ const files = await this.client.list(remotePath);
387
+ return files.map(toRemoteFileInfo);
388
+ }
389
+ async uploadFile(localPath, remotePath, onStep) {
390
+ try {
391
+ await this.client.fastPut(localPath, remotePath, this.transferOptions(onStep));
392
+ }
393
+ catch (error) {
394
+ throw new TransferError(`Upload failed for ${localPath}: ${formatErrorMessage(error)}`, {
395
+ cause: error,
396
+ context: { localPath, remotePath }
397
+ });
398
+ }
399
+ }
400
+ async downloadFile(remotePath, localPath, onStep) {
401
+ try {
402
+ await this.client.fastGet(remotePath, localPath, this.transferOptions(onStep));
403
+ }
404
+ catch (error) {
405
+ throw new TransferError(`Download failed for ${remotePath}: ${formatErrorMessage(error)}`, {
406
+ cause: error,
407
+ context: { remotePath, localPath }
408
+ });
409
+ }
425
410
  }
411
+ transferOptions(onStep) {
412
+ if (!onStep) {
413
+ return undefined;
414
+ }
415
+ return {
416
+ step(transferredBytes, chunkBytes, totalBytes) {
417
+ onStep({ transferredBytes, chunkBytes, totalBytes });
418
+ }
419
+ };
420
+ }
421
+ }
422
+ function connectionContext(options) {
426
423
  return {
427
- step(transferredBytes, chunkBytes, totalBytes) {
428
- onStep({ transferredBytes, chunkBytes, totalBytes });
429
- }
424
+ host: options.host,
425
+ port: options.port,
426
+ username: options.username
430
427
  };
431
- }
432
- };
433
- function connectionContext(options) {
434
- return {
435
- host: options.host,
436
- port: options.port,
437
- username: options.username
438
- };
439
428
  }
440
429
  function isLikelyHostVerificationFailure(error) {
441
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
442
- return message.includes("host") && (message.includes("verif") || message.includes("fingerprint") || message.includes("host key") || message.includes("key mismatch") || message.includes("denied"));
430
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
431
+ return (message.includes("host") &&
432
+ (message.includes("verif") ||
433
+ message.includes("fingerprint") ||
434
+ message.includes("host key") ||
435
+ message.includes("key mismatch") ||
436
+ message.includes("denied")));
443
437
  }
444
438
  function hostVerificationTroubleshooting(host) {
445
- const displayHost = host ?? "<host>";
446
- return [
447
- "",
448
- "",
449
- "Typical fix:",
450
- ` ssh ${displayHost}`,
451
- "",
452
- "If plain ssh works but scp-next fails, check that the host entry is available in",
453
- "`~/.ssh/known_hosts` for the same host and port used by scp-next. For non-default ports,",
454
- "OpenSSH usually stores entries as `[host]:port`.",
455
- "You can also configure hostFingerprint or knownHostsFile explicitly."
456
- ].join("\n");
439
+ const displayHost = host ?? "<host>";
440
+ return [
441
+ "",
442
+ "",
443
+ "Typical fix:",
444
+ ` ssh ${displayHost}`,
445
+ "",
446
+ "If plain ssh works but scp-next fails, check that the host entry is available in",
447
+ "`~/.ssh/known_hosts` for the same host and port used by scp-next. For non-default ports,",
448
+ "OpenSSH usually stores entries as `[host]:port`.",
449
+ "You can also configure hostFingerprint or knownHostsFile explicitly."
450
+ ].join("\n");
457
451
  }
458
452
 
459
- // src/client/command-executor.ts
460
- import { Client } from "ssh2";
461
- var SshCommandExecutor = class {
462
- client;
463
- connected = false;
464
- closed = true;
465
- constructor(client = new Client()) {
466
- this.client = client;
467
- this.client.on("error", () => {
468
- this.connected = false;
469
- });
470
- this.client.on("close", () => {
471
- this.connected = false;
472
- this.closed = true;
473
- });
474
- }
475
- async connect(options) {
476
- const connectOptions = await createSshConnectOptions(options);
477
- this.closed = false;
478
- await new Promise((resolve, reject) => {
479
- const onReady = () => {
480
- cleanup();
481
- this.connected = true;
482
- resolve();
483
- };
484
- const onError = (error) => {
485
- cleanup();
486
- this.connected = false;
487
- this.closed = true;
488
- try {
489
- this.client.end();
490
- } catch {
491
- }
492
- const converted = toScpNextError(error);
493
- if (converted instanceof HostVerificationError) {
494
- reject(converted);
495
- return;
453
+ class SshCommandExecutor {
454
+ client;
455
+ connected = false;
456
+ closed = true;
457
+ constructor(client = new Client()) {
458
+ this.client = client;
459
+ this.client.on("error", () => {
460
+ this.connected = false;
461
+ });
462
+ this.client.on("close", () => {
463
+ this.connected = false;
464
+ this.closed = true;
465
+ });
466
+ }
467
+ async connect(options) {
468
+ const connectOptions = await createSshConnectOptions(options);
469
+ this.closed = false;
470
+ await new Promise((resolve, reject) => {
471
+ const onReady = () => {
472
+ cleanup();
473
+ this.connected = true;
474
+ resolve();
475
+ };
476
+ const onError = (error) => {
477
+ cleanup();
478
+ this.connected = false;
479
+ this.closed = true;
480
+ try {
481
+ this.client.end();
482
+ }
483
+ catch {
484
+ // The failed connection may not have a writable socket.
485
+ }
486
+ const converted = toScpNextError(error);
487
+ if (converted instanceof HostVerificationError) {
488
+ reject(converted);
489
+ return;
490
+ }
491
+ reject(new ConnectionError(`Unable to connect command executor to SSH server: ${formatErrorMessage(error)}`, {
492
+ cause: error,
493
+ context: {
494
+ host: options.host,
495
+ port: options.port,
496
+ username: options.username
497
+ }
498
+ }));
499
+ };
500
+ const onClose = () => {
501
+ cleanup();
502
+ reject(new ConnectionError("SSH connection closed before it was ready."));
503
+ };
504
+ const cleanup = () => {
505
+ this.client.off("ready", onReady);
506
+ this.client.off("error", onError);
507
+ this.client.off("close", onClose);
508
+ };
509
+ this.client.once("ready", onReady);
510
+ this.client.once("error", onError);
511
+ this.client.once("close", onClose);
512
+ try {
513
+ this.client.connect(connectOptions);
514
+ }
515
+ catch (error) {
516
+ cleanup();
517
+ this.closed = true;
518
+ reject(new ConnectionError(`Unable to start SSH connection: ${formatErrorMessage(error)}`));
519
+ }
520
+ });
521
+ }
522
+ async exec(command, options = {}) {
523
+ if (!this.connected) {
524
+ throw new ConnectionError("Command executor is not connected.");
496
525
  }
497
- reject(
498
- new ConnectionError(
499
- `Unable to connect command executor to SSH server: ${formatErrorMessage(error)}`,
500
- {
501
- cause: error,
502
- context: {
503
- host: options.host,
504
- port: options.port,
505
- username: options.username
506
- }
526
+ return new Promise((resolve, reject) => {
527
+ try {
528
+ this.client.exec(command, (error, stream) => {
529
+ if (error) {
530
+ reject(new RemoteCommandError("Unable to start remote command.", {
531
+ exitCode: null
532
+ }));
533
+ return;
534
+ }
535
+ collectCommandResult(command, stream, options, resolve, reject);
536
+ });
507
537
  }
508
- )
509
- );
510
- };
511
- const onClose = () => {
512
- cleanup();
513
- reject(new ConnectionError("SSH connection closed before it was ready."));
514
- };
515
- const cleanup = () => {
516
- this.client.off("ready", onReady);
517
- this.client.off("error", onError);
518
- this.client.off("close", onClose);
519
- };
520
- this.client.once("ready", onReady);
521
- this.client.once("error", onError);
522
- this.client.once("close", onClose);
523
- try {
524
- this.client.connect(connectOptions);
525
- } catch (error) {
526
- cleanup();
527
- this.closed = true;
528
- reject(
529
- new ConnectionError(
530
- `Unable to start SSH connection: ${formatErrorMessage(error)}`
531
- )
532
- );
533
- }
534
- });
535
- }
536
- async exec(command, options = {}) {
537
- if (!this.connected) {
538
- throw new ConnectionError("Command executor is not connected.");
539
- }
540
- return new Promise((resolve, reject) => {
541
- try {
542
- this.client.exec(command, (error, stream) => {
543
- if (error) {
544
- reject(
545
- new RemoteCommandError("Unable to start remote command.", {
546
- exitCode: null
547
- })
548
- );
538
+ catch (error) {
539
+ reject(new ConnectionError(`Unable to start remote command: ${formatErrorMessage(error)}`));
540
+ }
541
+ });
542
+ }
543
+ async close() {
544
+ if (this.closed) {
549
545
  return;
550
- }
551
- collectCommandResult(command, stream, options, resolve, reject);
546
+ }
547
+ this.connected = false;
548
+ await new Promise((resolve) => {
549
+ this.client.once("close", resolve);
550
+ this.client.end();
552
551
  });
553
- } catch (error) {
554
- reject(
555
- new ConnectionError(
556
- `Unable to start remote command: ${formatErrorMessage(error)}`
557
- )
558
- );
559
- }
560
- });
561
- }
562
- async close() {
563
- if (this.closed) {
564
- return;
565
- }
566
- this.connected = false;
567
- await new Promise((resolve) => {
568
- this.client.once("close", resolve);
569
- this.client.end();
570
- });
571
- }
572
- };
552
+ }
553
+ }
573
554
  function collectCommandResult(command, stream, options, resolve, reject) {
574
- let stdout = "";
575
- let stderr = "";
576
- let outputBytes = 0;
577
- let settled = false;
578
- const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
579
- const cleanup = () => {
580
- if (timer) clearTimeout(timer);
581
- stream.off("data", onStdoutData);
582
- stream.stderr.off("data", onStderrData);
583
- stream.off("close", onClose);
584
- stream.off("error", onStreamError);
585
- stream.stderr.off("error", onStreamError);
586
- };
587
- const fail = (error) => {
588
- if (settled) return;
589
- settled = true;
590
- cleanup();
591
- try {
592
- stream.close();
593
- } catch {
594
- }
595
- reject(error);
596
- };
597
- const appendOutput = (target, chunk) => {
598
- outputBytes += Buffer.byteLength(chunk);
599
- if (outputBytes > maxBuffer) {
600
- fail(
601
- new RemoteCommandError(
602
- `Remote command output exceeded maxBuffer of ${maxBuffer} bytes.`,
603
- {
555
+ let stdout = "";
556
+ let stderr = "";
557
+ let outputBytes = 0;
558
+ let settled = false;
559
+ const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
560
+ const cleanup = () => {
561
+ if (timer)
562
+ clearTimeout(timer);
563
+ stream.off("data", onStdoutData);
564
+ stream.stderr.off("data", onStderrData);
565
+ stream.off("close", onClose);
566
+ stream.off("error", onStreamError);
567
+ stream.stderr.off("error", onStreamError);
568
+ };
569
+ const fail = (error) => {
570
+ if (settled)
571
+ return;
572
+ settled = true;
573
+ cleanup();
574
+ try {
575
+ stream.close();
576
+ }
577
+ catch {
578
+ // The connection may already be closed.
579
+ }
580
+ reject(error);
581
+ };
582
+ const appendOutput = (target, chunk) => {
583
+ outputBytes += Buffer.byteLength(chunk);
584
+ if (outputBytes > maxBuffer) {
585
+ fail(new RemoteCommandError(`Remote command output exceeded maxBuffer of ${maxBuffer} bytes.`, {
586
+ exitCode: null,
587
+ stdout,
588
+ stderr
589
+ }));
590
+ return;
591
+ }
592
+ if (target === "stdout")
593
+ stdout += chunk;
594
+ else
595
+ stderr += chunk;
596
+ };
597
+ const onStreamError = () => {
598
+ fail(new RemoteCommandError("Remote command stream failed.", {
604
599
  exitCode: null,
605
600
  stdout,
606
601
  stderr
607
- }
608
- )
609
- );
610
- return;
611
- }
612
- if (target === "stdout") stdout += chunk;
613
- else stderr += chunk;
614
- };
615
- const onStreamError = () => {
616
- fail(
617
- new RemoteCommandError("Remote command stream failed.", {
618
- exitCode: null,
619
- stdout,
620
- stderr
621
- })
622
- );
623
- };
624
- const onStdoutData = (chunk) => {
625
- appendOutput("stdout", chunk);
626
- };
627
- const onStderrData = (chunk) => {
628
- appendOutput("stderr", chunk);
629
- };
630
- const onClose = (exitCode, signal) => {
631
- if (settled) return;
632
- settled = true;
633
- cleanup();
634
- resolve({
635
- command,
636
- stdout,
637
- stderr,
638
- exitCode: typeof exitCode === "number" ? exitCode : null,
639
- ...signal ? { signal } : {}
640
- });
641
- };
642
- const timer = options.timeout === void 0 ? void 0 : setTimeout(() => {
643
- fail(
644
- new RemoteCommandError(
645
- `Remote command timed out after ${options.timeout} ms.`,
646
- {
647
- exitCode: null,
648
- stdout,
649
- stderr
650
- }
651
- )
652
- );
653
- }, options.timeout);
654
- stream.setEncoding("utf8");
655
- stream.stderr.setEncoding("utf8");
656
- stream.on("data", onStdoutData);
657
- stream.stderr.on("data", onStderrData);
658
- stream.once("error", onStreamError);
659
- stream.stderr.once("error", onStreamError);
660
- stream.once("close", onClose);
602
+ }));
603
+ };
604
+ const onStdoutData = (chunk) => {
605
+ appendOutput("stdout", chunk);
606
+ };
607
+ const onStderrData = (chunk) => {
608
+ appendOutput("stderr", chunk);
609
+ };
610
+ const onClose = (exitCode, signal) => {
611
+ if (settled)
612
+ return;
613
+ settled = true;
614
+ cleanup();
615
+ resolve({
616
+ command,
617
+ stdout,
618
+ stderr,
619
+ exitCode: typeof exitCode === "number" ? exitCode : null,
620
+ ...(signal ? { signal } : {})
621
+ });
622
+ };
623
+ const timer = options.timeout === undefined
624
+ ? undefined
625
+ : setTimeout(() => {
626
+ fail(new RemoteCommandError(`Remote command timed out after ${options.timeout} ms.`, {
627
+ exitCode: null,
628
+ stdout,
629
+ stderr
630
+ }));
631
+ }, options.timeout);
632
+ stream.setEncoding("utf8");
633
+ stream.stderr.setEncoding("utf8");
634
+ stream.on("data", onStdoutData);
635
+ stream.stderr.on("data", onStderrData);
636
+ stream.once("error", onStreamError);
637
+ stream.stderr.once("error", onStreamError);
638
+ stream.once("close", onClose);
661
639
  }
662
640
 
663
- // src/client/walk.ts
664
- import { readdir, stat as stat2 } from "fs/promises";
665
- import path3 from "path";
666
-
667
- // src/paths/remote-path.ts
668
- import path2 from "path";
669
641
  function assertRemotePath(inputPath, label = "remote path") {
670
- if (!inputPath || inputPath.trim().length === 0) {
671
- throw new ValidationError(`Invalid ${label}: remote paths must not be empty.`, {
672
- context: { path: inputPath }
673
- });
674
- }
642
+ if (!inputPath || inputPath.trim().length === 0) {
643
+ throw new ValidationError(`Invalid ${label}: remote paths must not be empty.`, {
644
+ context: { path: inputPath }
645
+ });
646
+ }
675
647
  }
676
648
  function normalizeRemotePath(inputPath) {
677
- assertRemotePath(inputPath);
678
- return path2.posix.normalize(inputPath.replace(/\\/g, "/"));
649
+ assertRemotePath(inputPath);
650
+ return path.posix.normalize(inputPath.replace(/\\/g, "/"));
679
651
  }
680
652
  function remoteJoin(...parts) {
681
- const joined = path2.posix.join(...parts);
682
- assertRemotePath(joined);
683
- return joined;
653
+ const joined = path.posix.join(...parts);
654
+ assertRemotePath(joined);
655
+ return joined;
684
656
  }
685
657
  function remoteDirname(inputPath) {
686
- assertRemotePath(inputPath);
687
- return path2.posix.dirname(inputPath);
658
+ assertRemotePath(inputPath);
659
+ return path.posix.dirname(inputPath);
688
660
  }
689
661
 
690
- // src/client/walk.ts
691
662
  async function walkLocalFiles(rootPath) {
692
- const resolvedRoot = resolveLocalPath(rootPath);
693
- const stats = await stat2(resolvedRoot);
694
- if (stats.isFile()) {
695
- return [
696
- {
697
- absolutePath: resolvedRoot,
698
- relativePath: path3.basename(resolvedRoot),
699
- size: stats.size
700
- }
701
- ];
702
- }
703
- const files = [];
704
- async function visit(directory) {
705
- const entries = await readdir(directory, { withFileTypes: true });
706
- for (const entry of entries) {
707
- const absolutePath = path3.join(directory, entry.name);
708
- if (entry.isDirectory()) {
709
- await visit(absolutePath);
710
- } else if (entry.isFile()) {
711
- const entryStats = await stat2(absolutePath);
712
- files.push({
713
- absolutePath,
714
- relativePath: path3.relative(resolvedRoot, absolutePath),
715
- size: entryStats.size
716
- });
717
- }
663
+ const resolvedRoot = resolveLocalPath(rootPath);
664
+ const stats = await stat(resolvedRoot);
665
+ if (stats.isFile()) {
666
+ return [
667
+ {
668
+ absolutePath: resolvedRoot,
669
+ relativePath: path.basename(resolvedRoot),
670
+ size: stats.size
671
+ }
672
+ ];
673
+ }
674
+ const files = [];
675
+ async function visit(directory) {
676
+ const entries = await readdir(directory, { withFileTypes: true });
677
+ for (const entry of entries) {
678
+ const absolutePath = path.join(directory, entry.name);
679
+ if (entry.isDirectory()) {
680
+ await visit(absolutePath);
681
+ }
682
+ else if (entry.isFile()) {
683
+ const entryStats = await stat(absolutePath);
684
+ files.push({
685
+ absolutePath,
686
+ relativePath: path.relative(resolvedRoot, absolutePath),
687
+ size: entryStats.size
688
+ });
689
+ }
690
+ }
718
691
  }
719
- }
720
- await visit(resolvedRoot);
721
- return files;
692
+ await visit(resolvedRoot);
693
+ return files;
722
694
  }
723
695
  async function walkRemoteFiles(transport, rootPath) {
724
- const rootEntries = await transport.list(rootPath);
725
- const files = [];
726
- async function visit(directory, relativeBase, entries) {
727
- for (const entry of entries) {
728
- const fullPath = remoteJoin(directory, entry.name);
729
- const relativePath = relativeBase ? remoteJoin(relativeBase, entry.name) : entry.name;
730
- if (entry.type === "d") {
731
- await visit(fullPath, relativePath, await transport.list(fullPath));
732
- } else if (entry.type === "-") {
733
- files.push({
734
- remotePath: fullPath,
735
- relativePath,
736
- size: entry.size
737
- });
738
- }
696
+ const rootEntries = await transport.list(rootPath);
697
+ const files = [];
698
+ async function visit(directory, relativeBase, entries) {
699
+ for (const entry of entries) {
700
+ const fullPath = remoteJoin(directory, entry.name);
701
+ const relativePath = relativeBase ? remoteJoin(relativeBase, entry.name) : entry.name;
702
+ if (entry.type === "d") {
703
+ await visit(fullPath, relativePath, await transport.list(fullPath));
704
+ }
705
+ else if (entry.type === "-") {
706
+ files.push({
707
+ remotePath: fullPath,
708
+ relativePath,
709
+ size: entry.size
710
+ });
711
+ }
712
+ }
739
713
  }
740
- }
741
- await visit(rootPath, "", rootEntries);
742
- return files;
714
+ await visit(rootPath, "", rootEntries);
715
+ return files;
743
716
  }
744
717
 
745
- // src/config/validation.ts
746
- import { access as access3 } from "fs/promises";
747
-
748
- // src/config/post-upload-commands.ts
749
718
  function isPostUploadCommands(value) {
750
- return Array.isArray(value) && value.every(
751
- (command) => typeof command === "string" && command.trim().length > 0
752
- );
719
+ return (Array.isArray(value) &&
720
+ value.every((command) => typeof command === "string" && command.trim().length > 0));
753
721
  }
754
722
  function getPostUploadCommands(source) {
755
- const commands = readPostUploadCommands(source);
756
- if (commands === void 0) {
757
- return void 0;
758
- }
759
- if (!isPostUploadCommands(commands)) {
760
- throw new ValidationError(
761
- "postUploadCommands must be an array of non-empty command strings."
762
- );
763
- }
764
- return commands;
723
+ const commands = readPostUploadCommands(source);
724
+ if (commands === undefined) {
725
+ return undefined;
726
+ }
727
+ if (!isPostUploadCommands(commands)) {
728
+ throw new ValidationError("postUploadCommands must be an array of non-empty command strings.");
729
+ }
730
+ return commands;
765
731
  }
766
732
  function readPostUploadCommands(source) {
767
- if (typeof source !== "object" || source === null) {
768
- return void 0;
769
- }
770
- return source.postUploadCommands;
733
+ if (typeof source !== "object" || source === null) {
734
+ return undefined;
735
+ }
736
+ return source
737
+ .postUploadCommands;
771
738
  }
772
739
 
773
- // src/config/validation.ts
774
740
  function validatePort(port) {
775
- if (port === void 0) {
776
- return;
777
- }
778
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
779
- throw new ValidationError("Port must be an integer between 1 and 65535.", {
780
- context: { port }
781
- });
782
- }
741
+ if (port === undefined) {
742
+ return;
743
+ }
744
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
745
+ throw new ValidationError("Port must be an integer between 1 and 65535.", {
746
+ context: { port }
747
+ });
748
+ }
783
749
  }
784
750
  function validateTimeout(timeout) {
785
- if (timeout === void 0) {
786
- return;
787
- }
788
- if (!Number.isFinite(timeout) || timeout <= 0) {
789
- throw new ValidationError("Timeout must be a positive number.", {
790
- context: { timeout }
791
- });
792
- }
751
+ if (timeout === undefined) {
752
+ return;
753
+ }
754
+ if (!Number.isFinite(timeout) || timeout <= 0) {
755
+ throw new ValidationError("Timeout must be a positive number.", {
756
+ context: { timeout }
757
+ });
758
+ }
793
759
  }
794
760
  async function validateServerOptions(options) {
795
- if (!options.host) {
796
- throw new ValidationError("Host is required.");
797
- }
798
- if (!options.username) {
799
- throw new ValidationError("Username is required.");
800
- }
801
- validatePort(options.port);
802
- validateTimeout(options.timeout);
803
- if (options.privateKeyFile) {
804
- try {
805
- await access3(resolveLocalPath(options.privateKeyFile));
806
- } catch (error) {
807
- throw new ValidationError(`Private key file does not exist: ${options.privateKeyFile}`, {
808
- cause: error,
809
- context: { privateKeyFile: options.privateKeyFile }
810
- });
811
- }
812
- }
813
- const hasAuthentication = Boolean(options.password) || Boolean(options.privateKey) || Boolean(options.privateKeyFile) || Boolean(options.agent) || Boolean(process.env.SSH_AUTH_SOCK);
814
- if (!hasAuthentication) {
815
- throw new ValidationError(
816
- "Authentication is required. Provide a password, private key, private key file, or SSH agent."
817
- );
818
- }
761
+ if (!options.host) {
762
+ throw new ValidationError("Host is required.");
763
+ }
764
+ if (!options.username) {
765
+ throw new ValidationError("Username is required.");
766
+ }
767
+ validatePort(options.port);
768
+ validateTimeout(options.timeout);
769
+ if (options.privateKeyFile) {
770
+ try {
771
+ await access(resolveLocalPath(options.privateKeyFile));
772
+ }
773
+ catch (error) {
774
+ throw new ValidationError(`Private key file does not exist: ${options.privateKeyFile}`, {
775
+ cause: error,
776
+ context: { privateKeyFile: options.privateKeyFile }
777
+ });
778
+ }
779
+ }
780
+ const hasAuthentication = Boolean(options.password) ||
781
+ Boolean(options.privateKey) ||
782
+ Boolean(options.privateKeyFile) ||
783
+ Boolean(options.agent) ||
784
+ Boolean(process.env.SSH_AUTH_SOCK);
785
+ if (!hasAuthentication) {
786
+ throw new ValidationError("Authentication is required. Provide a password, private key, private key file, or SSH agent.");
787
+ }
819
788
  }
820
789
  async function validateUploadOptions(options) {
821
- if (!options.localPath) {
822
- throw new ValidationError("Missing localPath.");
823
- }
824
- if (!options.remotePath) {
825
- throw new ValidationError("Missing remotePath.");
826
- }
827
- assertRemotePath(options.remotePath, "remotePath");
828
- validateTimeout(options.timeout);
829
- getPostUploadCommands(options);
830
- await assertLocalPathExists(options.localPath);
831
- if (!options.dryRun) {
832
- await validateServerOptions(options);
833
- }
790
+ if (!options.localPath) {
791
+ throw new ValidationError("Missing localPath.");
792
+ }
793
+ if (!options.remotePath) {
794
+ throw new ValidationError("Missing remotePath.");
795
+ }
796
+ assertRemotePath(options.remotePath, "remotePath");
797
+ validateTimeout(options.timeout);
798
+ getPostUploadCommands(options);
799
+ await assertLocalPathExists(options.localPath);
800
+ if (!options.dryRun) {
801
+ await validateServerOptions(options);
802
+ }
834
803
  }
835
804
  async function validateDownloadOptions(options) {
836
- if (!options.remotePath) {
837
- throw new ValidationError("Missing remotePath.");
838
- }
839
- if (!options.localPath) {
840
- throw new ValidationError("Missing localPath.");
841
- }
842
- assertRemotePath(options.remotePath, "remotePath");
843
- validateTimeout(options.timeout);
844
- if (getPostUploadCommands(options)?.length) {
845
- throw new ValidationError("Post-upload commands are not supported for downloads.");
846
- }
847
- await assertDownloadDestination(options.localPath, options.createDirectories ?? true);
848
- if (!options.dryRun) {
849
- await validateServerOptions(options);
850
- }
805
+ if (!options.remotePath) {
806
+ throw new ValidationError("Missing remotePath.");
807
+ }
808
+ if (!options.localPath) {
809
+ throw new ValidationError("Missing localPath.");
810
+ }
811
+ assertRemotePath(options.remotePath, "remotePath");
812
+ validateTimeout(options.timeout);
813
+ if (getPostUploadCommands(options)?.length) {
814
+ throw new ValidationError("Post-upload commands are not supported for downloads.");
815
+ }
816
+ await assertDownloadDestination(options.localPath, options.createDirectories ?? true);
817
+ if (!options.dryRun) {
818
+ await validateServerOptions(options);
819
+ }
851
820
  }
852
821
 
853
- // src/client/client.ts
854
822
  function percentage(transferredBytes, totalBytes) {
855
- if (!totalBytes || totalBytes <= 0) {
856
- return void 0;
857
- }
858
- return Math.min(100, Math.round(transferredBytes / totalBytes * 100));
823
+ if (!totalBytes || totalBytes <= 0) {
824
+ return undefined;
825
+ }
826
+ return Math.min(100, Math.round((transferredBytes / totalBytes) * 100));
859
827
  }
860
828
  function mergeOptions(serverOptions, transferOptions) {
861
- return { ...serverOptions, ...transferOptions ?? {} };
829
+ return { ...serverOptions, ...(transferOptions ?? {}) };
862
830
  }
863
831
  function shouldCreateDirectories(options) {
864
- return options.createDirectories ?? true;
832
+ return options.createDirectories ?? true;
865
833
  }
866
834
  function hasTrailingPathSeparator(inputPath) {
867
- return /[\\/]$/.test(inputPath);
835
+ return /[\\/]$/.test(inputPath);
868
836
  }
869
837
  function trimmedRemoteDirectory(remotePath) {
870
- return remotePath.replace(/\/+$/g, "") || "/";
838
+ return remotePath.replace(/\/+$/g, "") || "/";
871
839
  }
872
840
  function remoteBasename(remotePath) {
873
- return path4.posix.basename(trimmedRemoteDirectory(remotePath));
841
+ return path.posix.basename(trimmedRemoteDirectory(remotePath));
874
842
  }
875
843
  async function isExistingLocalDirectory(localPath) {
876
- try {
877
- return (await stat3(localPath)).isDirectory();
878
- } catch {
879
- return false;
880
- }
844
+ try {
845
+ return (await stat(localPath)).isDirectory();
846
+ }
847
+ catch {
848
+ return false;
849
+ }
881
850
  }
882
851
  async function ensureLocalDirectory(localPath, createParents = false) {
883
- try {
884
- const stats = await stat3(localPath);
885
- if (stats.isDirectory()) {
886
- return;
852
+ try {
853
+ const stats = await stat(localPath);
854
+ if (stats.isDirectory()) {
855
+ return;
856
+ }
857
+ throw new TransferError(`Local destination already exists and is not a directory: ${localPath}`, {
858
+ context: { localPath }
859
+ });
887
860
  }
888
- throw new TransferError(`Local destination already exists and is not a directory: ${localPath}`, {
889
- context: { localPath }
890
- });
891
- } catch (error) {
892
- if (error instanceof TransferError) {
893
- throw error;
861
+ catch (error) {
862
+ if (error instanceof TransferError) {
863
+ throw error;
864
+ }
865
+ await mkdir(localPath, { recursive: createParents });
894
866
  }
895
- await mkdir2(localPath, { recursive: createParents });
896
- }
897
867
  }
898
- var ScpNextClientImpl = class {
899
- serverOptions;
900
- transport;
901
- commandExecutor;
902
- connected = false;
903
- commandExecutorConnected = false;
904
- constructor(serverOptions, dependencies = {}) {
905
- this.serverOptions = serverOptions;
906
- this.transport = dependencies.transport ?? new Ssh2SftpTransport();
907
- this.commandExecutor = dependencies.commandExecutor ?? new SshCommandExecutor();
908
- }
909
- async connect() {
910
- await validateServerOptions(this.serverOptions);
911
- await this.transport.connect(this.serverOptions);
912
- this.connected = true;
913
- }
914
- async upload(localPath, remotePath, options = {}) {
915
- const uploadOptions = {
916
- ...mergeOptions(this.serverOptions, options),
917
- localPath,
918
- remotePath
919
- };
920
- await validateUploadOptions(uploadOptions);
921
- if (options.dryRun) {
922
- return [];
923
- }
924
- await this.ensureConnected();
925
- await this.uploadWithTransport(uploadOptions);
926
- const results = [];
927
- for (const command of getPostUploadCommands(options) ?? []) {
928
- results.push(await this.exec(command));
929
- }
930
- return results;
931
- }
932
- async download(remotePath, localPath, options = {}) {
933
- const downloadOptions = {
934
- ...mergeOptions(this.serverOptions, options),
935
- remotePath,
936
- localPath
937
- };
938
- await validateDownloadOptions(downloadOptions);
939
- if (options.dryRun) {
940
- return;
868
+ class ScpNextClientImpl {
869
+ serverOptions;
870
+ transport;
871
+ commandExecutor;
872
+ connected = false;
873
+ commandExecutorConnected = false;
874
+ constructor(serverOptions, dependencies = {}) {
875
+ this.serverOptions = serverOptions;
876
+ this.transport = dependencies.transport ?? new Ssh2SftpTransport();
877
+ this.commandExecutor = dependencies.commandExecutor ?? new SshCommandExecutor();
941
878
  }
942
- await this.ensureConnected();
943
- await this.downloadWithTransport(downloadOptions);
944
- }
945
- async exec(command, options = {}) {
946
- if (typeof command !== "string" || !command.trim()) {
947
- throw new ValidationError("Remote command must not be empty.");
879
+ async connect() {
880
+ await validateServerOptions(this.serverOptions);
881
+ await this.transport.connect(this.serverOptions);
882
+ this.connected = true;
948
883
  }
949
- if (options.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout <= 0)) {
950
- throw new ValidationError("Command timeout must be a positive number.");
884
+ async upload(localPath, remotePath, options = {}) {
885
+ const uploadOptions = {
886
+ ...mergeOptions(this.serverOptions, options),
887
+ localPath,
888
+ remotePath
889
+ };
890
+ await validateUploadOptions(uploadOptions);
891
+ if (options.dryRun) {
892
+ return [];
893
+ }
894
+ await this.ensureConnected();
895
+ await this.uploadWithTransport(uploadOptions);
896
+ const results = [];
897
+ for (const command of getPostUploadCommands(options) ?? []) {
898
+ results.push(await this.exec(command));
899
+ }
900
+ return results;
951
901
  }
952
- if (options.maxBuffer !== void 0 && (!Number.isInteger(options.maxBuffer) || options.maxBuffer <= 0)) {
953
- throw new ValidationError("Command maxBuffer must be a positive integer.");
902
+ async download(remotePath, localPath, options = {}) {
903
+ const downloadOptions = {
904
+ ...mergeOptions(this.serverOptions, options),
905
+ remotePath,
906
+ localPath
907
+ };
908
+ await validateDownloadOptions(downloadOptions);
909
+ if (options.dryRun) {
910
+ return;
911
+ }
912
+ await this.ensureConnected();
913
+ await this.downloadWithTransport(downloadOptions);
954
914
  }
955
- if (options.failOnStderr !== void 0 && typeof options.failOnStderr !== "boolean") {
956
- throw new ValidationError("Command failOnStderr must be a boolean.");
915
+ async exec(command, options = {}) {
916
+ if (typeof command !== "string" || !command.trim()) {
917
+ throw new ValidationError("Remote command must not be empty.");
918
+ }
919
+ if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout <= 0)) {
920
+ throw new ValidationError("Command timeout must be a positive number.");
921
+ }
922
+ if (options.maxBuffer !== undefined &&
923
+ (!Number.isInteger(options.maxBuffer) || options.maxBuffer <= 0)) {
924
+ throw new ValidationError("Command maxBuffer must be a positive integer.");
925
+ }
926
+ if (options.failOnStderr !== undefined &&
927
+ typeof options.failOnStderr !== "boolean") {
928
+ throw new ValidationError("Command failOnStderr must be a boolean.");
929
+ }
930
+ await this.ensureCommandExecutorConnected();
931
+ let result;
932
+ try {
933
+ result = await this.commandExecutor.exec(command, options);
934
+ }
935
+ catch (error) {
936
+ if (error instanceof RemoteCommandError) {
937
+ throw new RemoteCommandError(error.message, {
938
+ exitCode: error.exitCode,
939
+ signal: error.signal,
940
+ stdout: redactKnownSensitiveValues(error.stdout, this.serverOptions),
941
+ stderr: redactKnownSensitiveValues(error.stderr, this.serverOptions)
942
+ });
943
+ }
944
+ throw error;
945
+ }
946
+ if (result.exitCode !== 0 || (options.failOnStderr && result.stderr.length > 0)) {
947
+ const reason = result.exitCode === null
948
+ ? "Remote command ended without an exit code."
949
+ : result.exitCode !== 0
950
+ ? `Remote command failed with exit code ${String(result.exitCode)}.`
951
+ : "Remote command wrote to stderr.";
952
+ throw new RemoteCommandError(reason, {
953
+ exitCode: result.exitCode,
954
+ signal: result.signal,
955
+ stdout: redactKnownSensitiveValues(result.stdout, this.serverOptions),
956
+ stderr: redactKnownSensitiveValues(result.stderr, this.serverOptions)
957
+ });
958
+ }
959
+ return result;
957
960
  }
958
- await this.ensureCommandExecutorConnected();
959
- let result;
960
- try {
961
- result = await this.commandExecutor.exec(command, options);
962
- } catch (error) {
963
- if (error instanceof RemoteCommandError) {
964
- throw new RemoteCommandError(error.message, {
965
- exitCode: error.exitCode,
966
- signal: error.signal,
967
- stdout: redactKnownSensitiveValues(error.stdout, this.serverOptions),
968
- stderr: redactKnownSensitiveValues(error.stderr, this.serverOptions)
969
- });
970
- }
971
- throw error;
972
- }
973
- if (result.exitCode !== 0 || options.failOnStderr && result.stderr.length > 0) {
974
- const reason = result.exitCode === null ? "Remote command ended without an exit code." : result.exitCode !== 0 ? `Remote command failed with exit code ${String(result.exitCode)}.` : "Remote command wrote to stderr.";
975
- throw new RemoteCommandError(reason, {
976
- exitCode: result.exitCode,
977
- signal: result.signal,
978
- stdout: redactKnownSensitiveValues(result.stdout, this.serverOptions),
979
- stderr: redactKnownSensitiveValues(result.stderr, this.serverOptions)
980
- });
961
+ async close() {
962
+ const closures = [];
963
+ if (this.connected)
964
+ closures.push(this.transport.close());
965
+ if (this.commandExecutorConnected)
966
+ closures.push(this.commandExecutor.close());
967
+ const results = await Promise.allSettled(closures);
968
+ this.connected = false;
969
+ this.commandExecutorConnected = false;
970
+ const failure = results.find((result) => result.status === "rejected");
971
+ if (failure) {
972
+ throw failure.reason;
973
+ }
981
974
  }
982
- return result;
983
- }
984
- async close() {
985
- const closures = [];
986
- if (this.connected) closures.push(this.transport.close());
987
- if (this.commandExecutorConnected) closures.push(this.commandExecutor.close());
988
- const results = await Promise.allSettled(closures);
989
- this.connected = false;
990
- this.commandExecutorConnected = false;
991
- const failure = results.find(
992
- (result) => result.status === "rejected"
993
- );
994
- if (failure) {
995
- throw failure.reason;
996
- }
997
- }
998
- async ensureConnected() {
999
- if (!this.connected) {
1000
- await this.connect();
1001
- }
1002
- }
1003
- async ensureCommandExecutorConnected() {
1004
- if (!this.commandExecutorConnected) {
1005
- await validateServerOptions(this.serverOptions);
1006
- await this.commandExecutor.connect(this.serverOptions);
1007
- this.commandExecutorConnected = true;
1008
- }
1009
- }
1010
- async uploadWithTransport(options) {
1011
- const localKind = await getLocalPathKind(options.localPath);
1012
- const remotePath = normalizeRemotePath(options.remotePath);
1013
- if (localKind === "directory" && !options.recursive) {
1014
- throw new ValidationError("Uploading a directory requires recursive mode.");
1015
- }
1016
- if (localKind === "file") {
1017
- const destinationFile = await this.resolveRemoteDestinationForLocalFile(options, remotePath);
1018
- const totalBytes2 = void 0;
1019
- await this.transport.uploadFile(resolveLocalPath(options.localPath), destinationFile, (step) => {
1020
- options.onProgress?.({
1021
- operation: "upload",
1022
- source: options.localPath,
1023
- destination: options.remotePath,
1024
- transferredBytes: step.transferredBytes,
1025
- totalBytes: step.totalBytes || totalBytes2,
1026
- percentage: percentage(step.transferredBytes, step.totalBytes),
1027
- currentFile: options.localPath
1028
- });
1029
- });
1030
- return;
1031
- }
1032
- const files = await walkLocalFiles(options.localPath);
1033
- const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1034
- let completedBytes = 0;
1035
- let completedFiles = 0;
1036
- const destinationRoot = await this.resolveRemoteDestinationForLocalDirectory(
1037
- options,
1038
- remotePath
1039
- );
1040
- for (const file of files) {
1041
- const destinationFile = remoteJoin(
1042
- destinationRoot,
1043
- file.relativePath.split(path4.sep).join("/")
1044
- );
1045
- await this.ensureRemoteParent(destinationFile, true);
1046
- await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1047
- await this.transport.uploadFile(file.absolutePath, destinationFile, (step) => {
1048
- const transferredBytes = completedBytes + step.transferredBytes;
1049
- options.onProgress?.({
1050
- operation: "upload",
1051
- source: options.localPath,
1052
- destination: options.remotePath,
1053
- transferredBytes,
1054
- totalBytes,
1055
- percentage: percentage(transferredBytes, totalBytes),
1056
- currentFile: file.relativePath,
1057
- completedFiles,
1058
- totalFiles: files.length
1059
- });
1060
- });
1061
- completedBytes += file.size;
1062
- completedFiles += 1;
1063
- }
1064
- }
1065
- async resolveRemoteDestinationForLocalFile(options, remotePath) {
1066
- const remoteKind = await this.transport.exists(remotePath);
1067
- const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1068
- if (!isDirectoryLikeDestination) {
1069
- await this.ensureRemoteParent(remotePath, shouldCreateDirectories(options));
1070
- await this.assertRemoteOverwrite(remotePath, options.overwrite);
1071
- return remotePath;
1072
- }
1073
- if (remoteKind && remoteKind !== "d") {
1074
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1075
- context: { remotePath }
1076
- });
1077
- }
1078
- const remoteDirectory = remotePath.replace(/\/+$/g, "") || "/";
1079
- if (!remoteKind) {
1080
- if (!shouldCreateDirectories(options)) {
1081
- throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1082
- context: { remotePath }
1083
- });
1084
- }
1085
- await this.transport.mkdir(remoteDirectory, true);
1086
- }
1087
- const destinationFile = remoteJoin(
1088
- remoteDirectory,
1089
- path4.basename(resolveLocalPath(options.localPath))
1090
- );
1091
- await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1092
- return destinationFile;
1093
- }
1094
- async resolveRemoteDestinationForLocalDirectory(options, remotePath) {
1095
- const remoteKind = await this.transport.exists(remotePath);
1096
- const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1097
- const localDirectoryName = path4.basename(resolveLocalPath(options.localPath));
1098
- if (remoteKind && remoteKind !== "d") {
1099
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1100
- context: { remotePath }
1101
- });
1102
- }
1103
- if (isDirectoryLikeDestination) {
1104
- const remoteDirectory = trimmedRemoteDirectory(remotePath);
1105
- if (!remoteKind) {
1106
- if (!shouldCreateDirectories(options)) {
1107
- throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1108
- context: { remotePath }
1109
- });
1110
- }
1111
- await this.transport.mkdir(remoteDirectory, true);
1112
- }
1113
- const destinationRoot = remoteJoin(remoteDirectory, localDirectoryName);
1114
- await this.ensureRemoteDirectory(destinationRoot);
1115
- return destinationRoot;
1116
- }
1117
- if (!remoteKind) {
1118
- if (!shouldCreateDirectories(options)) {
1119
- throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
1120
- context: { remotePath }
1121
- });
1122
- }
1123
- await this.transport.mkdir(remotePath, true);
1124
- }
1125
- return remotePath;
1126
- }
1127
- async downloadWithTransport(options) {
1128
- const remotePath = normalizeRemotePath(options.remotePath);
1129
- const remoteKind = await this.transport.exists(remotePath);
1130
- if (!remoteKind) {
1131
- throw new TransferError(`Remote path does not exist: ${options.remotePath}`, {
1132
- context: { remotePath: options.remotePath }
1133
- });
1134
- }
1135
- if (remoteKind === "d") {
1136
- if (!options.recursive) {
1137
- throw new ValidationError("Downloading a directory requires recursive mode.");
1138
- }
1139
- await this.downloadDirectory(options);
1140
- return;
1141
- }
1142
- await this.downloadSingleFile(options, remotePath);
1143
- }
1144
- async downloadSingleFile(options, remotePath) {
1145
- const localPath = await this.resolveLocalDestinationForRemoteFile(options, remotePath);
1146
- await this.assertLocalOverwrite(localPath, options.overwrite);
1147
- if (shouldCreateDirectories(options)) {
1148
- await mkdir2(path4.dirname(localPath), { recursive: true });
1149
- }
1150
- await this.transport.downloadFile(remotePath, localPath, (step) => {
1151
- options.onProgress?.({
1152
- operation: "download",
1153
- source: options.remotePath,
1154
- destination: options.localPath,
1155
- transferredBytes: step.transferredBytes,
1156
- totalBytes: step.totalBytes,
1157
- percentage: percentage(step.transferredBytes, step.totalBytes),
1158
- currentFile: options.remotePath
1159
- });
1160
- });
1161
- }
1162
- async downloadDirectory(options) {
1163
- const remotePath = normalizeRemotePath(options.remotePath);
1164
- const localRoot = await this.resolveLocalDestinationForRemoteDirectory(options, remotePath);
1165
- const files = await walkRemoteFiles(this.transport, remotePath);
1166
- const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1167
- let completedBytes = 0;
1168
- let completedFiles = 0;
1169
- await ensureLocalDirectory(localRoot, shouldCreateDirectories(options));
1170
- for (const file of files) {
1171
- const localFile = path4.join(localRoot, ...file.relativePath.split("/"));
1172
- await this.assertLocalOverwrite(localFile, options.overwrite);
1173
- await mkdir2(path4.dirname(localFile), { recursive: true });
1174
- await this.transport.downloadFile(file.remotePath, localFile, (step) => {
1175
- const transferredBytes = completedBytes + step.transferredBytes;
1176
- options.onProgress?.({
1177
- operation: "download",
1178
- source: options.remotePath,
1179
- destination: options.localPath,
1180
- transferredBytes,
1181
- totalBytes,
1182
- percentage: percentage(transferredBytes, totalBytes),
1183
- currentFile: file.relativePath,
1184
- completedFiles,
1185
- totalFiles: files.length
1186
- });
1187
- });
1188
- completedBytes += file.size;
1189
- completedFiles += 1;
1190
- }
1191
- }
1192
- async resolveLocalDestinationForRemoteFile(options, remotePath) {
1193
- const localDestination = resolveLocalPath(options.localPath);
1194
- const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
1195
- if (!isDirectoryLikeDestination) {
1196
- return localDestination;
1197
- }
1198
- if (!await isExistingLocalDirectory(localDestination)) {
1199
- if (!shouldCreateDirectories(options)) {
1200
- throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1201
- context: { localPath: options.localPath }
1202
- });
1203
- }
1204
- await mkdir2(localDestination, { recursive: true });
1205
- }
1206
- return path4.join(localDestination, remoteBasename(remotePath));
1207
- }
1208
- async resolveLocalDestinationForRemoteDirectory(options, remotePath) {
1209
- const localDestination = resolveLocalPath(options.localPath);
1210
- const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
1211
- if (!isDirectoryLikeDestination) {
1212
- return localDestination;
1213
- }
1214
- if (!await isExistingLocalDirectory(localDestination)) {
1215
- if (!shouldCreateDirectories(options)) {
1216
- throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1217
- context: { localPath: options.localPath }
975
+ async ensureConnected() {
976
+ if (!this.connected) {
977
+ await this.connect();
978
+ }
979
+ }
980
+ async ensureCommandExecutorConnected() {
981
+ if (!this.commandExecutorConnected) {
982
+ await validateServerOptions(this.serverOptions);
983
+ await this.commandExecutor.connect(this.serverOptions);
984
+ this.commandExecutorConnected = true;
985
+ }
986
+ }
987
+ async uploadWithTransport(options) {
988
+ const localKind = await getLocalPathKind(options.localPath);
989
+ const remotePath = normalizeRemotePath(options.remotePath);
990
+ if (localKind === "directory" && !options.recursive) {
991
+ throw new ValidationError("Uploading a directory requires recursive mode.");
992
+ }
993
+ if (localKind === "file") {
994
+ const destinationFile = await this.resolveRemoteDestinationForLocalFile(options, remotePath);
995
+ const totalBytes = undefined;
996
+ await this.transport.uploadFile(resolveLocalPath(options.localPath), destinationFile, (step) => {
997
+ options.onProgress?.({
998
+ operation: "upload",
999
+ source: options.localPath,
1000
+ destination: options.remotePath,
1001
+ transferredBytes: step.transferredBytes,
1002
+ totalBytes: step.totalBytes || totalBytes,
1003
+ percentage: percentage(step.transferredBytes, step.totalBytes),
1004
+ currentFile: options.localPath
1005
+ });
1006
+ });
1007
+ return;
1008
+ }
1009
+ const files = await walkLocalFiles(options.localPath);
1010
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1011
+ let completedBytes = 0;
1012
+ let completedFiles = 0;
1013
+ const destinationRoot = await this.resolveRemoteDestinationForLocalDirectory(options, remotePath);
1014
+ for (const file of files) {
1015
+ const destinationFile = remoteJoin(destinationRoot, file.relativePath.split(path.sep).join("/"));
1016
+ await this.ensureRemoteParent(destinationFile, true);
1017
+ await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1018
+ await this.transport.uploadFile(file.absolutePath, destinationFile, (step) => {
1019
+ const transferredBytes = completedBytes + step.transferredBytes;
1020
+ options.onProgress?.({
1021
+ operation: "upload",
1022
+ source: options.localPath,
1023
+ destination: options.remotePath,
1024
+ transferredBytes,
1025
+ totalBytes,
1026
+ percentage: percentage(transferredBytes, totalBytes),
1027
+ currentFile: file.relativePath,
1028
+ completedFiles,
1029
+ totalFiles: files.length
1030
+ });
1031
+ });
1032
+ completedBytes += file.size;
1033
+ completedFiles += 1;
1034
+ }
1035
+ }
1036
+ async resolveRemoteDestinationForLocalFile(options, remotePath) {
1037
+ const remoteKind = await this.transport.exists(remotePath);
1038
+ const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1039
+ if (!isDirectoryLikeDestination) {
1040
+ await this.ensureRemoteParent(remotePath, shouldCreateDirectories(options));
1041
+ await this.assertRemoteOverwrite(remotePath, options.overwrite);
1042
+ return remotePath;
1043
+ }
1044
+ if (remoteKind && remoteKind !== "d") {
1045
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1046
+ context: { remotePath }
1047
+ });
1048
+ }
1049
+ const remoteDirectory = remotePath.replace(/\/+$/g, "") || "/";
1050
+ if (!remoteKind) {
1051
+ if (!shouldCreateDirectories(options)) {
1052
+ throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1053
+ context: { remotePath }
1054
+ });
1055
+ }
1056
+ await this.transport.mkdir(remoteDirectory, true);
1057
+ }
1058
+ const destinationFile = remoteJoin(remoteDirectory, path.basename(resolveLocalPath(options.localPath)));
1059
+ await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1060
+ return destinationFile;
1061
+ }
1062
+ async resolveRemoteDestinationForLocalDirectory(options, remotePath) {
1063
+ const remoteKind = await this.transport.exists(remotePath);
1064
+ const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1065
+ const localDirectoryName = path.basename(resolveLocalPath(options.localPath));
1066
+ if (remoteKind && remoteKind !== "d") {
1067
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1068
+ context: { remotePath }
1069
+ });
1070
+ }
1071
+ if (isDirectoryLikeDestination) {
1072
+ const remoteDirectory = trimmedRemoteDirectory(remotePath);
1073
+ if (!remoteKind) {
1074
+ if (!shouldCreateDirectories(options)) {
1075
+ throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1076
+ context: { remotePath }
1077
+ });
1078
+ }
1079
+ await this.transport.mkdir(remoteDirectory, true);
1080
+ }
1081
+ const destinationRoot = remoteJoin(remoteDirectory, localDirectoryName);
1082
+ await this.ensureRemoteDirectory(destinationRoot);
1083
+ return destinationRoot;
1084
+ }
1085
+ if (!remoteKind) {
1086
+ if (!shouldCreateDirectories(options)) {
1087
+ throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
1088
+ context: { remotePath }
1089
+ });
1090
+ }
1091
+ await this.transport.mkdir(remotePath, true);
1092
+ }
1093
+ return remotePath;
1094
+ }
1095
+ async downloadWithTransport(options) {
1096
+ const remotePath = normalizeRemotePath(options.remotePath);
1097
+ const remoteKind = await this.transport.exists(remotePath);
1098
+ if (!remoteKind) {
1099
+ throw new TransferError(`Remote path does not exist: ${options.remotePath}`, {
1100
+ context: { remotePath: options.remotePath }
1101
+ });
1102
+ }
1103
+ if (remoteKind === "d") {
1104
+ if (!options.recursive) {
1105
+ throw new ValidationError("Downloading a directory requires recursive mode.");
1106
+ }
1107
+ await this.downloadDirectory(options);
1108
+ return;
1109
+ }
1110
+ await this.downloadSingleFile(options, remotePath);
1111
+ }
1112
+ async downloadSingleFile(options, remotePath) {
1113
+ const localPath = await this.resolveLocalDestinationForRemoteFile(options, remotePath);
1114
+ await this.assertLocalOverwrite(localPath, options.overwrite);
1115
+ if (shouldCreateDirectories(options)) {
1116
+ await mkdir(path.dirname(localPath), { recursive: true });
1117
+ }
1118
+ await this.transport.downloadFile(remotePath, localPath, (step) => {
1119
+ options.onProgress?.({
1120
+ operation: "download",
1121
+ source: options.remotePath,
1122
+ destination: options.localPath,
1123
+ transferredBytes: step.transferredBytes,
1124
+ totalBytes: step.totalBytes,
1125
+ percentage: percentage(step.transferredBytes, step.totalBytes),
1126
+ currentFile: options.remotePath
1127
+ });
1218
1128
  });
1219
- }
1220
- await mkdir2(localDestination, { recursive: true });
1221
1129
  }
1222
- return path4.join(localDestination, remoteBasename(remotePath));
1223
- }
1224
- async ensureRemoteParent(remotePath, createDirectories = false) {
1225
- const parent = remoteDirname(remotePath);
1226
- if (parent === "." || parent === "/") {
1227
- return;
1130
+ async downloadDirectory(options) {
1131
+ const remotePath = normalizeRemotePath(options.remotePath);
1132
+ const localRoot = await this.resolveLocalDestinationForRemoteDirectory(options, remotePath);
1133
+ const files = await walkRemoteFiles(this.transport, remotePath);
1134
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1135
+ let completedBytes = 0;
1136
+ let completedFiles = 0;
1137
+ await ensureLocalDirectory(localRoot, shouldCreateDirectories(options));
1138
+ for (const file of files) {
1139
+ const localFile = path.join(localRoot, ...file.relativePath.split("/"));
1140
+ await this.assertLocalOverwrite(localFile, options.overwrite);
1141
+ await mkdir(path.dirname(localFile), { recursive: true });
1142
+ await this.transport.downloadFile(file.remotePath, localFile, (step) => {
1143
+ const transferredBytes = completedBytes + step.transferredBytes;
1144
+ options.onProgress?.({
1145
+ operation: "download",
1146
+ source: options.remotePath,
1147
+ destination: options.localPath,
1148
+ transferredBytes,
1149
+ totalBytes,
1150
+ percentage: percentage(transferredBytes, totalBytes),
1151
+ currentFile: file.relativePath,
1152
+ completedFiles,
1153
+ totalFiles: files.length
1154
+ });
1155
+ });
1156
+ completedBytes += file.size;
1157
+ completedFiles += 1;
1158
+ }
1228
1159
  }
1229
- if (createDirectories) {
1230
- await this.transport.mkdir(parent, true);
1231
- return;
1232
- }
1233
- const exists = await this.transport.exists(parent);
1234
- if (exists !== "d") {
1235
- throw new TransferError(`Remote destination parent does not exist: ${parent}`, {
1236
- context: { remotePath }
1237
- });
1238
- }
1239
- }
1240
- async ensureRemoteDirectory(remotePath) {
1241
- const exists = await this.transport.exists(remotePath);
1242
- if (exists && exists !== "d") {
1243
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1244
- context: { remotePath }
1245
- });
1246
- }
1247
- if (!exists) {
1248
- await this.transport.mkdir(remotePath, true);
1249
- }
1250
- }
1251
- async assertRemoteOverwrite(remotePath, overwrite = false) {
1252
- if (overwrite) {
1253
- return;
1254
- }
1255
- const exists = await this.transport.exists(remotePath);
1256
- if (exists) {
1257
- throw new TransferError(
1258
- `Remote destination already exists: ${remotePath}. Use --overwrite to overwrite it.`,
1259
- {
1260
- context: { remotePath }
1261
- }
1262
- );
1263
- }
1264
- }
1265
- async assertLocalOverwrite(localPath, overwrite = false) {
1266
- if (overwrite) {
1267
- return;
1160
+ async resolveLocalDestinationForRemoteFile(options, remotePath) {
1161
+ const localDestination = resolveLocalPath(options.localPath);
1162
+ const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || (await isExistingLocalDirectory(localDestination));
1163
+ if (!isDirectoryLikeDestination) {
1164
+ return localDestination;
1165
+ }
1166
+ if (!(await isExistingLocalDirectory(localDestination))) {
1167
+ if (!shouldCreateDirectories(options)) {
1168
+ throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1169
+ context: { localPath: options.localPath }
1170
+ });
1171
+ }
1172
+ await mkdir(localDestination, { recursive: true });
1173
+ }
1174
+ return path.join(localDestination, remoteBasename(remotePath));
1268
1175
  }
1269
- try {
1270
- await access4(localPath);
1271
- throw new TransferError(`Local destination already exists: ${localPath}`, {
1272
- context: { localPath }
1273
- });
1274
- } catch (error) {
1275
- if (error instanceof TransferError) {
1276
- throw error;
1277
- }
1176
+ async resolveLocalDestinationForRemoteDirectory(options, remotePath) {
1177
+ const localDestination = resolveLocalPath(options.localPath);
1178
+ const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || (await isExistingLocalDirectory(localDestination));
1179
+ if (!isDirectoryLikeDestination) {
1180
+ return localDestination;
1181
+ }
1182
+ if (!(await isExistingLocalDirectory(localDestination))) {
1183
+ if (!shouldCreateDirectories(options)) {
1184
+ throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1185
+ context: { localPath: options.localPath }
1186
+ });
1187
+ }
1188
+ await mkdir(localDestination, { recursive: true });
1189
+ }
1190
+ return path.join(localDestination, remoteBasename(remotePath));
1278
1191
  }
1279
- }
1280
- };
1192
+ async ensureRemoteParent(remotePath, createDirectories = false) {
1193
+ const parent = remoteDirname(remotePath);
1194
+ if (parent === "." || parent === "/") {
1195
+ return;
1196
+ }
1197
+ if (createDirectories) {
1198
+ await this.transport.mkdir(parent, true);
1199
+ return;
1200
+ }
1201
+ const exists = await this.transport.exists(parent);
1202
+ if (exists !== "d") {
1203
+ throw new TransferError(`Remote destination parent does not exist: ${parent}`, {
1204
+ context: { remotePath }
1205
+ });
1206
+ }
1207
+ }
1208
+ async ensureRemoteDirectory(remotePath) {
1209
+ const exists = await this.transport.exists(remotePath);
1210
+ if (exists && exists !== "d") {
1211
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1212
+ context: { remotePath }
1213
+ });
1214
+ }
1215
+ if (!exists) {
1216
+ await this.transport.mkdir(remotePath, true);
1217
+ }
1218
+ }
1219
+ async assertRemoteOverwrite(remotePath, overwrite = false) {
1220
+ if (overwrite) {
1221
+ return;
1222
+ }
1223
+ const exists = await this.transport.exists(remotePath);
1224
+ if (exists) {
1225
+ throw new TransferError(`Remote destination already exists: ${remotePath}. Use --overwrite to overwrite it.`, {
1226
+ context: { remotePath }
1227
+ });
1228
+ }
1229
+ }
1230
+ async assertLocalOverwrite(localPath, overwrite = false) {
1231
+ if (overwrite) {
1232
+ return;
1233
+ }
1234
+ try {
1235
+ await access(localPath);
1236
+ throw new TransferError(`Local destination already exists: ${localPath}`, {
1237
+ context: { localPath }
1238
+ });
1239
+ }
1240
+ catch (error) {
1241
+ if (error instanceof TransferError) {
1242
+ throw error;
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1281
1247
  function createClient(options) {
1282
- return new ScpNextClientImpl(options);
1248
+ return new ScpNextClientImpl(options);
1283
1249
  }
1284
1250
 
1285
- // src/client/upload.ts
1286
1251
  async function upload(options) {
1287
- const client = new ScpNextClientImpl(options);
1288
- let result;
1289
- try {
1290
- result = await client.upload(options.localPath, options.remotePath, options);
1291
- } catch (error) {
1252
+ const client = new ScpNextClientImpl(options);
1253
+ let result;
1292
1254
  try {
1293
- await client.close();
1294
- } catch {
1255
+ result = await client.upload(options.localPath, options.remotePath, options);
1256
+ }
1257
+ catch (error) {
1258
+ try {
1259
+ await client.close();
1260
+ }
1261
+ catch {
1262
+ // Preserve the upload or command failure as the primary error.
1263
+ }
1264
+ throw error;
1295
1265
  }
1296
- throw error;
1297
- }
1298
- await client.close();
1299
- return result;
1266
+ await client.close();
1267
+ return result;
1300
1268
  }
1301
1269
 
1302
- // src/client/download.ts
1303
1270
  async function download(options) {
1304
- const client = new ScpNextClientImpl(options);
1305
- try {
1306
- await client.download(options.remotePath, options.localPath, options);
1307
- } finally {
1308
- await client.close();
1309
- }
1271
+ const client = new ScpNextClientImpl(options);
1272
+ try {
1273
+ await client.download(options.remotePath, options.localPath, options);
1274
+ }
1275
+ finally {
1276
+ await client.close();
1277
+ }
1310
1278
  }
1311
1279
 
1312
- // src/client/copy.ts
1313
1280
  async function copy(options) {
1314
- if (options.source.type === "local" && options.destination.type === "remote") {
1315
- await upload({
1316
- ...options,
1317
- localPath: options.source.path,
1318
- remotePath: options.destination.path
1319
- });
1320
- return;
1321
- }
1322
- if (options.source.type === "remote" && options.destination.type === "local") {
1323
- await download({
1324
- ...options,
1325
- remotePath: options.source.path,
1326
- localPath: options.destination.path
1281
+ if (options.source.type === "local" && options.destination.type === "remote") {
1282
+ await upload({
1283
+ ...options,
1284
+ localPath: options.source.path,
1285
+ remotePath: options.destination.path
1286
+ });
1287
+ return;
1288
+ }
1289
+ if (options.source.type === "remote" && options.destination.type === "local") {
1290
+ await download({
1291
+ ...options,
1292
+ remotePath: options.source.path,
1293
+ localPath: options.destination.path
1294
+ });
1295
+ return;
1296
+ }
1297
+ throw new ValidationError("copy() supports only local-to-remote or remote-to-local transfers.", {
1298
+ context: {
1299
+ sourceType: options.source.type,
1300
+ destinationType: options.destination.type
1301
+ }
1327
1302
  });
1328
- return;
1329
- }
1330
- throw new ValidationError("copy() supports only local-to-remote or remote-to-local transfers.", {
1331
- context: {
1332
- sourceType: options.source.type,
1333
- destinationType: options.destination.type
1334
- }
1335
- });
1336
1303
  }
1337
- export {
1338
- AuthenticationError,
1339
- ConfigurationError,
1340
- ConnectionError,
1341
- FileSystemError,
1342
- HostVerificationError,
1343
- RemoteCommandError,
1344
- ScpNextClientImpl,
1345
- ScpNextError,
1346
- TransferError,
1347
- ValidationError,
1348
- copy,
1349
- createClient,
1350
- download,
1351
- toScpNextError,
1352
- upload
1353
- };
1354
- //# sourceMappingURL=index.js.map
1304
+
1305
+ export { AuthenticationError, ConfigurationError, ConnectionError, FileSystemError, HostVerificationError, RemoteCommandError, ScpNextClientImpl, ScpNextError, TransferError, ValidationError, copy, createClient, download, toScpNextError, upload };
1306
+ //# sourceMappingURL=index.js.map