scp-next 1.1.2 → 1.1.4

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/cli/index.js CHANGED
@@ -1,1708 +1,1665 @@
1
1
  #!/usr/bin/env node
2
+ import { constants, realpathSync, readFileSync } from 'node:fs';
3
+ import path, { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Option, Command, CommanderError } from 'commander';
6
+ import { stat, access, mkdir, readFile, readdir } from 'node:fs/promises';
7
+ import SftpClient from 'ssh2-sftp-client';
8
+ import { createHash } from 'node:crypto';
9
+ import os from 'node:os';
10
+ import { Client } from 'ssh2';
11
+ import { z } from 'zod';
12
+ import { assignDefined, formatByteSize } from 'mazey';
2
13
 
3
- // src/cli/index.ts
4
- import { readFileSync, realpathSync } from "fs";
5
- import { dirname, resolve } from "path";
6
- import { fileURLToPath } from "url";
7
- import { Command as Command4, CommanderError } from "commander";
8
-
9
- // src/client/client.ts
10
- import { access as access4, mkdir as mkdir2, stat as stat3 } from "fs/promises";
11
- import path4 from "path";
12
-
13
- // src/client/transport.ts
14
- import SftpClient from "ssh2-sftp-client";
15
-
16
- // src/security/redact.ts
17
- var SENSITIVE_KEYS = [
18
- "password",
19
- "passphrase",
20
- "privatekey",
21
- "privatekeyfile",
22
- "token",
23
- "authorization",
24
- "credential",
25
- "secret"
14
+ const SENSITIVE_KEYS = [
15
+ "password",
16
+ "passphrase",
17
+ "privatekey",
18
+ "privatekeyfile",
19
+ "token",
20
+ "authorization",
21
+ "credential",
22
+ "secret"
26
23
  ];
27
- var PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
28
- var SECRET_OPTION_PATTERN = /(--(?:password|passphrase|token|secret|authorization|credential)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi;
29
- var SECRET_ASSIGNMENT_PATTERN = /(\b(?:password|passphrase|token|secret|authorization|credential)[A-Z0-9_-]*\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
24
+ const PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
25
+ const SECRET_OPTION_PATTERN = /(--(?:password|passphrase|token|secret|authorization|credential)(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/gi;
26
+ const SECRET_ASSIGNMENT_PATTERN = /(\b(?:password|passphrase|token|secret|authorization|credential)[A-Z0-9_-]*\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
30
27
  function isSensitiveKey(key) {
31
- const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
32
- return SENSITIVE_KEYS.some((sensitive) => normalized.includes(sensitive));
28
+ const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
29
+ return SENSITIVE_KEYS.some((sensitive) => normalized.includes(sensitive));
33
30
  }
34
31
  function redactSensitiveValues(value) {
35
- if (typeof value === "string") {
36
- return value.replace(PRIVATE_KEY_PATTERN, "[REDACTED]");
37
- }
38
- if (Array.isArray(value)) {
39
- const redacted = value.map((item) => redactSensitiveValues(item));
40
- return redacted;
41
- }
42
- if (value && typeof value === "object") {
43
- const redacted = {};
44
- for (const [key, nestedValue] of Object.entries(value)) {
45
- redacted[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveValues(nestedValue);
46
- }
47
- return redacted;
48
- }
49
- return value;
32
+ if (typeof value === "string") {
33
+ return value.replace(PRIVATE_KEY_PATTERN, "[REDACTED]");
34
+ }
35
+ if (Array.isArray(value)) {
36
+ const redacted = value.map((item) => redactSensitiveValues(item));
37
+ return redacted;
38
+ }
39
+ if (value && typeof value === "object") {
40
+ const redacted = {};
41
+ for (const [key, nestedValue] of Object.entries(value)) {
42
+ redacted[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveValues(nestedValue);
43
+ }
44
+ return redacted;
45
+ }
46
+ return value;
50
47
  }
51
48
  function formatErrorMessage(error) {
52
- if (error instanceof Error) {
53
- return redactKnownSensitiveValues(error.message, {});
54
- }
55
- return redactKnownSensitiveValues(String(error), {});
49
+ if (error instanceof Error) {
50
+ return redactKnownSensitiveValues(error.message, {});
51
+ }
52
+ return redactKnownSensitiveValues(String(error), {});
56
53
  }
57
54
  function redactKnownSensitiveValues(text, source) {
58
- const sensitiveValues = collectSensitiveValues(source);
59
- const redactedKnownValues = sensitiveValues.reduce(
60
- (redacted, value) => redacted.split(value).join("[REDACTED]"),
61
- String(redactSensitiveValues(text))
62
- );
63
- return redactedKnownValues.replace(SECRET_OPTION_PATTERN, "$1[REDACTED]").replace(SECRET_ASSIGNMENT_PATTERN, "$1[REDACTED]");
55
+ const sensitiveValues = collectSensitiveValues(source);
56
+ const redactedKnownValues = sensitiveValues.reduce((redacted, value) => redacted.split(value).join("[REDACTED]"), String(redactSensitiveValues(text)));
57
+ return redactedKnownValues
58
+ .replace(SECRET_OPTION_PATTERN, "$1[REDACTED]")
59
+ .replace(SECRET_ASSIGNMENT_PATTERN, "$1[REDACTED]");
64
60
  }
65
61
  function collectSensitiveValues(value, key = "") {
66
- if (isSensitiveKey(key)) {
67
- if (typeof value === "string" && value) {
68
- return [value];
69
- }
70
- if (Buffer.isBuffer(value) && value.length > 0) {
71
- return [value.toString("utf8")];
72
- }
73
- return [];
74
- }
75
- if (!value || typeof value !== "object") {
76
- return [];
77
- }
78
- return Object.entries(value).flatMap(
79
- ([nestedKey, nestedValue]) => collectSensitiveValues(nestedValue, nestedKey)
80
- );
62
+ if (isSensitiveKey(key)) {
63
+ if (typeof value === "string" && value) {
64
+ return [value];
65
+ }
66
+ if (Buffer.isBuffer(value) && value.length > 0) {
67
+ return [value.toString("utf8")];
68
+ }
69
+ return [];
70
+ }
71
+ if (!value || typeof value !== "object") {
72
+ return [];
73
+ }
74
+ return Object.entries(value).flatMap(([nestedKey, nestedValue]) => collectSensitiveValues(nestedValue, nestedKey));
81
75
  }
82
76
 
83
- // src/errors/index.ts
84
- var ScpNextError = class extends Error {
85
- code;
86
- context;
87
- constructor(message, options = {}) {
88
- super(message, { cause: options.cause });
89
- this.name = new.target.name;
90
- this.code = options.code ?? "SCP_NEXT_ERROR";
91
- this.context = redactSensitiveValues(options.context ?? {});
92
- }
93
- };
94
- var ConfigurationError = class extends ScpNextError {
95
- constructor(message, options = {}) {
96
- super(message, { ...options, code: "SCP_NEXT_CONFIGURATION_ERROR" });
97
- }
98
- };
99
- var ValidationError = class extends ScpNextError {
100
- constructor(message, options = {}) {
101
- super(message, { ...options, code: "SCP_NEXT_VALIDATION_ERROR" });
102
- }
103
- };
104
- var AuthenticationError = class extends ScpNextError {
105
- constructor(message, options = {}) {
106
- super(message, { ...options, code: "SCP_NEXT_AUTHENTICATION_ERROR" });
107
- }
108
- };
109
- var ConnectionError = class extends ScpNextError {
110
- constructor(message, options = {}) {
111
- super(message, { ...options, code: "SCP_NEXT_CONNECTION_ERROR" });
112
- }
113
- };
114
- var TransferError = class extends ScpNextError {
115
- constructor(message, options = {}) {
116
- super(message, { ...options, code: "SCP_NEXT_TRANSFER_ERROR" });
117
- }
118
- };
119
- var RemoteCommandError = class extends ScpNextError {
120
- exitCode;
121
- signal;
122
- stdout;
123
- stderr;
124
- constructor(message, options) {
125
- super(message, {
126
- code: "SCP_NEXT_REMOTE_COMMAND_ERROR",
127
- ...options.cause !== void 0 ? { cause: options.cause } : {},
128
- ...options.context !== void 0 ? { context: options.context } : {}
129
- });
130
- this.exitCode = options.exitCode;
131
- this.signal = options.signal;
132
- this.stdout = options.stdout ?? "";
133
- this.stderr = options.stderr ?? "";
134
- }
135
- };
136
- var FileSystemError = class extends ScpNextError {
137
- constructor(message, options = {}) {
138
- super(message, { ...options, code: "SCP_NEXT_FILESYSTEM_ERROR" });
139
- }
140
- };
141
- var HostVerificationError = class extends ScpNextError {
142
- constructor(message, options = {}) {
143
- super(message, { ...options, code: "SCP_NEXT_HOST_VERIFICATION_ERROR" });
144
- }
145
- };
77
+ class ScpNextError extends Error {
78
+ code;
79
+ context;
80
+ constructor(message, options = {}) {
81
+ super(message, { cause: options.cause });
82
+ this.name = new.target.name;
83
+ this.code = options.code ?? "SCP_NEXT_ERROR";
84
+ this.context = redactSensitiveValues(options.context ?? {});
85
+ }
86
+ }
87
+ class ConfigurationError extends ScpNextError {
88
+ constructor(message, options = {}) {
89
+ super(message, { ...options, code: "SCP_NEXT_CONFIGURATION_ERROR" });
90
+ }
91
+ }
92
+ class ValidationError extends ScpNextError {
93
+ constructor(message, options = {}) {
94
+ super(message, { ...options, code: "SCP_NEXT_VALIDATION_ERROR" });
95
+ }
96
+ }
97
+ class AuthenticationError extends ScpNextError {
98
+ constructor(message, options = {}) {
99
+ super(message, { ...options, code: "SCP_NEXT_AUTHENTICATION_ERROR" });
100
+ }
101
+ }
102
+ class ConnectionError extends ScpNextError {
103
+ constructor(message, options = {}) {
104
+ super(message, { ...options, code: "SCP_NEXT_CONNECTION_ERROR" });
105
+ }
106
+ }
107
+ class TransferError extends ScpNextError {
108
+ constructor(message, options = {}) {
109
+ super(message, { ...options, code: "SCP_NEXT_TRANSFER_ERROR" });
110
+ }
111
+ }
112
+ class RemoteCommandError extends ScpNextError {
113
+ exitCode;
114
+ signal;
115
+ stdout;
116
+ stderr;
117
+ constructor(message, options) {
118
+ super(message, {
119
+ code: "SCP_NEXT_REMOTE_COMMAND_ERROR",
120
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
121
+ ...(options.context !== undefined ? { context: options.context } : {})
122
+ });
123
+ this.exitCode = options.exitCode;
124
+ this.signal = options.signal;
125
+ this.stdout = options.stdout ?? "";
126
+ this.stderr = options.stderr ?? "";
127
+ }
128
+ }
129
+ class FileSystemError extends ScpNextError {
130
+ constructor(message, options = {}) {
131
+ super(message, { ...options, code: "SCP_NEXT_FILESYSTEM_ERROR" });
132
+ }
133
+ }
134
+ class HostVerificationError extends ScpNextError {
135
+ constructor(message, options = {}) {
136
+ super(message, { ...options, code: "SCP_NEXT_HOST_VERIFICATION_ERROR" });
137
+ }
138
+ }
146
139
  function toScpNextError(error) {
147
- if (error instanceof ScpNextError) {
148
- return error;
149
- }
150
- const message = error instanceof Error ? error.message : "Unexpected scp-next error.";
151
- const lowerMessage = message.toLowerCase();
152
- if (lowerMessage.includes("authentication") || lowerMessage.includes("permission denied")) {
153
- return new AuthenticationError("SSH authentication failed.", { cause: error });
154
- }
155
- if (lowerMessage.includes("host verification") || lowerMessage.includes("host key") || lowerMessage.includes("fingerprint") || lowerMessage.includes("host") && lowerMessage.includes("denied")) {
156
- return new HostVerificationError("SSH host verification failed.", { cause: error });
157
- }
158
- if (lowerMessage.includes("timed out") || lowerMessage.includes("connection") || lowerMessage.includes("econn")) {
159
- return new ConnectionError("SSH connection failed.", { cause: error });
160
- }
161
- return new TransferError(String(redactSensitiveValues(message)), { cause: error });
140
+ if (error instanceof ScpNextError) {
141
+ return error;
142
+ }
143
+ const message = error instanceof Error ? error.message : "Unexpected scp-next error.";
144
+ const lowerMessage = message.toLowerCase();
145
+ if (lowerMessage.includes("authentication") || lowerMessage.includes("permission denied")) {
146
+ return new AuthenticationError("SSH authentication failed.", { cause: error });
147
+ }
148
+ if (lowerMessage.includes("host verification") ||
149
+ lowerMessage.includes("host key") ||
150
+ lowerMessage.includes("fingerprint") ||
151
+ (lowerMessage.includes("host") && lowerMessage.includes("denied"))) {
152
+ return new HostVerificationError("SSH host verification failed.", { cause: error });
153
+ }
154
+ if (lowerMessage.includes("timed out") ||
155
+ lowerMessage.includes("connection") ||
156
+ lowerMessage.includes("econn")) {
157
+ return new ConnectionError("SSH connection failed.", { cause: error });
158
+ }
159
+ return new TransferError(String(redactSensitiveValues(message)), { cause: error });
162
160
  }
163
161
 
164
- // src/client/ssh-options.ts
165
- import { readFile as readFile2 } from "fs/promises";
166
-
167
- // src/security/host-verification.ts
168
- import { createHash } from "crypto";
169
- import { access as access2, readFile } from "fs/promises";
170
-
171
- // src/paths/local-path.ts
172
- import { constants } from "fs";
173
- import { access, mkdir, stat } from "fs/promises";
174
- import os from "os";
175
- import path from "path";
176
162
  function expandHome(inputPath) {
177
- if (inputPath === "~") {
178
- return os.homedir();
179
- }
180
- if (inputPath.startsWith(`~${path.sep}`) || inputPath.startsWith("~/")) {
181
- return path.join(os.homedir(), inputPath.slice(2));
182
- }
183
- return inputPath;
163
+ if (inputPath === "~") {
164
+ return os.homedir();
165
+ }
166
+ if (inputPath.startsWith(`~${path.sep}`) || inputPath.startsWith("~/")) {
167
+ return path.join(os.homedir(), inputPath.slice(2));
168
+ }
169
+ return inputPath;
184
170
  }
185
171
  function resolveLocalPath(inputPath, baseDir = process.cwd()) {
186
- const expanded = expandHome(inputPath);
187
- return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(baseDir, expanded);
172
+ const expanded = expandHome(inputPath);
173
+ return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(baseDir, expanded);
188
174
  }
189
175
  async function assertLocalPathExists(inputPath) {
190
- try {
191
- await access(resolveLocalPath(inputPath), constants.F_OK);
192
- } catch (error) {
193
- throw new FileSystemError(`Local path does not exist: ${inputPath}`, {
194
- cause: error,
195
- context: { path: inputPath }
196
- });
197
- }
176
+ try {
177
+ await access(resolveLocalPath(inputPath), constants.F_OK);
178
+ }
179
+ catch (error) {
180
+ throw new FileSystemError(`Local path does not exist: ${inputPath}`, {
181
+ cause: error,
182
+ context: { path: inputPath }
183
+ });
184
+ }
198
185
  }
199
186
  async function assertDownloadDestination(inputPath, createDirectories = false) {
200
- const resolved = resolveLocalPath(inputPath);
201
- const parent = path.dirname(resolved);
202
- if (createDirectories) {
203
- await mkdir(parent, { recursive: true });
204
- return;
205
- }
206
- try {
207
- await access(parent, constants.W_OK);
208
- } catch (error) {
209
- throw new FileSystemError(`Local destination parent does not exist or is not writable: ${parent}`, {
210
- cause: error,
211
- context: { path: inputPath }
212
- });
213
- }
187
+ const resolved = resolveLocalPath(inputPath);
188
+ const parent = path.dirname(resolved);
189
+ if (createDirectories) {
190
+ await mkdir(parent, { recursive: true });
191
+ return;
192
+ }
193
+ try {
194
+ await access(parent, constants.W_OK);
195
+ }
196
+ catch (error) {
197
+ throw new FileSystemError(`Local destination parent does not exist or is not writable: ${parent}`, {
198
+ cause: error,
199
+ context: { path: inputPath }
200
+ });
201
+ }
214
202
  }
215
203
  async function getLocalPathKind(inputPath) {
216
- const stats = await stat(resolveLocalPath(inputPath));
217
- if (stats.isDirectory()) {
218
- return "directory";
219
- }
220
- if (stats.isFile()) {
221
- return "file";
222
- }
223
- throw new ValidationError(`Local path must be a file or directory: ${inputPath}`, {
224
- context: { path: inputPath }
225
- });
204
+ const stats = await stat(resolveLocalPath(inputPath));
205
+ if (stats.isDirectory()) {
206
+ return "directory";
207
+ }
208
+ if (stats.isFile()) {
209
+ return "file";
210
+ }
211
+ throw new ValidationError(`Local path must be a file or directory: ${inputPath}`, {
212
+ context: { path: inputPath }
213
+ });
226
214
  }
227
215
 
228
- // src/security/host-verification.ts
229
216
  function normalizeFingerprint(fingerprint) {
230
- return fingerprint.trim().replace(/^SHA256:/i, "").replace(/^sha256:/i, "").replace(/=+$/g, "");
217
+ return fingerprint
218
+ .trim()
219
+ .replace(/^SHA256:/i, "")
220
+ .replace(/^sha256:/i, "")
221
+ .replace(/=+$/g, "");
231
222
  }
232
223
  function isHexFingerprint(fingerprint) {
233
- return /^[0-9a-f]+$/i.test(fingerprint);
224
+ return /^[0-9a-f]+$/i.test(fingerprint);
234
225
  }
235
226
  function fingerprintCandidates(fingerprint) {
236
- const normalized = normalizeFingerprint(fingerprint);
237
- const candidates = /* @__PURE__ */ new Set([normalized]);
238
- if (isHexFingerprint(normalized)) {
239
- candidates.add(normalized.toLowerCase());
240
- } else {
241
- try {
242
- candidates.add(Buffer.from(normalized, "base64").toString("hex"));
243
- } catch {
227
+ const normalized = normalizeFingerprint(fingerprint);
228
+ const candidates = new Set([normalized]);
229
+ if (isHexFingerprint(normalized)) {
230
+ candidates.add(normalized.toLowerCase());
244
231
  }
245
- }
246
- return [...candidates];
232
+ else {
233
+ try {
234
+ candidates.add(Buffer.from(normalized, "base64").toString("hex"));
235
+ }
236
+ catch {
237
+ // Keep the original normalized value when it is not valid base64.
238
+ }
239
+ }
240
+ return [...candidates];
247
241
  }
248
242
  function isFingerprintMatch(actual, expected) {
249
- const normalizedActual = normalizeFingerprint(actual);
250
- const normalizedExpected = normalizeFingerprint(expected);
251
- if (normalizedActual === normalizedExpected) {
252
- return true;
253
- }
254
- return isHexFingerprint(normalizedActual) && isHexFingerprint(normalizedExpected) && normalizedActual.toLowerCase() === normalizedExpected.toLowerCase();
243
+ const normalizedActual = normalizeFingerprint(actual);
244
+ const normalizedExpected = normalizeFingerprint(expected);
245
+ if (normalizedActual === normalizedExpected) {
246
+ return true;
247
+ }
248
+ return (isHexFingerprint(normalizedActual) &&
249
+ isHexFingerprint(normalizedExpected) &&
250
+ normalizedActual.toLowerCase() === normalizedExpected.toLowerCase());
255
251
  }
256
252
  function hostVerificationHint(host) {
257
- return [
258
- "",
259
- "Typical fix:",
260
- ` ssh ${host}`,
261
- "",
262
- "If you trust the host key, accept it and retry the scp-next command.",
263
- "Alternatively configure hostFingerprint or knownHostsFile explicitly."
264
- ].join("\n");
253
+ return [
254
+ "",
255
+ "Typical fix:",
256
+ ` ssh ${host}`,
257
+ "",
258
+ "If you trust the host key, accept it and retry the scp-next command.",
259
+ "Alternatively configure hostFingerprint or knownHostsFile explicitly."
260
+ ].join("\n");
265
261
  }
266
262
  async function resolveAllowedFingerprints(options) {
267
- const fingerprints = /* @__PURE__ */ new Set();
268
- const knownHostsFile = options.knownHostsFile ?? "~/.ssh/known_hosts";
269
- if (!options.host) {
270
- throw new HostVerificationError("A host is required for SSH host verification.");
271
- }
272
- if (options.hostFingerprint) {
273
- for (const fingerprint of fingerprintCandidates(options.hostFingerprint)) {
274
- fingerprints.add(fingerprint);
275
- }
276
- }
277
- try {
278
- await access2(expandHome(knownHostsFile));
279
- const content = await readFile(expandHome(knownHostsFile), "utf8");
280
- for (const line of content.split(/\r?\n/)) {
281
- const trimmed = line.trim();
282
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("@")) {
283
- continue;
284
- }
285
- const [hosts, algorithm, key] = trimmed.split(/\s+/);
286
- const hostPatterns = hosts?.split(",") ?? [];
287
- const portHost = options.port ? `[${options.host}]:${options.port}` : void 0;
288
- const matchesHost = hostPatterns.includes(options.host) || Boolean(portHost && hostPatterns.includes(portHost));
289
- if (!hosts || !algorithm || !key || !matchesHost) {
290
- continue;
291
- }
292
- const keyBuffer = Buffer.from(key, "base64");
293
- fingerprints.add(createHash("sha256").update(keyBuffer).digest("base64").replace(/=+$/g, ""));
294
- fingerprints.add(createHash("sha256").update(keyBuffer).digest("hex"));
295
- }
296
- } catch (error) {
297
- if (options.knownHostsFile || !options.hostFingerprint) {
298
- throw new HostVerificationError(
299
- "SSH host verification requires hostFingerprint or an accessible knownHostsFile.",
300
- { cause: error, context: { knownHostsFile } }
301
- );
302
- }
303
- }
304
- if (fingerprints.size === 0) {
305
- throw new HostVerificationError(
306
- `SSH host verification failed before connecting: no matching host fingerprint was configured.${hostVerificationHint(
307
- options.host
308
- )}`
309
- );
310
- }
311
- return fingerprints;
263
+ const fingerprints = new Set();
264
+ const knownHostsFile = options.knownHostsFile ?? "~/.ssh/known_hosts";
265
+ if (!options.host) {
266
+ throw new HostVerificationError("A host is required for SSH host verification.");
267
+ }
268
+ if (options.hostFingerprint) {
269
+ for (const fingerprint of fingerprintCandidates(options.hostFingerprint)) {
270
+ fingerprints.add(fingerprint);
271
+ }
272
+ }
273
+ try {
274
+ await access(expandHome(knownHostsFile));
275
+ const content = await readFile(expandHome(knownHostsFile), "utf8");
276
+ for (const line of content.split(/\r?\n/)) {
277
+ const trimmed = line.trim();
278
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("@")) {
279
+ continue;
280
+ }
281
+ const [hosts, algorithm, key] = trimmed.split(/\s+/);
282
+ const hostPatterns = hosts?.split(",") ?? [];
283
+ const portHost = options.port ? `[${options.host}]:${options.port}` : undefined;
284
+ const matchesHost = hostPatterns.includes(options.host) || Boolean(portHost && hostPatterns.includes(portHost));
285
+ if (!hosts || !algorithm || !key || !matchesHost) {
286
+ continue;
287
+ }
288
+ const keyBuffer = Buffer.from(key, "base64");
289
+ fingerprints.add(createHash("sha256").update(keyBuffer).digest("base64").replace(/=+$/g, ""));
290
+ fingerprints.add(createHash("sha256").update(keyBuffer).digest("hex"));
291
+ }
292
+ }
293
+ catch (error) {
294
+ if (options.knownHostsFile || !options.hostFingerprint) {
295
+ throw new HostVerificationError("SSH host verification requires hostFingerprint or an accessible knownHostsFile.", { cause: error, context: { knownHostsFile } });
296
+ }
297
+ }
298
+ if (fingerprints.size === 0) {
299
+ throw new HostVerificationError(`SSH host verification failed before connecting: no matching host fingerprint was configured.${hostVerificationHint(options.host)}`);
300
+ }
301
+ return fingerprints;
312
302
  }
313
303
  function createHostVerifier(allowedFingerprints) {
314
- return (hashedKey) => {
315
- for (const expected of allowedFingerprints) {
316
- if (isFingerprintMatch(hashedKey, expected)) {
317
- return true;
318
- }
319
- }
320
- return false;
321
- };
304
+ return (hashedKey) => {
305
+ for (const expected of allowedFingerprints) {
306
+ if (isFingerprintMatch(hashedKey, expected)) {
307
+ return true;
308
+ }
309
+ }
310
+ return false;
311
+ };
322
312
  }
323
313
 
324
- // src/client/ssh-options.ts
325
314
  async function createSshConnectOptions(options) {
326
- const connectOptions = {};
327
- if (options.host !== void 0) connectOptions.host = options.host;
328
- if (options.port !== void 0) connectOptions.port = options.port;
329
- if (options.username !== void 0) connectOptions.username = options.username;
330
- if (options.password !== void 0) connectOptions.password = options.password;
331
- if (options.privateKey !== void 0) {
332
- connectOptions.privateKey = options.privateKey;
333
- } else if (options.privateKeyFile) {
334
- connectOptions.privateKey = await readFile2(
335
- expandHome(options.privateKeyFile),
336
- "utf8"
337
- );
338
- }
339
- if (options.passphrase !== void 0) connectOptions.passphrase = options.passphrase;
340
- const agent = options.agent ?? process.env.SSH_AUTH_SOCK;
341
- if (agent !== void 0) connectOptions.agent = agent;
342
- if (options.timeout !== void 0) connectOptions.readyTimeout = options.timeout;
343
- const allowedFingerprints = await resolveAllowedFingerprints(options);
344
- const hostVerifier = createHostVerifier(allowedFingerprints);
345
- if (hostVerifier) {
346
- connectOptions.hostHash = "sha256";
347
- connectOptions.hostVerifier = hostVerifier;
348
- }
349
- return connectOptions;
315
+ const connectOptions = {};
316
+ if (options.host !== undefined)
317
+ connectOptions.host = options.host;
318
+ if (options.port !== undefined)
319
+ connectOptions.port = options.port;
320
+ if (options.username !== undefined)
321
+ connectOptions.username = options.username;
322
+ if (options.password !== undefined)
323
+ connectOptions.password = options.password;
324
+ if (options.privateKey !== undefined) {
325
+ connectOptions.privateKey = options.privateKey;
326
+ }
327
+ else if (options.privateKeyFile) {
328
+ connectOptions.privateKey = await readFile(expandHome(options.privateKeyFile), "utf8");
329
+ }
330
+ if (options.passphrase !== undefined)
331
+ connectOptions.passphrase = options.passphrase;
332
+ const agent = options.agent ?? process.env.SSH_AUTH_SOCK;
333
+ if (agent !== undefined)
334
+ connectOptions.agent = agent;
335
+ if (options.timeout !== undefined)
336
+ connectOptions.readyTimeout = options.timeout;
337
+ const allowedFingerprints = await resolveAllowedFingerprints(options);
338
+ const hostVerifier = createHostVerifier(allowedFingerprints);
339
+ if (hostVerifier) {
340
+ connectOptions.hostHash = "sha256";
341
+ connectOptions.hostVerifier = hostVerifier;
342
+ }
343
+ return connectOptions;
350
344
  }
351
345
 
352
- // src/client/transport.ts
353
346
  function toRemoteFileInfo(info) {
354
- return {
355
- type: info.type,
356
- name: info.name,
357
- size: info.size
358
- };
359
- }
360
- var Ssh2SftpTransport = class {
361
- client;
362
- constructor(name = "scp-next") {
363
- this.client = new SftpClient(name, {
364
- error: () => void 0,
365
- end: () => void 0,
366
- close: () => void 0
367
- });
368
- }
369
- async connect(options) {
370
- const connectOptions = await createSshConnectOptions(options);
371
- try {
372
- await this.client.connect(connectOptions);
373
- } catch (error) {
374
- const converted = toScpNextError(error);
375
- if (converted instanceof HostVerificationError) {
376
- throw new HostVerificationError(
377
- `SSH host verification failed during connection: ${formatErrorMessage(
378
- error
379
- )}.${hostVerificationTroubleshooting(options.host)}`,
380
- { cause: error, context: connectionContext(options) }
381
- );
382
- }
383
- if (isLikelyHostVerificationFailure(error)) {
384
- throw new HostVerificationError(
385
- `SSH host verification failed during connection.${hostVerificationTroubleshooting(
386
- options.host
387
- )}`,
388
- { cause: error, context: connectionContext(options) }
389
- );
390
- }
391
- throw new ConnectionError(`Unable to connect to SSH server: ${formatErrorMessage(error)}`, {
392
- cause: error,
393
- context: connectionContext(options)
394
- });
395
- }
396
- }
397
- async close() {
398
- await this.client.end();
399
- }
400
- async exists(remotePath) {
401
- return this.client.exists(remotePath);
402
- }
403
- async mkdir(remotePath, recursive = true) {
404
- await this.client.mkdir(remotePath, recursive);
405
- }
406
- async list(remotePath) {
407
- const files = await this.client.list(remotePath);
408
- return files.map(toRemoteFileInfo);
409
- }
410
- async uploadFile(localPath, remotePath, onStep) {
411
- try {
412
- await this.client.fastPut(localPath, remotePath, this.transferOptions(onStep));
413
- } catch (error) {
414
- throw new TransferError(`Upload failed for ${localPath}: ${formatErrorMessage(error)}`, {
415
- cause: error,
416
- context: { localPath, remotePath }
417
- });
418
- }
419
- }
420
- async downloadFile(remotePath, localPath, onStep) {
421
- try {
422
- await this.client.fastGet(remotePath, localPath, this.transferOptions(onStep));
423
- } catch (error) {
424
- throw new TransferError(`Download failed for ${remotePath}: ${formatErrorMessage(error)}`, {
425
- cause: error,
426
- context: { remotePath, localPath }
427
- });
428
- }
429
- }
430
- transferOptions(onStep) {
431
- if (!onStep) {
432
- return void 0;
433
- }
434
347
  return {
435
- step(transferredBytes, chunkBytes, totalBytes) {
436
- onStep({ transferredBytes, chunkBytes, totalBytes });
437
- }
348
+ type: info.type,
349
+ name: info.name,
350
+ size: info.size
438
351
  };
439
- }
440
- };
352
+ }
353
+ class Ssh2SftpTransport {
354
+ client;
355
+ constructor(name = "scp-next") {
356
+ this.client = new SftpClient(name, {
357
+ error: () => undefined,
358
+ end: () => undefined,
359
+ close: () => undefined
360
+ });
361
+ }
362
+ async connect(options) {
363
+ const connectOptions = await createSshConnectOptions(options);
364
+ try {
365
+ await this.client.connect(connectOptions);
366
+ }
367
+ catch (error) {
368
+ const converted = toScpNextError(error);
369
+ if (converted instanceof HostVerificationError) {
370
+ throw new HostVerificationError(`SSH host verification failed during connection: ${formatErrorMessage(error)}.${hostVerificationTroubleshooting(options.host)}`, { cause: error, context: connectionContext(options) });
371
+ }
372
+ if (isLikelyHostVerificationFailure(error)) {
373
+ throw new HostVerificationError(`SSH host verification failed during connection.${hostVerificationTroubleshooting(options.host)}`, { cause: error, context: connectionContext(options) });
374
+ }
375
+ throw new ConnectionError(`Unable to connect to SSH server: ${formatErrorMessage(error)}`, {
376
+ cause: error,
377
+ context: connectionContext(options)
378
+ });
379
+ }
380
+ }
381
+ async close() {
382
+ await this.client.end();
383
+ }
384
+ async exists(remotePath) {
385
+ return this.client.exists(remotePath);
386
+ }
387
+ async mkdir(remotePath, recursive = true) {
388
+ await this.client.mkdir(remotePath, recursive);
389
+ }
390
+ async list(remotePath) {
391
+ const files = await this.client.list(remotePath);
392
+ return files.map(toRemoteFileInfo);
393
+ }
394
+ async uploadFile(localPath, remotePath, onStep) {
395
+ try {
396
+ await this.client.fastPut(localPath, remotePath, this.transferOptions(onStep));
397
+ }
398
+ catch (error) {
399
+ throw new TransferError(`Upload failed for ${localPath}: ${formatErrorMessage(error)}`, {
400
+ cause: error,
401
+ context: { localPath, remotePath }
402
+ });
403
+ }
404
+ }
405
+ async downloadFile(remotePath, localPath, onStep) {
406
+ try {
407
+ await this.client.fastGet(remotePath, localPath, this.transferOptions(onStep));
408
+ }
409
+ catch (error) {
410
+ throw new TransferError(`Download failed for ${remotePath}: ${formatErrorMessage(error)}`, {
411
+ cause: error,
412
+ context: { remotePath, localPath }
413
+ });
414
+ }
415
+ }
416
+ transferOptions(onStep) {
417
+ if (!onStep) {
418
+ return undefined;
419
+ }
420
+ return {
421
+ step(transferredBytes, chunkBytes, totalBytes) {
422
+ onStep({ transferredBytes, chunkBytes, totalBytes });
423
+ }
424
+ };
425
+ }
426
+ }
441
427
  function connectionContext(options) {
442
- return {
443
- host: options.host,
444
- port: options.port,
445
- username: options.username
446
- };
428
+ return {
429
+ host: options.host,
430
+ port: options.port,
431
+ username: options.username
432
+ };
447
433
  }
448
434
  function isLikelyHostVerificationFailure(error) {
449
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
450
- return message.includes("host") && (message.includes("verif") || message.includes("fingerprint") || message.includes("host key") || message.includes("key mismatch") || message.includes("denied"));
435
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
436
+ return (message.includes("host") &&
437
+ (message.includes("verif") ||
438
+ message.includes("fingerprint") ||
439
+ message.includes("host key") ||
440
+ message.includes("key mismatch") ||
441
+ message.includes("denied")));
451
442
  }
452
443
  function hostVerificationTroubleshooting(host) {
453
- const displayHost = host ?? "<host>";
454
- return [
455
- "",
456
- "",
457
- "Typical fix:",
458
- ` ssh ${displayHost}`,
459
- "",
460
- "If plain ssh works but scp-next fails, check that the host entry is available in",
461
- "`~/.ssh/known_hosts` for the same host and port used by scp-next. For non-default ports,",
462
- "OpenSSH usually stores entries as `[host]:port`.",
463
- "You can also configure hostFingerprint or knownHostsFile explicitly."
464
- ].join("\n");
444
+ const displayHost = host ?? "<host>";
445
+ return [
446
+ "",
447
+ "",
448
+ "Typical fix:",
449
+ ` ssh ${displayHost}`,
450
+ "",
451
+ "If plain ssh works but scp-next fails, check that the host entry is available in",
452
+ "`~/.ssh/known_hosts` for the same host and port used by scp-next. For non-default ports,",
453
+ "OpenSSH usually stores entries as `[host]:port`.",
454
+ "You can also configure hostFingerprint or knownHostsFile explicitly."
455
+ ].join("\n");
465
456
  }
466
457
 
467
- // src/client/command-executor.ts
468
- import { Client } from "ssh2";
469
- var SshCommandExecutor = class {
470
- client;
471
- connected = false;
472
- closed = true;
473
- constructor(client = new Client()) {
474
- this.client = client;
475
- this.client.on("error", () => {
476
- this.connected = false;
477
- });
478
- this.client.on("close", () => {
479
- this.connected = false;
480
- this.closed = true;
481
- });
482
- }
483
- async connect(options) {
484
- const connectOptions = await createSshConnectOptions(options);
485
- this.closed = false;
486
- await new Promise((resolve2, reject) => {
487
- const onReady = () => {
488
- cleanup();
489
- this.connected = true;
490
- resolve2();
491
- };
492
- const onError = (error) => {
493
- cleanup();
458
+ class SshCommandExecutor {
459
+ client;
460
+ connected = false;
461
+ closed = true;
462
+ constructor(client = new Client()) {
463
+ this.client = client;
464
+ this.client.on("error", () => {
465
+ this.connected = false;
466
+ });
467
+ this.client.on("close", () => {
468
+ this.connected = false;
469
+ this.closed = true;
470
+ });
471
+ }
472
+ async connect(options) {
473
+ const connectOptions = await createSshConnectOptions(options);
474
+ this.closed = false;
475
+ await new Promise((resolve, reject) => {
476
+ const onReady = () => {
477
+ cleanup();
478
+ this.connected = true;
479
+ resolve();
480
+ };
481
+ const onError = (error) => {
482
+ cleanup();
483
+ this.connected = false;
484
+ this.closed = true;
485
+ try {
486
+ this.client.end();
487
+ }
488
+ catch {
489
+ // The failed connection may not have a writable socket.
490
+ }
491
+ const converted = toScpNextError(error);
492
+ if (converted instanceof HostVerificationError) {
493
+ reject(converted);
494
+ return;
495
+ }
496
+ reject(new ConnectionError(`Unable to connect command executor to SSH server: ${formatErrorMessage(error)}`, {
497
+ cause: error,
498
+ context: {
499
+ host: options.host,
500
+ port: options.port,
501
+ username: options.username
502
+ }
503
+ }));
504
+ };
505
+ const onClose = () => {
506
+ cleanup();
507
+ reject(new ConnectionError("SSH connection closed before it was ready."));
508
+ };
509
+ const cleanup = () => {
510
+ this.client.off("ready", onReady);
511
+ this.client.off("error", onError);
512
+ this.client.off("close", onClose);
513
+ };
514
+ this.client.once("ready", onReady);
515
+ this.client.once("error", onError);
516
+ this.client.once("close", onClose);
517
+ try {
518
+ this.client.connect(connectOptions);
519
+ }
520
+ catch (error) {
521
+ cleanup();
522
+ this.closed = true;
523
+ reject(new ConnectionError(`Unable to start SSH connection: ${formatErrorMessage(error)}`));
524
+ }
525
+ });
526
+ }
527
+ async exec(command, options = {}) {
528
+ if (!this.connected) {
529
+ throw new ConnectionError("Command executor is not connected.");
530
+ }
531
+ return new Promise((resolve, reject) => {
532
+ try {
533
+ this.client.exec(command, (error, stream) => {
534
+ if (error) {
535
+ reject(new RemoteCommandError("Unable to start remote command.", {
536
+ exitCode: null
537
+ }));
538
+ return;
539
+ }
540
+ collectCommandResult(command, stream, options, resolve, reject);
541
+ });
542
+ }
543
+ catch (error) {
544
+ reject(new ConnectionError(`Unable to start remote command: ${formatErrorMessage(error)}`));
545
+ }
546
+ });
547
+ }
548
+ async close() {
549
+ if (this.closed) {
550
+ return;
551
+ }
494
552
  this.connected = false;
495
- this.closed = true;
553
+ await new Promise((resolve) => {
554
+ this.client.once("close", resolve);
555
+ this.client.end();
556
+ });
557
+ }
558
+ }
559
+ function collectCommandResult(command, stream, options, resolve, reject) {
560
+ let stdout = "";
561
+ let stderr = "";
562
+ let outputBytes = 0;
563
+ let settled = false;
564
+ const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
565
+ const cleanup = () => {
566
+ if (timer)
567
+ clearTimeout(timer);
568
+ stream.off("data", onStdoutData);
569
+ stream.stderr.off("data", onStderrData);
570
+ stream.off("close", onClose);
571
+ stream.off("error", onStreamError);
572
+ stream.stderr.off("error", onStreamError);
573
+ };
574
+ const fail = (error) => {
575
+ if (settled)
576
+ return;
577
+ settled = true;
578
+ cleanup();
496
579
  try {
497
- this.client.end();
498
- } catch {
580
+ stream.close();
499
581
  }
500
- const converted = toScpNextError(error);
501
- if (converted instanceof HostVerificationError) {
502
- reject(converted);
503
- return;
582
+ catch {
583
+ // The connection may already be closed.
504
584
  }
505
- reject(
506
- new ConnectionError(
507
- `Unable to connect command executor to SSH server: ${formatErrorMessage(error)}`,
508
- {
509
- cause: error,
510
- context: {
511
- host: options.host,
512
- port: options.port,
513
- username: options.username
514
- }
515
- }
516
- )
517
- );
518
- };
519
- const onClose = () => {
520
- cleanup();
521
- reject(new ConnectionError("SSH connection closed before it was ready."));
522
- };
523
- const cleanup = () => {
524
- this.client.off("ready", onReady);
525
- this.client.off("error", onError);
526
- this.client.off("close", onClose);
527
- };
528
- this.client.once("ready", onReady);
529
- this.client.once("error", onError);
530
- this.client.once("close", onClose);
531
- try {
532
- this.client.connect(connectOptions);
533
- } catch (error) {
534
- cleanup();
535
- this.closed = true;
536
- reject(
537
- new ConnectionError(
538
- `Unable to start SSH connection: ${formatErrorMessage(error)}`
539
- )
540
- );
541
- }
542
- });
543
- }
544
- async exec(command, options = {}) {
545
- if (!this.connected) {
546
- throw new ConnectionError("Command executor is not connected.");
547
- }
548
- return new Promise((resolve2, reject) => {
549
- try {
550
- this.client.exec(command, (error, stream) => {
551
- if (error) {
552
- reject(
553
- new RemoteCommandError("Unable to start remote command.", {
554
- exitCode: null
555
- })
556
- );
585
+ reject(error);
586
+ };
587
+ const appendOutput = (target, chunk) => {
588
+ outputBytes += Buffer.byteLength(chunk);
589
+ if (outputBytes > maxBuffer) {
590
+ fail(new RemoteCommandError(`Remote command output exceeded maxBuffer of ${maxBuffer} bytes.`, {
591
+ exitCode: null,
592
+ stdout,
593
+ stderr
594
+ }));
557
595
  return;
558
- }
559
- collectCommandResult(command, stream, options, resolve2, reject);
560
- });
561
- } catch (error) {
562
- reject(
563
- new ConnectionError(
564
- `Unable to start remote command: ${formatErrorMessage(error)}`
565
- )
566
- );
567
- }
568
- });
569
- }
570
- async close() {
571
- if (this.closed) {
572
- return;
573
- }
574
- this.connected = false;
575
- await new Promise((resolve2) => {
576
- this.client.once("close", resolve2);
577
- this.client.end();
578
- });
579
- }
580
- };
581
- function collectCommandResult(command, stream, options, resolve2, reject) {
582
- let stdout = "";
583
- let stderr = "";
584
- let outputBytes = 0;
585
- let settled = false;
586
- const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
587
- const cleanup = () => {
588
- if (timer) clearTimeout(timer);
589
- stream.off("data", onStdoutData);
590
- stream.stderr.off("data", onStderrData);
591
- stream.off("close", onClose);
592
- stream.off("error", onStreamError);
593
- stream.stderr.off("error", onStreamError);
594
- };
595
- const fail = (error) => {
596
- if (settled) return;
597
- settled = true;
598
- cleanup();
599
- try {
600
- stream.close();
601
- } catch {
602
- }
603
- reject(error);
604
- };
605
- const appendOutput = (target, chunk) => {
606
- outputBytes += Buffer.byteLength(chunk);
607
- if (outputBytes > maxBuffer) {
608
- fail(
609
- new RemoteCommandError(
610
- `Remote command output exceeded maxBuffer of ${maxBuffer} bytes.`,
611
- {
596
+ }
597
+ if (target === "stdout")
598
+ stdout += chunk;
599
+ else
600
+ stderr += chunk;
601
+ };
602
+ const onStreamError = () => {
603
+ fail(new RemoteCommandError("Remote command stream failed.", {
612
604
  exitCode: null,
613
605
  stdout,
614
606
  stderr
615
- }
616
- )
617
- );
618
- return;
619
- }
620
- if (target === "stdout") stdout += chunk;
621
- else stderr += chunk;
622
- };
623
- const onStreamError = () => {
624
- fail(
625
- new RemoteCommandError("Remote command stream failed.", {
626
- exitCode: null,
627
- stdout,
628
- stderr
629
- })
630
- );
631
- };
632
- const onStdoutData = (chunk) => {
633
- appendOutput("stdout", chunk);
634
- };
635
- const onStderrData = (chunk) => {
636
- appendOutput("stderr", chunk);
637
- };
638
- const onClose = (exitCode, signal) => {
639
- if (settled) return;
640
- settled = true;
641
- cleanup();
642
- resolve2({
643
- command,
644
- stdout,
645
- stderr,
646
- exitCode: typeof exitCode === "number" ? exitCode : null,
647
- ...signal ? { signal } : {}
648
- });
649
- };
650
- const timer = options.timeout === void 0 ? void 0 : setTimeout(() => {
651
- fail(
652
- new RemoteCommandError(
653
- `Remote command timed out after ${options.timeout} ms.`,
654
- {
655
- exitCode: null,
656
- stdout,
657
- stderr
658
- }
659
- )
660
- );
661
- }, options.timeout);
662
- stream.setEncoding("utf8");
663
- stream.stderr.setEncoding("utf8");
664
- stream.on("data", onStdoutData);
665
- stream.stderr.on("data", onStderrData);
666
- stream.once("error", onStreamError);
667
- stream.stderr.once("error", onStreamError);
668
- stream.once("close", onClose);
607
+ }));
608
+ };
609
+ const onStdoutData = (chunk) => {
610
+ appendOutput("stdout", chunk);
611
+ };
612
+ const onStderrData = (chunk) => {
613
+ appendOutput("stderr", chunk);
614
+ };
615
+ const onClose = (exitCode, signal) => {
616
+ if (settled)
617
+ return;
618
+ settled = true;
619
+ cleanup();
620
+ resolve({
621
+ command,
622
+ stdout,
623
+ stderr,
624
+ exitCode: typeof exitCode === "number" ? exitCode : null,
625
+ ...(signal ? { signal } : {})
626
+ });
627
+ };
628
+ const timer = options.timeout === undefined
629
+ ? undefined
630
+ : setTimeout(() => {
631
+ fail(new RemoteCommandError(`Remote command timed out after ${options.timeout} ms.`, {
632
+ exitCode: null,
633
+ stdout,
634
+ stderr
635
+ }));
636
+ }, options.timeout);
637
+ stream.setEncoding("utf8");
638
+ stream.stderr.setEncoding("utf8");
639
+ stream.on("data", onStdoutData);
640
+ stream.stderr.on("data", onStderrData);
641
+ stream.once("error", onStreamError);
642
+ stream.stderr.once("error", onStreamError);
643
+ stream.once("close", onClose);
669
644
  }
670
645
 
671
- // src/client/walk.ts
672
- import { readdir, stat as stat2 } from "fs/promises";
673
- import path3 from "path";
674
-
675
- // src/paths/remote-path.ts
676
- import path2 from "path";
677
646
  function assertRemotePath(inputPath, label = "remote path") {
678
- if (!inputPath || inputPath.trim().length === 0) {
679
- throw new ValidationError(`Invalid ${label}: remote paths must not be empty.`, {
680
- context: { path: inputPath }
681
- });
682
- }
647
+ if (!inputPath || inputPath.trim().length === 0) {
648
+ throw new ValidationError(`Invalid ${label}: remote paths must not be empty.`, {
649
+ context: { path: inputPath }
650
+ });
651
+ }
683
652
  }
684
653
  function normalizeRemotePath(inputPath) {
685
- assertRemotePath(inputPath);
686
- return path2.posix.normalize(inputPath.replace(/\\/g, "/"));
654
+ assertRemotePath(inputPath);
655
+ return path.posix.normalize(inputPath.replace(/\\/g, "/"));
687
656
  }
688
657
  function restoreMsysConvertedRemotePath(inputPath) {
689
- const normalized = inputPath.replace(/\\/g, "/");
690
- const match = /^[A-Za-z]:\/(?:Program Files\/Git|Program Files \(x86\)\/Git|Git|msys64)\/(.+)$/i.exec(
691
- normalized
692
- );
693
- return match ? `/${match[1]}` : inputPath;
658
+ const normalized = inputPath.replace(/\\/g, "/");
659
+ const match = /^[A-Za-z]:\/(?:Program Files\/Git|Program Files \(x86\)\/Git|Git|msys64)\/(.+)$/i.exec(normalized);
660
+ return match ? `/${match[1]}` : inputPath;
694
661
  }
695
662
  function remoteJoin(...parts) {
696
- const joined = path2.posix.join(...parts);
697
- assertRemotePath(joined);
698
- return joined;
663
+ const joined = path.posix.join(...parts);
664
+ assertRemotePath(joined);
665
+ return joined;
699
666
  }
700
667
  function remoteDirname(inputPath) {
701
- assertRemotePath(inputPath);
702
- return path2.posix.dirname(inputPath);
668
+ assertRemotePath(inputPath);
669
+ return path.posix.dirname(inputPath);
703
670
  }
704
671
 
705
- // src/client/walk.ts
706
672
  async function walkLocalFiles(rootPath) {
707
- const resolvedRoot = resolveLocalPath(rootPath);
708
- const stats = await stat2(resolvedRoot);
709
- if (stats.isFile()) {
710
- return [
711
- {
712
- absolutePath: resolvedRoot,
713
- relativePath: path3.basename(resolvedRoot),
714
- size: stats.size
715
- }
716
- ];
717
- }
718
- const files = [];
719
- async function visit(directory) {
720
- const entries = await readdir(directory, { withFileTypes: true });
721
- for (const entry of entries) {
722
- const absolutePath = path3.join(directory, entry.name);
723
- if (entry.isDirectory()) {
724
- await visit(absolutePath);
725
- } else if (entry.isFile()) {
726
- const entryStats = await stat2(absolutePath);
727
- files.push({
728
- absolutePath,
729
- relativePath: path3.relative(resolvedRoot, absolutePath),
730
- size: entryStats.size
731
- });
732
- }
673
+ const resolvedRoot = resolveLocalPath(rootPath);
674
+ const stats = await stat(resolvedRoot);
675
+ if (stats.isFile()) {
676
+ return [
677
+ {
678
+ absolutePath: resolvedRoot,
679
+ relativePath: path.basename(resolvedRoot),
680
+ size: stats.size
681
+ }
682
+ ];
683
+ }
684
+ const files = [];
685
+ async function visit(directory) {
686
+ const entries = await readdir(directory, { withFileTypes: true });
687
+ for (const entry of entries) {
688
+ const absolutePath = path.join(directory, entry.name);
689
+ if (entry.isDirectory()) {
690
+ await visit(absolutePath);
691
+ }
692
+ else if (entry.isFile()) {
693
+ const entryStats = await stat(absolutePath);
694
+ files.push({
695
+ absolutePath,
696
+ relativePath: path.relative(resolvedRoot, absolutePath),
697
+ size: entryStats.size
698
+ });
699
+ }
700
+ }
733
701
  }
734
- }
735
- await visit(resolvedRoot);
736
- return files;
702
+ await visit(resolvedRoot);
703
+ return files;
737
704
  }
738
705
  async function walkRemoteFiles(transport, rootPath) {
739
- const rootEntries = await transport.list(rootPath);
740
- const files = [];
741
- async function visit(directory, relativeBase, entries) {
742
- for (const entry of entries) {
743
- const fullPath = remoteJoin(directory, entry.name);
744
- const relativePath = relativeBase ? remoteJoin(relativeBase, entry.name) : entry.name;
745
- if (entry.type === "d") {
746
- await visit(fullPath, relativePath, await transport.list(fullPath));
747
- } else if (entry.type === "-") {
748
- files.push({
749
- remotePath: fullPath,
750
- relativePath,
751
- size: entry.size
752
- });
753
- }
706
+ const rootEntries = await transport.list(rootPath);
707
+ const files = [];
708
+ async function visit(directory, relativeBase, entries) {
709
+ for (const entry of entries) {
710
+ const fullPath = remoteJoin(directory, entry.name);
711
+ const relativePath = relativeBase ? remoteJoin(relativeBase, entry.name) : entry.name;
712
+ if (entry.type === "d") {
713
+ await visit(fullPath, relativePath, await transport.list(fullPath));
714
+ }
715
+ else if (entry.type === "-") {
716
+ files.push({
717
+ remotePath: fullPath,
718
+ relativePath,
719
+ size: entry.size
720
+ });
721
+ }
722
+ }
754
723
  }
755
- }
756
- await visit(rootPath, "", rootEntries);
757
- return files;
724
+ await visit(rootPath, "", rootEntries);
725
+ return files;
758
726
  }
759
727
 
760
- // src/config/validation.ts
761
- import { access as access3 } from "fs/promises";
762
-
763
- // src/config/post-upload-commands.ts
764
728
  function isPostUploadCommands(value) {
765
- return Array.isArray(value) && value.every(
766
- (command) => typeof command === "string" && command.trim().length > 0
767
- );
729
+ return (Array.isArray(value) &&
730
+ value.every((command) => typeof command === "string" && command.trim().length > 0));
768
731
  }
769
732
  function getPostUploadCommands(source) {
770
- const commands = readPostUploadCommands(source);
771
- if (commands === void 0) {
772
- return void 0;
773
- }
774
- if (!isPostUploadCommands(commands)) {
775
- throw new ValidationError(
776
- "postUploadCommands must be an array of non-empty command strings."
777
- );
778
- }
779
- return commands;
733
+ const commands = readPostUploadCommands(source);
734
+ if (commands === undefined) {
735
+ return undefined;
736
+ }
737
+ if (!isPostUploadCommands(commands)) {
738
+ throw new ValidationError("postUploadCommands must be an array of non-empty command strings.");
739
+ }
740
+ return commands;
780
741
  }
781
742
  function readPostUploadCommands(source) {
782
- if (typeof source !== "object" || source === null) {
783
- return void 0;
784
- }
785
- return source.postUploadCommands;
743
+ if (typeof source !== "object" || source === null) {
744
+ return undefined;
745
+ }
746
+ return source
747
+ .postUploadCommands;
786
748
  }
787
749
  function clearPostUploadCommands(source) {
788
- source.postUploadCommands = void 0;
750
+ source.postUploadCommands =
751
+ undefined;
789
752
  }
790
753
 
791
- // src/config/validation.ts
792
754
  function validatePort(port) {
793
- if (port === void 0) {
794
- return;
795
- }
796
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
797
- throw new ValidationError("Port must be an integer between 1 and 65535.", {
798
- context: { port }
799
- });
800
- }
755
+ if (port === undefined) {
756
+ return;
757
+ }
758
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
759
+ throw new ValidationError("Port must be an integer between 1 and 65535.", {
760
+ context: { port }
761
+ });
762
+ }
801
763
  }
802
764
  function validateTimeout(timeout) {
803
- if (timeout === void 0) {
804
- return;
805
- }
806
- if (!Number.isFinite(timeout) || timeout <= 0) {
807
- throw new ValidationError("Timeout must be a positive number.", {
808
- context: { timeout }
809
- });
810
- }
765
+ if (timeout === undefined) {
766
+ return;
767
+ }
768
+ if (!Number.isFinite(timeout) || timeout <= 0) {
769
+ throw new ValidationError("Timeout must be a positive number.", {
770
+ context: { timeout }
771
+ });
772
+ }
811
773
  }
812
774
  async function validateServerOptions(options) {
813
- if (!options.host) {
814
- throw new ValidationError("Host is required.");
815
- }
816
- if (!options.username) {
817
- throw new ValidationError("Username is required.");
818
- }
819
- validatePort(options.port);
820
- validateTimeout(options.timeout);
821
- if (options.privateKeyFile) {
822
- try {
823
- await access3(resolveLocalPath(options.privateKeyFile));
824
- } catch (error) {
825
- throw new ValidationError(`Private key file does not exist: ${options.privateKeyFile}`, {
826
- cause: error,
827
- context: { privateKeyFile: options.privateKeyFile }
828
- });
829
- }
830
- }
831
- const hasAuthentication = Boolean(options.password) || Boolean(options.privateKey) || Boolean(options.privateKeyFile) || Boolean(options.agent) || Boolean(process.env.SSH_AUTH_SOCK);
832
- if (!hasAuthentication) {
833
- throw new ValidationError(
834
- "Authentication is required. Provide a password, private key, private key file, or SSH agent."
835
- );
836
- }
775
+ if (!options.host) {
776
+ throw new ValidationError("Host is required.");
777
+ }
778
+ if (!options.username) {
779
+ throw new ValidationError("Username is required.");
780
+ }
781
+ validatePort(options.port);
782
+ validateTimeout(options.timeout);
783
+ if (options.privateKeyFile) {
784
+ try {
785
+ await access(resolveLocalPath(options.privateKeyFile));
786
+ }
787
+ catch (error) {
788
+ throw new ValidationError(`Private key file does not exist: ${options.privateKeyFile}`, {
789
+ cause: error,
790
+ context: { privateKeyFile: options.privateKeyFile }
791
+ });
792
+ }
793
+ }
794
+ const hasAuthentication = Boolean(options.password) ||
795
+ Boolean(options.privateKey) ||
796
+ Boolean(options.privateKeyFile) ||
797
+ Boolean(options.agent) ||
798
+ Boolean(process.env.SSH_AUTH_SOCK);
799
+ if (!hasAuthentication) {
800
+ throw new ValidationError("Authentication is required. Provide a password, private key, private key file, or SSH agent.");
801
+ }
837
802
  }
838
803
  async function validateUploadOptions(options) {
839
- if (!options.localPath) {
840
- throw new ValidationError("Missing localPath.");
841
- }
842
- if (!options.remotePath) {
843
- throw new ValidationError("Missing remotePath.");
844
- }
845
- assertRemotePath(options.remotePath, "remotePath");
846
- validateTimeout(options.timeout);
847
- getPostUploadCommands(options);
848
- await assertLocalPathExists(options.localPath);
849
- if (!options.dryRun) {
850
- await validateServerOptions(options);
851
- }
804
+ if (!options.localPath) {
805
+ throw new ValidationError("Missing localPath.");
806
+ }
807
+ if (!options.remotePath) {
808
+ throw new ValidationError("Missing remotePath.");
809
+ }
810
+ assertRemotePath(options.remotePath, "remotePath");
811
+ validateTimeout(options.timeout);
812
+ getPostUploadCommands(options);
813
+ await assertLocalPathExists(options.localPath);
814
+ if (!options.dryRun) {
815
+ await validateServerOptions(options);
816
+ }
852
817
  }
853
818
  async function validateDownloadOptions(options) {
854
- if (!options.remotePath) {
855
- throw new ValidationError("Missing remotePath.");
856
- }
857
- if (!options.localPath) {
858
- throw new ValidationError("Missing localPath.");
859
- }
860
- assertRemotePath(options.remotePath, "remotePath");
861
- validateTimeout(options.timeout);
862
- if (getPostUploadCommands(options)?.length) {
863
- throw new ValidationError("Post-upload commands are not supported for downloads.");
864
- }
865
- await assertDownloadDestination(options.localPath, options.createDirectories ?? true);
866
- if (!options.dryRun) {
867
- await validateServerOptions(options);
868
- }
819
+ if (!options.remotePath) {
820
+ throw new ValidationError("Missing remotePath.");
821
+ }
822
+ if (!options.localPath) {
823
+ throw new ValidationError("Missing localPath.");
824
+ }
825
+ assertRemotePath(options.remotePath, "remotePath");
826
+ validateTimeout(options.timeout);
827
+ if (getPostUploadCommands(options)?.length) {
828
+ throw new ValidationError("Post-upload commands are not supported for downloads.");
829
+ }
830
+ await assertDownloadDestination(options.localPath, options.createDirectories ?? true);
831
+ if (!options.dryRun) {
832
+ await validateServerOptions(options);
833
+ }
869
834
  }
870
835
 
871
- // src/client/client.ts
872
836
  function percentage(transferredBytes, totalBytes) {
873
- if (!totalBytes || totalBytes <= 0) {
874
- return void 0;
875
- }
876
- return Math.min(100, Math.round(transferredBytes / totalBytes * 100));
837
+ if (!totalBytes || totalBytes <= 0) {
838
+ return undefined;
839
+ }
840
+ return Math.min(100, Math.round((transferredBytes / totalBytes) * 100));
877
841
  }
878
842
  function mergeOptions(serverOptions, transferOptions) {
879
- return { ...serverOptions, ...transferOptions ?? {} };
843
+ return { ...serverOptions, ...(transferOptions ?? {}) };
880
844
  }
881
845
  function shouldCreateDirectories(options) {
882
- return options.createDirectories ?? true;
846
+ return options.createDirectories ?? true;
883
847
  }
884
848
  function hasTrailingPathSeparator(inputPath) {
885
- return /[\\/]$/.test(inputPath);
849
+ return /[\\/]$/.test(inputPath);
886
850
  }
887
851
  function trimmedRemoteDirectory(remotePath) {
888
- return remotePath.replace(/\/+$/g, "") || "/";
852
+ return remotePath.replace(/\/+$/g, "") || "/";
889
853
  }
890
854
  function remoteBasename(remotePath) {
891
- return path4.posix.basename(trimmedRemoteDirectory(remotePath));
855
+ return path.posix.basename(trimmedRemoteDirectory(remotePath));
892
856
  }
893
857
  async function isExistingLocalDirectory(localPath) {
894
- try {
895
- return (await stat3(localPath)).isDirectory();
896
- } catch {
897
- return false;
898
- }
858
+ try {
859
+ return (await stat(localPath)).isDirectory();
860
+ }
861
+ catch {
862
+ return false;
863
+ }
899
864
  }
900
865
  async function ensureLocalDirectory(localPath, createParents = false) {
901
- try {
902
- const stats = await stat3(localPath);
903
- if (stats.isDirectory()) {
904
- return;
866
+ try {
867
+ const stats = await stat(localPath);
868
+ if (stats.isDirectory()) {
869
+ return;
870
+ }
871
+ throw new TransferError(`Local destination already exists and is not a directory: ${localPath}`, {
872
+ context: { localPath }
873
+ });
905
874
  }
906
- throw new TransferError(`Local destination already exists and is not a directory: ${localPath}`, {
907
- context: { localPath }
908
- });
909
- } catch (error) {
910
- if (error instanceof TransferError) {
911
- throw error;
912
- }
913
- await mkdir2(localPath, { recursive: createParents });
914
- }
915
- }
916
- var ScpNextClientImpl = class {
917
- serverOptions;
918
- transport;
919
- commandExecutor;
920
- connected = false;
921
- commandExecutorConnected = false;
922
- constructor(serverOptions, dependencies = {}) {
923
- this.serverOptions = serverOptions;
924
- this.transport = dependencies.transport ?? new Ssh2SftpTransport();
925
- this.commandExecutor = dependencies.commandExecutor ?? new SshCommandExecutor();
926
- }
927
- async connect() {
928
- await validateServerOptions(this.serverOptions);
929
- await this.transport.connect(this.serverOptions);
930
- this.connected = true;
931
- }
932
- async upload(localPath, remotePath, options = {}) {
933
- const uploadOptions = {
934
- ...mergeOptions(this.serverOptions, options),
935
- localPath,
936
- remotePath
937
- };
938
- await validateUploadOptions(uploadOptions);
939
- if (options.dryRun) {
940
- return [];
941
- }
942
- await this.ensureConnected();
943
- await this.uploadWithTransport(uploadOptions);
944
- const results = [];
945
- for (const command of getPostUploadCommands(options) ?? []) {
946
- results.push(await this.exec(command));
947
- }
948
- return results;
949
- }
950
- async download(remotePath, localPath, options = {}) {
951
- const downloadOptions = {
952
- ...mergeOptions(this.serverOptions, options),
953
- remotePath,
954
- localPath
955
- };
956
- await validateDownloadOptions(downloadOptions);
957
- if (options.dryRun) {
958
- return;
875
+ catch (error) {
876
+ if (error instanceof TransferError) {
877
+ throw error;
878
+ }
879
+ await mkdir(localPath, { recursive: createParents });
880
+ }
881
+ }
882
+ class ScpNextClientImpl {
883
+ serverOptions;
884
+ transport;
885
+ commandExecutor;
886
+ connected = false;
887
+ commandExecutorConnected = false;
888
+ constructor(serverOptions, dependencies = {}) {
889
+ this.serverOptions = serverOptions;
890
+ this.transport = dependencies.transport ?? new Ssh2SftpTransport();
891
+ this.commandExecutor = dependencies.commandExecutor ?? new SshCommandExecutor();
892
+ }
893
+ async connect() {
894
+ await validateServerOptions(this.serverOptions);
895
+ await this.transport.connect(this.serverOptions);
896
+ this.connected = true;
959
897
  }
960
- await this.ensureConnected();
961
- await this.downloadWithTransport(downloadOptions);
962
- }
963
- async exec(command, options = {}) {
964
- if (typeof command !== "string" || !command.trim()) {
965
- throw new ValidationError("Remote command must not be empty.");
898
+ async upload(localPath, remotePath, options = {}) {
899
+ const uploadOptions = {
900
+ ...mergeOptions(this.serverOptions, options),
901
+ localPath,
902
+ remotePath
903
+ };
904
+ await validateUploadOptions(uploadOptions);
905
+ if (options.dryRun) {
906
+ return [];
907
+ }
908
+ await this.ensureConnected();
909
+ await this.uploadWithTransport(uploadOptions);
910
+ const results = [];
911
+ for (const command of getPostUploadCommands(options) ?? []) {
912
+ results.push(await this.exec(command));
913
+ }
914
+ return results;
915
+ }
916
+ async download(remotePath, localPath, options = {}) {
917
+ const downloadOptions = {
918
+ ...mergeOptions(this.serverOptions, options),
919
+ remotePath,
920
+ localPath
921
+ };
922
+ await validateDownloadOptions(downloadOptions);
923
+ if (options.dryRun) {
924
+ return;
925
+ }
926
+ await this.ensureConnected();
927
+ await this.downloadWithTransport(downloadOptions);
966
928
  }
967
- if (options.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout <= 0)) {
968
- throw new ValidationError("Command timeout must be a positive number.");
929
+ async exec(command, options = {}) {
930
+ if (typeof command !== "string" || !command.trim()) {
931
+ throw new ValidationError("Remote command must not be empty.");
932
+ }
933
+ if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout <= 0)) {
934
+ throw new ValidationError("Command timeout must be a positive number.");
935
+ }
936
+ if (options.maxBuffer !== undefined &&
937
+ (!Number.isInteger(options.maxBuffer) || options.maxBuffer <= 0)) {
938
+ throw new ValidationError("Command maxBuffer must be a positive integer.");
939
+ }
940
+ if (options.failOnStderr !== undefined &&
941
+ typeof options.failOnStderr !== "boolean") {
942
+ throw new ValidationError("Command failOnStderr must be a boolean.");
943
+ }
944
+ await this.ensureCommandExecutorConnected();
945
+ let result;
946
+ try {
947
+ result = await this.commandExecutor.exec(command, options);
948
+ }
949
+ catch (error) {
950
+ if (error instanceof RemoteCommandError) {
951
+ throw new RemoteCommandError(error.message, {
952
+ exitCode: error.exitCode,
953
+ signal: error.signal,
954
+ stdout: redactKnownSensitiveValues(error.stdout, this.serverOptions),
955
+ stderr: redactKnownSensitiveValues(error.stderr, this.serverOptions)
956
+ });
957
+ }
958
+ throw error;
959
+ }
960
+ if (result.exitCode !== 0 || (options.failOnStderr && result.stderr.length > 0)) {
961
+ const reason = result.exitCode === null
962
+ ? "Remote command ended without an exit code."
963
+ : result.exitCode !== 0
964
+ ? `Remote command failed with exit code ${String(result.exitCode)}.`
965
+ : "Remote command wrote to stderr.";
966
+ throw new RemoteCommandError(reason, {
967
+ exitCode: result.exitCode,
968
+ signal: result.signal,
969
+ stdout: redactKnownSensitiveValues(result.stdout, this.serverOptions),
970
+ stderr: redactKnownSensitiveValues(result.stderr, this.serverOptions)
971
+ });
972
+ }
973
+ return result;
974
+ }
975
+ async close() {
976
+ const closures = [];
977
+ if (this.connected)
978
+ closures.push(this.transport.close());
979
+ if (this.commandExecutorConnected)
980
+ closures.push(this.commandExecutor.close());
981
+ const results = await Promise.allSettled(closures);
982
+ this.connected = false;
983
+ this.commandExecutorConnected = false;
984
+ const failure = results.find((result) => result.status === "rejected");
985
+ if (failure) {
986
+ throw failure.reason;
987
+ }
969
988
  }
970
- if (options.maxBuffer !== void 0 && (!Number.isInteger(options.maxBuffer) || options.maxBuffer <= 0)) {
971
- throw new ValidationError("Command maxBuffer must be a positive integer.");
989
+ async ensureConnected() {
990
+ if (!this.connected) {
991
+ await this.connect();
992
+ }
972
993
  }
973
- if (options.failOnStderr !== void 0 && typeof options.failOnStderr !== "boolean") {
974
- throw new ValidationError("Command failOnStderr must be a boolean.");
994
+ async ensureCommandExecutorConnected() {
995
+ if (!this.commandExecutorConnected) {
996
+ await validateServerOptions(this.serverOptions);
997
+ await this.commandExecutor.connect(this.serverOptions);
998
+ this.commandExecutorConnected = true;
999
+ }
975
1000
  }
976
- await this.ensureCommandExecutorConnected();
977
- let result;
978
- try {
979
- result = await this.commandExecutor.exec(command, options);
980
- } catch (error) {
981
- if (error instanceof RemoteCommandError) {
982
- throw new RemoteCommandError(error.message, {
983
- exitCode: error.exitCode,
984
- signal: error.signal,
985
- stdout: redactKnownSensitiveValues(error.stdout, this.serverOptions),
986
- stderr: redactKnownSensitiveValues(error.stderr, this.serverOptions)
987
- });
988
- }
989
- throw error;
990
- }
991
- if (result.exitCode !== 0 || options.failOnStderr && result.stderr.length > 0) {
992
- 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.";
993
- throw new RemoteCommandError(reason, {
994
- exitCode: result.exitCode,
995
- signal: result.signal,
996
- stdout: redactKnownSensitiveValues(result.stdout, this.serverOptions),
997
- stderr: redactKnownSensitiveValues(result.stderr, this.serverOptions)
998
- });
1001
+ async uploadWithTransport(options) {
1002
+ const localKind = await getLocalPathKind(options.localPath);
1003
+ const remotePath = normalizeRemotePath(options.remotePath);
1004
+ if (localKind === "directory" && !options.recursive) {
1005
+ throw new ValidationError("Uploading a directory requires recursive mode.");
1006
+ }
1007
+ if (localKind === "file") {
1008
+ const destinationFile = await this.resolveRemoteDestinationForLocalFile(options, remotePath);
1009
+ const totalBytes = undefined;
1010
+ await this.transport.uploadFile(resolveLocalPath(options.localPath), destinationFile, (step) => {
1011
+ options.onProgress?.({
1012
+ operation: "upload",
1013
+ source: options.localPath,
1014
+ destination: options.remotePath,
1015
+ transferredBytes: step.transferredBytes,
1016
+ totalBytes: step.totalBytes || totalBytes,
1017
+ percentage: percentage(step.transferredBytes, step.totalBytes),
1018
+ currentFile: options.localPath
1019
+ });
1020
+ });
1021
+ return;
1022
+ }
1023
+ const files = await walkLocalFiles(options.localPath);
1024
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1025
+ let completedBytes = 0;
1026
+ let completedFiles = 0;
1027
+ const destinationRoot = await this.resolveRemoteDestinationForLocalDirectory(options, remotePath);
1028
+ for (const file of files) {
1029
+ const destinationFile = remoteJoin(destinationRoot, file.relativePath.split(path.sep).join("/"));
1030
+ await this.ensureRemoteParent(destinationFile, true);
1031
+ await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1032
+ await this.transport.uploadFile(file.absolutePath, destinationFile, (step) => {
1033
+ const transferredBytes = completedBytes + step.transferredBytes;
1034
+ options.onProgress?.({
1035
+ operation: "upload",
1036
+ source: options.localPath,
1037
+ destination: options.remotePath,
1038
+ transferredBytes,
1039
+ totalBytes,
1040
+ percentage: percentage(transferredBytes, totalBytes),
1041
+ currentFile: file.relativePath,
1042
+ completedFiles,
1043
+ totalFiles: files.length
1044
+ });
1045
+ });
1046
+ completedBytes += file.size;
1047
+ completedFiles += 1;
1048
+ }
999
1049
  }
1000
- return result;
1001
- }
1002
- async close() {
1003
- const closures = [];
1004
- if (this.connected) closures.push(this.transport.close());
1005
- if (this.commandExecutorConnected) closures.push(this.commandExecutor.close());
1006
- const results = await Promise.allSettled(closures);
1007
- this.connected = false;
1008
- this.commandExecutorConnected = false;
1009
- const failure = results.find(
1010
- (result) => result.status === "rejected"
1011
- );
1012
- if (failure) {
1013
- throw failure.reason;
1014
- }
1015
- }
1016
- async ensureConnected() {
1017
- if (!this.connected) {
1018
- await this.connect();
1019
- }
1020
- }
1021
- async ensureCommandExecutorConnected() {
1022
- if (!this.commandExecutorConnected) {
1023
- await validateServerOptions(this.serverOptions);
1024
- await this.commandExecutor.connect(this.serverOptions);
1025
- this.commandExecutorConnected = true;
1026
- }
1027
- }
1028
- async uploadWithTransport(options) {
1029
- const localKind = await getLocalPathKind(options.localPath);
1030
- const remotePath = normalizeRemotePath(options.remotePath);
1031
- if (localKind === "directory" && !options.recursive) {
1032
- throw new ValidationError("Uploading a directory requires recursive mode.");
1033
- }
1034
- if (localKind === "file") {
1035
- const destinationFile = await this.resolveRemoteDestinationForLocalFile(options, remotePath);
1036
- const totalBytes2 = void 0;
1037
- await this.transport.uploadFile(resolveLocalPath(options.localPath), destinationFile, (step) => {
1038
- options.onProgress?.({
1039
- operation: "upload",
1040
- source: options.localPath,
1041
- destination: options.remotePath,
1042
- transferredBytes: step.transferredBytes,
1043
- totalBytes: step.totalBytes || totalBytes2,
1044
- percentage: percentage(step.transferredBytes, step.totalBytes),
1045
- currentFile: options.localPath
1046
- });
1047
- });
1048
- return;
1049
- }
1050
- const files = await walkLocalFiles(options.localPath);
1051
- const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1052
- let completedBytes = 0;
1053
- let completedFiles = 0;
1054
- const destinationRoot = await this.resolveRemoteDestinationForLocalDirectory(
1055
- options,
1056
- remotePath
1057
- );
1058
- for (const file of files) {
1059
- const destinationFile = remoteJoin(
1060
- destinationRoot,
1061
- file.relativePath.split(path4.sep).join("/")
1062
- );
1063
- await this.ensureRemoteParent(destinationFile, true);
1064
- await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1065
- await this.transport.uploadFile(file.absolutePath, destinationFile, (step) => {
1066
- const transferredBytes = completedBytes + step.transferredBytes;
1067
- options.onProgress?.({
1068
- operation: "upload",
1069
- source: options.localPath,
1070
- destination: options.remotePath,
1071
- transferredBytes,
1072
- totalBytes,
1073
- percentage: percentage(transferredBytes, totalBytes),
1074
- currentFile: file.relativePath,
1075
- completedFiles,
1076
- totalFiles: files.length
1077
- });
1078
- });
1079
- completedBytes += file.size;
1080
- completedFiles += 1;
1081
- }
1082
- }
1083
- async resolveRemoteDestinationForLocalFile(options, remotePath) {
1084
- const remoteKind = await this.transport.exists(remotePath);
1085
- const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1086
- if (!isDirectoryLikeDestination) {
1087
- await this.ensureRemoteParent(remotePath, shouldCreateDirectories(options));
1088
- await this.assertRemoteOverwrite(remotePath, options.overwrite);
1089
- return remotePath;
1090
- }
1091
- if (remoteKind && remoteKind !== "d") {
1092
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1093
- context: { remotePath }
1094
- });
1095
- }
1096
- const remoteDirectory = remotePath.replace(/\/+$/g, "") || "/";
1097
- if (!remoteKind) {
1098
- if (!shouldCreateDirectories(options)) {
1099
- throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1100
- context: { remotePath }
1101
- });
1102
- }
1103
- await this.transport.mkdir(remoteDirectory, true);
1104
- }
1105
- const destinationFile = remoteJoin(
1106
- remoteDirectory,
1107
- path4.basename(resolveLocalPath(options.localPath))
1108
- );
1109
- await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1110
- return destinationFile;
1111
- }
1112
- async resolveRemoteDestinationForLocalDirectory(options, remotePath) {
1113
- const remoteKind = await this.transport.exists(remotePath);
1114
- const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1115
- const localDirectoryName = path4.basename(resolveLocalPath(options.localPath));
1116
- if (remoteKind && remoteKind !== "d") {
1117
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1118
- context: { remotePath }
1119
- });
1120
- }
1121
- if (isDirectoryLikeDestination) {
1122
- const remoteDirectory = trimmedRemoteDirectory(remotePath);
1123
- if (!remoteKind) {
1124
- if (!shouldCreateDirectories(options)) {
1125
- throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1126
- context: { remotePath }
1127
- });
1128
- }
1129
- await this.transport.mkdir(remoteDirectory, true);
1130
- }
1131
- const destinationRoot = remoteJoin(remoteDirectory, localDirectoryName);
1132
- await this.ensureRemoteDirectory(destinationRoot);
1133
- return destinationRoot;
1134
- }
1135
- if (!remoteKind) {
1136
- if (!shouldCreateDirectories(options)) {
1137
- throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
1138
- context: { remotePath }
1139
- });
1140
- }
1141
- await this.transport.mkdir(remotePath, true);
1142
- }
1143
- return remotePath;
1144
- }
1145
- async downloadWithTransport(options) {
1146
- const remotePath = normalizeRemotePath(options.remotePath);
1147
- const remoteKind = await this.transport.exists(remotePath);
1148
- if (!remoteKind) {
1149
- throw new TransferError(`Remote path does not exist: ${options.remotePath}`, {
1150
- context: { remotePath: options.remotePath }
1151
- });
1152
- }
1153
- if (remoteKind === "d") {
1154
- if (!options.recursive) {
1155
- throw new ValidationError("Downloading a directory requires recursive mode.");
1156
- }
1157
- await this.downloadDirectory(options);
1158
- return;
1159
- }
1160
- await this.downloadSingleFile(options, remotePath);
1161
- }
1162
- async downloadSingleFile(options, remotePath) {
1163
- const localPath = await this.resolveLocalDestinationForRemoteFile(options, remotePath);
1164
- await this.assertLocalOverwrite(localPath, options.overwrite);
1165
- if (shouldCreateDirectories(options)) {
1166
- await mkdir2(path4.dirname(localPath), { recursive: true });
1167
- }
1168
- await this.transport.downloadFile(remotePath, localPath, (step) => {
1169
- options.onProgress?.({
1170
- operation: "download",
1171
- source: options.remotePath,
1172
- destination: options.localPath,
1173
- transferredBytes: step.transferredBytes,
1174
- totalBytes: step.totalBytes,
1175
- percentage: percentage(step.transferredBytes, step.totalBytes),
1176
- currentFile: options.remotePath
1177
- });
1178
- });
1179
- }
1180
- async downloadDirectory(options) {
1181
- const remotePath = normalizeRemotePath(options.remotePath);
1182
- const localRoot = await this.resolveLocalDestinationForRemoteDirectory(options, remotePath);
1183
- const files = await walkRemoteFiles(this.transport, remotePath);
1184
- const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1185
- let completedBytes = 0;
1186
- let completedFiles = 0;
1187
- await ensureLocalDirectory(localRoot, shouldCreateDirectories(options));
1188
- for (const file of files) {
1189
- const localFile = path4.join(localRoot, ...file.relativePath.split("/"));
1190
- await this.assertLocalOverwrite(localFile, options.overwrite);
1191
- await mkdir2(path4.dirname(localFile), { recursive: true });
1192
- await this.transport.downloadFile(file.remotePath, localFile, (step) => {
1193
- const transferredBytes = completedBytes + step.transferredBytes;
1194
- options.onProgress?.({
1195
- operation: "download",
1196
- source: options.remotePath,
1197
- destination: options.localPath,
1198
- transferredBytes,
1199
- totalBytes,
1200
- percentage: percentage(transferredBytes, totalBytes),
1201
- currentFile: file.relativePath,
1202
- completedFiles,
1203
- totalFiles: files.length
1204
- });
1205
- });
1206
- completedBytes += file.size;
1207
- completedFiles += 1;
1208
- }
1209
- }
1210
- async resolveLocalDestinationForRemoteFile(options, remotePath) {
1211
- const localDestination = resolveLocalPath(options.localPath);
1212
- const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
1213
- if (!isDirectoryLikeDestination) {
1214
- return localDestination;
1215
- }
1216
- if (!await isExistingLocalDirectory(localDestination)) {
1217
- if (!shouldCreateDirectories(options)) {
1218
- throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1219
- context: { localPath: options.localPath }
1220
- });
1221
- }
1222
- await mkdir2(localDestination, { recursive: true });
1223
- }
1224
- return path4.join(localDestination, remoteBasename(remotePath));
1225
- }
1226
- async resolveLocalDestinationForRemoteDirectory(options, remotePath) {
1227
- const localDestination = resolveLocalPath(options.localPath);
1228
- const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || await isExistingLocalDirectory(localDestination);
1229
- if (!isDirectoryLikeDestination) {
1230
- return localDestination;
1231
- }
1232
- if (!await isExistingLocalDirectory(localDestination)) {
1233
- if (!shouldCreateDirectories(options)) {
1234
- throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1235
- context: { localPath: options.localPath }
1050
+ async resolveRemoteDestinationForLocalFile(options, remotePath) {
1051
+ const remoteKind = await this.transport.exists(remotePath);
1052
+ const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1053
+ if (!isDirectoryLikeDestination) {
1054
+ await this.ensureRemoteParent(remotePath, shouldCreateDirectories(options));
1055
+ await this.assertRemoteOverwrite(remotePath, options.overwrite);
1056
+ return remotePath;
1057
+ }
1058
+ if (remoteKind && remoteKind !== "d") {
1059
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1060
+ context: { remotePath }
1061
+ });
1062
+ }
1063
+ const remoteDirectory = remotePath.replace(/\/+$/g, "") || "/";
1064
+ if (!remoteKind) {
1065
+ if (!shouldCreateDirectories(options)) {
1066
+ throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1067
+ context: { remotePath }
1068
+ });
1069
+ }
1070
+ await this.transport.mkdir(remoteDirectory, true);
1071
+ }
1072
+ const destinationFile = remoteJoin(remoteDirectory, path.basename(resolveLocalPath(options.localPath)));
1073
+ await this.assertRemoteOverwrite(destinationFile, options.overwrite);
1074
+ return destinationFile;
1075
+ }
1076
+ async resolveRemoteDestinationForLocalDirectory(options, remotePath) {
1077
+ const remoteKind = await this.transport.exists(remotePath);
1078
+ const isDirectoryLikeDestination = remoteKind === "d" || remotePath.endsWith("/");
1079
+ const localDirectoryName = path.basename(resolveLocalPath(options.localPath));
1080
+ if (remoteKind && remoteKind !== "d") {
1081
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1082
+ context: { remotePath }
1083
+ });
1084
+ }
1085
+ if (isDirectoryLikeDestination) {
1086
+ const remoteDirectory = trimmedRemoteDirectory(remotePath);
1087
+ if (!remoteKind) {
1088
+ if (!shouldCreateDirectories(options)) {
1089
+ throw new TransferError(`Remote destination directory does not exist: ${remoteDirectory}`, {
1090
+ context: { remotePath }
1091
+ });
1092
+ }
1093
+ await this.transport.mkdir(remoteDirectory, true);
1094
+ }
1095
+ const destinationRoot = remoteJoin(remoteDirectory, localDirectoryName);
1096
+ await this.ensureRemoteDirectory(destinationRoot);
1097
+ return destinationRoot;
1098
+ }
1099
+ if (!remoteKind) {
1100
+ if (!shouldCreateDirectories(options)) {
1101
+ throw new TransferError(`Remote destination directory does not exist: ${remotePath}`, {
1102
+ context: { remotePath }
1103
+ });
1104
+ }
1105
+ await this.transport.mkdir(remotePath, true);
1106
+ }
1107
+ return remotePath;
1108
+ }
1109
+ async downloadWithTransport(options) {
1110
+ const remotePath = normalizeRemotePath(options.remotePath);
1111
+ const remoteKind = await this.transport.exists(remotePath);
1112
+ if (!remoteKind) {
1113
+ throw new TransferError(`Remote path does not exist: ${options.remotePath}`, {
1114
+ context: { remotePath: options.remotePath }
1115
+ });
1116
+ }
1117
+ if (remoteKind === "d") {
1118
+ if (!options.recursive) {
1119
+ throw new ValidationError("Downloading a directory requires recursive mode.");
1120
+ }
1121
+ await this.downloadDirectory(options);
1122
+ return;
1123
+ }
1124
+ await this.downloadSingleFile(options, remotePath);
1125
+ }
1126
+ async downloadSingleFile(options, remotePath) {
1127
+ const localPath = await this.resolveLocalDestinationForRemoteFile(options, remotePath);
1128
+ await this.assertLocalOverwrite(localPath, options.overwrite);
1129
+ if (shouldCreateDirectories(options)) {
1130
+ await mkdir(path.dirname(localPath), { recursive: true });
1131
+ }
1132
+ await this.transport.downloadFile(remotePath, localPath, (step) => {
1133
+ options.onProgress?.({
1134
+ operation: "download",
1135
+ source: options.remotePath,
1136
+ destination: options.localPath,
1137
+ transferredBytes: step.transferredBytes,
1138
+ totalBytes: step.totalBytes,
1139
+ percentage: percentage(step.transferredBytes, step.totalBytes),
1140
+ currentFile: options.remotePath
1141
+ });
1236
1142
  });
1237
- }
1238
- await mkdir2(localDestination, { recursive: true });
1239
1143
  }
1240
- return path4.join(localDestination, remoteBasename(remotePath));
1241
- }
1242
- async ensureRemoteParent(remotePath, createDirectories = false) {
1243
- const parent = remoteDirname(remotePath);
1244
- if (parent === "." || parent === "/") {
1245
- return;
1144
+ async downloadDirectory(options) {
1145
+ const remotePath = normalizeRemotePath(options.remotePath);
1146
+ const localRoot = await this.resolveLocalDestinationForRemoteDirectory(options, remotePath);
1147
+ const files = await walkRemoteFiles(this.transport, remotePath);
1148
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
1149
+ let completedBytes = 0;
1150
+ let completedFiles = 0;
1151
+ await ensureLocalDirectory(localRoot, shouldCreateDirectories(options));
1152
+ for (const file of files) {
1153
+ const localFile = path.join(localRoot, ...file.relativePath.split("/"));
1154
+ await this.assertLocalOverwrite(localFile, options.overwrite);
1155
+ await mkdir(path.dirname(localFile), { recursive: true });
1156
+ await this.transport.downloadFile(file.remotePath, localFile, (step) => {
1157
+ const transferredBytes = completedBytes + step.transferredBytes;
1158
+ options.onProgress?.({
1159
+ operation: "download",
1160
+ source: options.remotePath,
1161
+ destination: options.localPath,
1162
+ transferredBytes,
1163
+ totalBytes,
1164
+ percentage: percentage(transferredBytes, totalBytes),
1165
+ currentFile: file.relativePath,
1166
+ completedFiles,
1167
+ totalFiles: files.length
1168
+ });
1169
+ });
1170
+ completedBytes += file.size;
1171
+ completedFiles += 1;
1172
+ }
1246
1173
  }
1247
- if (createDirectories) {
1248
- await this.transport.mkdir(parent, true);
1249
- return;
1250
- }
1251
- const exists = await this.transport.exists(parent);
1252
- if (exists !== "d") {
1253
- throw new TransferError(`Remote destination parent does not exist: ${parent}`, {
1254
- context: { remotePath }
1255
- });
1256
- }
1257
- }
1258
- async ensureRemoteDirectory(remotePath) {
1259
- const exists = await this.transport.exists(remotePath);
1260
- if (exists && exists !== "d") {
1261
- throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1262
- context: { remotePath }
1263
- });
1264
- }
1265
- if (!exists) {
1266
- await this.transport.mkdir(remotePath, true);
1267
- }
1268
- }
1269
- async assertRemoteOverwrite(remotePath, overwrite = false) {
1270
- if (overwrite) {
1271
- return;
1272
- }
1273
- const exists = await this.transport.exists(remotePath);
1274
- if (exists) {
1275
- throw new TransferError(
1276
- `Remote destination already exists: ${remotePath}. Use --overwrite to overwrite it.`,
1277
- {
1278
- context: { remotePath }
1279
- }
1280
- );
1281
- }
1282
- }
1283
- async assertLocalOverwrite(localPath, overwrite = false) {
1284
- if (overwrite) {
1285
- return;
1174
+ async resolveLocalDestinationForRemoteFile(options, remotePath) {
1175
+ const localDestination = resolveLocalPath(options.localPath);
1176
+ const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || (await isExistingLocalDirectory(localDestination));
1177
+ if (!isDirectoryLikeDestination) {
1178
+ return localDestination;
1179
+ }
1180
+ if (!(await isExistingLocalDirectory(localDestination))) {
1181
+ if (!shouldCreateDirectories(options)) {
1182
+ throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1183
+ context: { localPath: options.localPath }
1184
+ });
1185
+ }
1186
+ await mkdir(localDestination, { recursive: true });
1187
+ }
1188
+ return path.join(localDestination, remoteBasename(remotePath));
1286
1189
  }
1287
- try {
1288
- await access4(localPath);
1289
- throw new TransferError(`Local destination already exists: ${localPath}`, {
1290
- context: { localPath }
1291
- });
1292
- } catch (error) {
1293
- if (error instanceof TransferError) {
1294
- throw error;
1295
- }
1190
+ async resolveLocalDestinationForRemoteDirectory(options, remotePath) {
1191
+ const localDestination = resolveLocalPath(options.localPath);
1192
+ const isDirectoryLikeDestination = hasTrailingPathSeparator(options.localPath) || (await isExistingLocalDirectory(localDestination));
1193
+ if (!isDirectoryLikeDestination) {
1194
+ return localDestination;
1195
+ }
1196
+ if (!(await isExistingLocalDirectory(localDestination))) {
1197
+ if (!shouldCreateDirectories(options)) {
1198
+ throw new TransferError(`Local destination directory does not exist: ${localDestination}`, {
1199
+ context: { localPath: options.localPath }
1200
+ });
1201
+ }
1202
+ await mkdir(localDestination, { recursive: true });
1203
+ }
1204
+ return path.join(localDestination, remoteBasename(remotePath));
1296
1205
  }
1297
- }
1298
- };
1206
+ async ensureRemoteParent(remotePath, createDirectories = false) {
1207
+ const parent = remoteDirname(remotePath);
1208
+ if (parent === "." || parent === "/") {
1209
+ return;
1210
+ }
1211
+ if (createDirectories) {
1212
+ await this.transport.mkdir(parent, true);
1213
+ return;
1214
+ }
1215
+ const exists = await this.transport.exists(parent);
1216
+ if (exists !== "d") {
1217
+ throw new TransferError(`Remote destination parent does not exist: ${parent}`, {
1218
+ context: { remotePath }
1219
+ });
1220
+ }
1221
+ }
1222
+ async ensureRemoteDirectory(remotePath) {
1223
+ const exists = await this.transport.exists(remotePath);
1224
+ if (exists && exists !== "d") {
1225
+ throw new TransferError(`Remote destination already exists and is not a directory: ${remotePath}`, {
1226
+ context: { remotePath }
1227
+ });
1228
+ }
1229
+ if (!exists) {
1230
+ await this.transport.mkdir(remotePath, true);
1231
+ }
1232
+ }
1233
+ async assertRemoteOverwrite(remotePath, overwrite = false) {
1234
+ if (overwrite) {
1235
+ return;
1236
+ }
1237
+ const exists = await this.transport.exists(remotePath);
1238
+ if (exists) {
1239
+ throw new TransferError(`Remote destination already exists: ${remotePath}. Use --overwrite to overwrite it.`, {
1240
+ context: { remotePath }
1241
+ });
1242
+ }
1243
+ }
1244
+ async assertLocalOverwrite(localPath, overwrite = false) {
1245
+ if (overwrite) {
1246
+ return;
1247
+ }
1248
+ try {
1249
+ await access(localPath);
1250
+ throw new TransferError(`Local destination already exists: ${localPath}`, {
1251
+ context: { localPath }
1252
+ });
1253
+ }
1254
+ catch (error) {
1255
+ if (error instanceof TransferError) {
1256
+ throw error;
1257
+ }
1258
+ }
1259
+ }
1260
+ }
1299
1261
 
1300
- // src/client/download.ts
1301
1262
  async function download(options) {
1302
- const client = new ScpNextClientImpl(options);
1303
- try {
1304
- await client.download(options.remotePath, options.localPath, options);
1305
- } finally {
1306
- await client.close();
1307
- }
1263
+ const client = new ScpNextClientImpl(options);
1264
+ try {
1265
+ await client.download(options.remotePath, options.localPath, options);
1266
+ }
1267
+ finally {
1268
+ await client.close();
1269
+ }
1308
1270
  }
1309
1271
 
1310
- // src/client/upload.ts
1311
1272
  async function upload(options) {
1312
- const client = new ScpNextClientImpl(options);
1313
- let result;
1314
- try {
1315
- result = await client.upload(options.localPath, options.remotePath, options);
1316
- } catch (error) {
1273
+ const client = new ScpNextClientImpl(options);
1274
+ let result;
1317
1275
  try {
1318
- await client.close();
1319
- } catch {
1276
+ result = await client.upload(options.localPath, options.remotePath, options);
1320
1277
  }
1321
- throw error;
1322
- }
1323
- await client.close();
1324
- return result;
1278
+ catch (error) {
1279
+ try {
1280
+ await client.close();
1281
+ }
1282
+ catch {
1283
+ // Preserve the upload or command failure as the primary error.
1284
+ }
1285
+ throw error;
1286
+ }
1287
+ await client.close();
1288
+ return result;
1325
1289
  }
1326
1290
 
1327
- // src/cli/commands/download.ts
1328
- import { Command } from "commander";
1329
-
1330
- // src/cli/operands.ts
1331
1291
  function usageFor(operation) {
1332
- return `scp-next ${operation} <source> <destination> [options]`;
1292
+ return `scp-next ${operation} <source> <destination> [options]`;
1333
1293
  }
1334
1294
  function assertSourceDestination(operation, source, destination) {
1335
- if (!source) {
1336
- throw new ValidationError(`Missing source path.
1337
-
1338
- Usage:
1339
- ${usageFor(operation)}`);
1340
- }
1341
- if (!destination) {
1342
- throw new ValidationError(`Missing destination path.
1343
-
1344
- Usage:
1345
- ${usageFor(operation)}`);
1346
- }
1295
+ if (!source) {
1296
+ throw new ValidationError(`Missing source path.\n\nUsage:\n ${usageFor(operation)}`);
1297
+ }
1298
+ if (!destination) {
1299
+ throw new ValidationError(`Missing destination path.\n\nUsage:\n ${usageFor(operation)}`);
1300
+ }
1347
1301
  }
1348
1302
 
1349
- // src/cli/commands/shared.ts
1350
- import { Option } from "commander";
1351
-
1352
- // src/config/environment.ts
1353
- function parseInteger(value, name) {
1354
- if (!value) {
1355
- return void 0;
1356
- }
1357
- const parsed = Number.parseInt(value, 10);
1358
- if (!Number.isFinite(parsed)) {
1359
- throw new Error(`${name} must be a number.`);
1360
- }
1361
- return parsed;
1303
+ function parseInteger$1(value, name) {
1304
+ if (!value) {
1305
+ return undefined;
1306
+ }
1307
+ const parsed = Number.parseInt(value, 10);
1308
+ if (!Number.isFinite(parsed)) {
1309
+ throw new Error(`${name} must be a number.`);
1310
+ }
1311
+ return parsed;
1362
1312
  }
1363
1313
  function readEnvironment(env = process.env) {
1364
- const result = {};
1365
- if (env.SCP_NEXT_HOST) result.host = env.SCP_NEXT_HOST;
1366
- if (env.SCP_NEXT_PORT) {
1367
- const port = parseInteger(env.SCP_NEXT_PORT, "SCP_NEXT_PORT");
1368
- if (port !== void 0) result.port = port;
1369
- }
1370
- if (env.SCP_NEXT_USERNAME) result.username = env.SCP_NEXT_USERNAME;
1371
- if (env.SCP_NEXT_PASSWORD) result.password = env.SCP_NEXT_PASSWORD;
1372
- if (env.SCP_NEXT_PRIVATE_KEY) result.privateKey = env.SCP_NEXT_PRIVATE_KEY;
1373
- if (env.SCP_NEXT_PRIVATE_KEY_FILE) result.privateKeyFile = env.SCP_NEXT_PRIVATE_KEY_FILE;
1374
- if (env.SCP_NEXT_PASSPHRASE) result.passphrase = env.SCP_NEXT_PASSPHRASE;
1375
- if (env.SCP_NEXT_TIMEOUT) {
1376
- const timeout = parseInteger(env.SCP_NEXT_TIMEOUT, "SCP_NEXT_TIMEOUT");
1377
- if (timeout !== void 0) result.timeout = timeout;
1378
- }
1379
- if (env.SCP_NEXT_PROFILE) result.profile = env.SCP_NEXT_PROFILE;
1380
- return result;
1314
+ const result = {};
1315
+ if (env.SCP_NEXT_HOST)
1316
+ result.host = env.SCP_NEXT_HOST;
1317
+ if (env.SCP_NEXT_PORT) {
1318
+ const port = parseInteger$1(env.SCP_NEXT_PORT, "SCP_NEXT_PORT");
1319
+ if (port !== undefined)
1320
+ result.port = port;
1321
+ }
1322
+ if (env.SCP_NEXT_USERNAME)
1323
+ result.username = env.SCP_NEXT_USERNAME;
1324
+ if (env.SCP_NEXT_PASSWORD)
1325
+ result.password = env.SCP_NEXT_PASSWORD;
1326
+ if (env.SCP_NEXT_PRIVATE_KEY)
1327
+ result.privateKey = env.SCP_NEXT_PRIVATE_KEY;
1328
+ if (env.SCP_NEXT_PRIVATE_KEY_FILE)
1329
+ result.privateKeyFile = env.SCP_NEXT_PRIVATE_KEY_FILE;
1330
+ if (env.SCP_NEXT_PASSPHRASE)
1331
+ result.passphrase = env.SCP_NEXT_PASSPHRASE;
1332
+ if (env.SCP_NEXT_TIMEOUT) {
1333
+ const timeout = parseInteger$1(env.SCP_NEXT_TIMEOUT, "SCP_NEXT_TIMEOUT");
1334
+ if (timeout !== undefined)
1335
+ result.timeout = timeout;
1336
+ }
1337
+ if (env.SCP_NEXT_PROFILE)
1338
+ result.profile = env.SCP_NEXT_PROFILE;
1339
+ return result;
1381
1340
  }
1382
1341
 
1383
- // src/config/load-config.ts
1384
- import { access as access5, readFile as readFile3 } from "fs/promises";
1385
- import path5 from "path";
1386
- import { z } from "zod";
1387
- var CONFIG_FILENAMES = ["scp-next.config.json", ".scp-nextrc", ".scp-nextrc.json"];
1388
- var serverSchema = z.object({
1389
- host: z.string().optional(),
1390
- port: z.number().int().optional(),
1391
- username: z.string().optional(),
1392
- password: z.string().optional(),
1393
- privateKey: z.string().optional(),
1394
- privateKeyFile: z.string().optional(),
1395
- passphrase: z.string().optional(),
1396
- agent: z.string().optional(),
1397
- timeout: z.number().int().optional(),
1398
- hostFingerprint: z.string().optional(),
1399
- knownHostsFile: z.string().optional()
1342
+ const CONFIG_FILENAMES = ["scp-next.config.json", ".scp-nextrc", ".scp-nextrc.json"];
1343
+ const serverSchema = z.object({
1344
+ host: z.string().optional(),
1345
+ port: z.number().int().optional(),
1346
+ username: z.string().optional(),
1347
+ password: z.string().optional(),
1348
+ privateKey: z.string().optional(),
1349
+ privateKeyFile: z.string().optional(),
1350
+ passphrase: z.string().optional(),
1351
+ agent: z.string().optional(),
1352
+ timeout: z.number().int().optional(),
1353
+ hostFingerprint: z.string().optional(),
1354
+ knownHostsFile: z.string().optional()
1400
1355
  });
1401
- var transferSchema = z.object({
1402
- recursive: z.boolean().optional(),
1403
- overwrite: z.boolean().optional(),
1404
- createDirectories: z.boolean().optional(),
1405
- dryRun: z.boolean().optional(),
1406
- timeout: z.number().int().optional(),
1407
- postUploadCommands: z.array(z.string().refine((command) => command.trim().length > 0)).optional()
1356
+ const transferSchema = z.object({
1357
+ recursive: z.boolean().optional(),
1358
+ overwrite: z.boolean().optional(),
1359
+ createDirectories: z.boolean().optional(),
1360
+ dryRun: z.boolean().optional(),
1361
+ timeout: z.number().int().optional(),
1362
+ postUploadCommands: z
1363
+ .array(z.string().refine((command) => command.trim().length > 0))
1364
+ .optional()
1408
1365
  });
1409
- var jobSchema = transferSchema.extend({
1410
- operation: z.enum(["upload", "download"]),
1411
- profile: z.string().optional(),
1412
- source: z.string(),
1413
- destination: z.string()
1366
+ const jobSchema = transferSchema.extend({
1367
+ operation: z.enum(["upload", "download"]),
1368
+ profile: z.string().optional(),
1369
+ source: z.string(),
1370
+ destination: z.string()
1414
1371
  });
1415
- var configSchema = serverSchema.extend({
1416
- defaultProfile: z.string().optional(),
1417
- server: serverSchema.optional(),
1418
- transfer: transferSchema.optional(),
1419
- profiles: z.record(serverSchema).optional(),
1420
- jobs: z.record(jobSchema).optional()
1421
- }).passthrough();
1372
+ const configSchema = serverSchema
1373
+ .extend({
1374
+ defaultProfile: z.string().optional(),
1375
+ server: serverSchema.optional(),
1376
+ transfer: transferSchema.optional(),
1377
+ profiles: z.record(serverSchema).optional(),
1378
+ jobs: z.record(jobSchema).optional()
1379
+ })
1380
+ .passthrough();
1422
1381
  async function fileExists(filePath) {
1423
- try {
1424
- await access5(filePath);
1425
- return true;
1426
- } catch {
1427
- return false;
1428
- }
1382
+ try {
1383
+ await access(filePath);
1384
+ return true;
1385
+ }
1386
+ catch {
1387
+ return false;
1388
+ }
1429
1389
  }
1430
1390
  async function findConfigFile(cwd = process.cwd()) {
1431
- for (const filename of CONFIG_FILENAMES) {
1432
- const candidate = path5.resolve(cwd, filename);
1433
- if (await fileExists(candidate)) {
1434
- return candidate;
1391
+ for (const filename of CONFIG_FILENAMES) {
1392
+ const candidate = path.resolve(cwd, filename);
1393
+ if (await fileExists(candidate)) {
1394
+ return candidate;
1395
+ }
1435
1396
  }
1436
- }
1437
- return void 0;
1397
+ return undefined;
1438
1398
  }
1439
1399
  async function loadConfig(explicitPath, cwd = process.cwd()) {
1440
- const configPath = explicitPath ? resolveLocalPath(explicitPath, cwd) : await findConfigFile(cwd);
1441
- if (!configPath) {
1442
- return { config: {}, directory: cwd };
1443
- }
1444
- let raw;
1445
- try {
1446
- raw = await readFile3(configPath, "utf8");
1447
- } catch (error) {
1448
- throw new ConfigurationError(`Unable to read configuration file: ${configPath}`, {
1449
- cause: error,
1450
- context: { path: configPath }
1451
- });
1452
- }
1453
- try {
1454
- const parsed = JSON.parse(raw);
1455
- const result = configSchema.safeParse(parsed);
1456
- if (!result.success) {
1457
- throw new ConfigurationError(`Invalid configuration file: ${result.error.message}`, {
1458
- context: { path: configPath }
1459
- });
1400
+ const configPath = explicitPath ? resolveLocalPath(explicitPath, cwd) : await findConfigFile(cwd);
1401
+ if (!configPath) {
1402
+ return { config: {}, directory: cwd };
1460
1403
  }
1461
- return {
1462
- config: result.data,
1463
- path: configPath,
1464
- directory: path5.dirname(configPath)
1465
- };
1466
- } catch (error) {
1467
- if (error instanceof ConfigurationError) {
1468
- throw error;
1404
+ let raw;
1405
+ try {
1406
+ raw = await readFile(configPath, "utf8");
1407
+ }
1408
+ catch (error) {
1409
+ throw new ConfigurationError(`Unable to read configuration file: ${configPath}`, {
1410
+ cause: error,
1411
+ context: { path: configPath }
1412
+ });
1413
+ }
1414
+ try {
1415
+ const parsed = JSON.parse(raw);
1416
+ const result = configSchema.safeParse(parsed);
1417
+ if (!result.success) {
1418
+ throw new ConfigurationError(`Invalid configuration file: ${result.error.message}`, {
1419
+ context: { path: configPath }
1420
+ });
1421
+ }
1422
+ return {
1423
+ config: result.data,
1424
+ path: configPath,
1425
+ directory: path.dirname(configPath)
1426
+ };
1427
+ }
1428
+ catch (error) {
1429
+ if (error instanceof ConfigurationError) {
1430
+ throw error;
1431
+ }
1432
+ throw new ConfigurationError(`Invalid JSON in configuration file: ${configPath}`, {
1433
+ cause: error,
1434
+ context: { path: configPath }
1435
+ });
1469
1436
  }
1470
- throw new ConfigurationError(`Invalid JSON in configuration file: ${configPath}`, {
1471
- cause: error,
1472
- context: { path: configPath }
1473
- });
1474
- }
1475
1437
  }
1476
1438
 
1477
- // src/config/resolve-config.ts
1478
- import path6 from "path";
1479
- import { assignDefined } from "mazey";
1480
-
1481
- // src/config/jobs.ts
1482
1439
  function selectJob(config, jobName) {
1483
- const job = config.jobs?.[jobName];
1484
- if (!job) {
1485
- throw new ConfigurationError(`Unknown job: ${jobName}`, {
1486
- context: { job: jobName }
1487
- });
1488
- }
1489
- return job;
1440
+ const job = config.jobs?.[jobName];
1441
+ if (!job) {
1442
+ throw new ConfigurationError(`Unknown job: ${jobName}`, {
1443
+ context: { job: jobName }
1444
+ });
1445
+ }
1446
+ return job;
1490
1447
  }
1491
1448
 
1492
- // src/config/profiles.ts
1493
1449
  function selectProfile(config, profileName) {
1494
- if (!profileName) {
1495
- return void 0;
1496
- }
1497
- const profile = config.profiles?.[profileName];
1498
- if (!profile) {
1499
- throw new ConfigurationError(`Unknown profile: ${profileName}`, {
1500
- context: { profile: profileName }
1501
- });
1502
- }
1503
- return profile;
1450
+ if (!profileName) {
1451
+ return undefined;
1452
+ }
1453
+ const profile = config.profiles?.[profileName];
1454
+ if (!profile) {
1455
+ throw new ConfigurationError(`Unknown profile: ${profileName}`, {
1456
+ context: { profile: profileName }
1457
+ });
1458
+ }
1459
+ return profile;
1504
1460
  }
1505
1461
 
1506
- // src/config/resolve-config.ts
1507
- var DEFAULT_TRANSFER = {
1508
- port: 22,
1509
- recursive: false,
1510
- overwrite: false,
1511
- createDirectories: true,
1512
- dryRun: false
1462
+ const DEFAULT_TRANSFER = {
1463
+ port: 22,
1464
+ recursive: false,
1465
+ overwrite: false,
1466
+ createDirectories: true,
1467
+ dryRun: false
1513
1468
  };
1514
1469
  function rootServer(config) {
1515
- const result = {};
1516
- assignDefined(result, config.server);
1517
- assignDefined(result, {
1518
- host: config.host,
1519
- port: config.port,
1520
- username: config.username,
1521
- password: config.password,
1522
- privateKey: config.privateKey,
1523
- privateKeyFile: config.privateKeyFile,
1524
- passphrase: config.passphrase,
1525
- agent: config.agent,
1526
- timeout: config.timeout,
1527
- hostFingerprint: config.hostFingerprint,
1528
- knownHostsFile: config.knownHostsFile
1529
- });
1530
- return result;
1470
+ const result = {};
1471
+ assignDefined(result, config.server);
1472
+ assignDefined(result, {
1473
+ host: config.host,
1474
+ port: config.port,
1475
+ username: config.username,
1476
+ password: config.password,
1477
+ privateKey: config.privateKey,
1478
+ privateKeyFile: config.privateKeyFile,
1479
+ passphrase: config.passphrase,
1480
+ agent: config.agent,
1481
+ timeout: config.timeout,
1482
+ hostFingerprint: config.hostFingerprint,
1483
+ knownHostsFile: config.knownHostsFile
1484
+ });
1485
+ return result;
1531
1486
  }
1532
1487
  function jobTransfer(job) {
1533
- const result = {};
1534
- if (!job) {
1535
- return result;
1536
- }
1537
- return assignDefined(result, {
1538
- recursive: job.recursive,
1539
- overwrite: job.overwrite,
1540
- createDirectories: job.createDirectories,
1541
- dryRun: job.dryRun,
1542
- timeout: job.timeout,
1543
- postUploadCommands: readPostUploadCommands(job)
1544
- });
1488
+ const result = {};
1489
+ if (!job) {
1490
+ return result;
1491
+ }
1492
+ return assignDefined(result, {
1493
+ recursive: job.recursive,
1494
+ overwrite: job.overwrite,
1495
+ createDirectories: job.createDirectories,
1496
+ dryRun: job.dryRun,
1497
+ timeout: job.timeout,
1498
+ postUploadCommands: readPostUploadCommands(job)
1499
+ });
1545
1500
  }
1546
1501
  function resolveConfigRelatedPaths(resolved, operation, configDirectory) {
1547
- const next = { ...resolved };
1548
- if (next.privateKeyFile && !next.privateKeyFile.startsWith("~")) {
1549
- next.privateKeyFile = path6.isAbsolute(next.privateKeyFile) ? next.privateKeyFile : resolveLocalPath(next.privateKeyFile, configDirectory);
1550
- }
1551
- if (next.knownHostsFile && !next.knownHostsFile.startsWith("~")) {
1552
- next.knownHostsFile = path6.isAbsolute(next.knownHostsFile) ? next.knownHostsFile : resolveLocalPath(next.knownHostsFile, configDirectory);
1553
- }
1554
- if (operation === "upload" && !next.source.startsWith("~")) {
1555
- next.source = path6.isAbsolute(next.source) ? next.source : resolveLocalPath(next.source, configDirectory);
1556
- }
1557
- if (operation === "download" && !next.destination.startsWith("~")) {
1558
- next.destination = path6.isAbsolute(next.destination) ? next.destination : resolveLocalPath(next.destination, configDirectory);
1559
- }
1560
- return next;
1502
+ const next = { ...resolved };
1503
+ if (next.privateKeyFile && !next.privateKeyFile.startsWith("~")) {
1504
+ next.privateKeyFile = path.isAbsolute(next.privateKeyFile)
1505
+ ? next.privateKeyFile
1506
+ : resolveLocalPath(next.privateKeyFile, configDirectory);
1507
+ }
1508
+ if (next.knownHostsFile && !next.knownHostsFile.startsWith("~")) {
1509
+ next.knownHostsFile = path.isAbsolute(next.knownHostsFile)
1510
+ ? next.knownHostsFile
1511
+ : resolveLocalPath(next.knownHostsFile, configDirectory);
1512
+ }
1513
+ if (operation === "upload" && !next.source.startsWith("~")) {
1514
+ next.source = path.isAbsolute(next.source)
1515
+ ? next.source
1516
+ : resolveLocalPath(next.source, configDirectory);
1517
+ }
1518
+ if (operation === "download" && !next.destination.startsWith("~")) {
1519
+ next.destination = path.isAbsolute(next.destination)
1520
+ ? next.destination
1521
+ : resolveLocalPath(next.destination, configDirectory);
1522
+ }
1523
+ return next;
1561
1524
  }
1562
1525
  function resolveTransferConfig(input) {
1563
- const config = input.config ?? {};
1564
- const env = input.env ?? {};
1565
- const cli = input.cli ?? {};
1566
- const configDirectory = input.configDirectory ?? process.cwd();
1567
- const cwd = input.cwd ?? process.cwd();
1568
- const job = input.jobName ? selectJob(config, input.jobName) : void 0;
1569
- const operation = input.operation ?? job?.operation;
1570
- if (!operation) {
1571
- throw new Error("Transfer operation is required.");
1572
- }
1573
- const selectedProfileName = cli.profile ?? env.profile ?? job?.profile ?? config.defaultProfile;
1574
- const profile = selectProfile(config, selectedProfileName);
1575
- const resolved = {
1576
- ...DEFAULT_TRANSFER,
1577
- operation,
1578
- source: job?.source ?? "",
1579
- destination: job?.destination ?? ""
1580
- };
1581
- assignDefined(resolved, jobTransfer(job));
1582
- assignDefined(resolved, config.transfer);
1583
- assignDefined(resolved, rootServer(config));
1584
- assignDefined(resolved, profile);
1585
- assignDefined(resolved, {
1586
- host: env.host,
1587
- port: env.port,
1588
- username: env.username,
1589
- password: env.password,
1590
- privateKey: env.privateKey,
1591
- privateKeyFile: env.privateKeyFile,
1592
- passphrase: env.passphrase,
1593
- timeout: env.timeout
1594
- });
1595
- if (input.source !== void 0) resolved.source = input.source;
1596
- if (input.destination !== void 0) resolved.destination = input.destination;
1597
- assignDefined(resolved, cli);
1598
- if (selectedProfileName) {
1599
- resolved.profile = selectedProfileName;
1600
- }
1601
- const pathResolved = resolveConfigRelatedPaths(resolved, operation, configDirectory);
1602
- getPostUploadCommands(pathResolved);
1603
- if (input.source && operation === "upload") {
1604
- pathResolved.source = resolveLocalPath(input.source, cwd);
1605
- }
1606
- if (operation === "upload") {
1607
- pathResolved.destination = restoreMsysConvertedRemotePath(pathResolved.destination);
1608
- }
1609
- if (operation === "download") {
1610
- pathResolved.source = restoreMsysConvertedRemotePath(pathResolved.source);
1611
- if (getPostUploadCommands(job)?.length || getPostUploadCommands(cli)?.length) {
1612
- throw new ValidationError("Post-upload commands are not supported for downloads.");
1613
- }
1614
- clearPostUploadCommands(pathResolved);
1615
- }
1616
- if (input.destination && operation === "download") {
1617
- pathResolved.destination = resolveLocalPath(input.destination, cwd);
1618
- }
1619
- return pathResolved;
1526
+ const config = input.config ?? {};
1527
+ const env = input.env ?? {};
1528
+ const cli = input.cli ?? {};
1529
+ const configDirectory = input.configDirectory ?? process.cwd();
1530
+ const cwd = input.cwd ?? process.cwd();
1531
+ const job = input.jobName ? selectJob(config, input.jobName) : undefined;
1532
+ const operation = input.operation ?? job?.operation;
1533
+ if (!operation) {
1534
+ throw new Error("Transfer operation is required.");
1535
+ }
1536
+ const selectedProfileName = cli.profile ?? env.profile ?? job?.profile ?? config.defaultProfile;
1537
+ const profile = selectProfile(config, selectedProfileName);
1538
+ const resolved = {
1539
+ ...DEFAULT_TRANSFER,
1540
+ operation,
1541
+ source: job?.source ?? "",
1542
+ destination: job?.destination ?? ""
1543
+ };
1544
+ assignDefined(resolved, jobTransfer(job));
1545
+ assignDefined(resolved, config.transfer);
1546
+ assignDefined(resolved, rootServer(config));
1547
+ assignDefined(resolved, profile);
1548
+ assignDefined(resolved, {
1549
+ host: env.host,
1550
+ port: env.port,
1551
+ username: env.username,
1552
+ password: env.password,
1553
+ privateKey: env.privateKey,
1554
+ privateKeyFile: env.privateKeyFile,
1555
+ passphrase: env.passphrase,
1556
+ timeout: env.timeout
1557
+ });
1558
+ if (input.source !== undefined)
1559
+ resolved.source = input.source;
1560
+ if (input.destination !== undefined)
1561
+ resolved.destination = input.destination;
1562
+ assignDefined(resolved, cli);
1563
+ if (selectedProfileName) {
1564
+ resolved.profile = selectedProfileName;
1565
+ }
1566
+ const pathResolved = resolveConfigRelatedPaths(resolved, operation, configDirectory);
1567
+ getPostUploadCommands(pathResolved);
1568
+ if (input.source && operation === "upload") {
1569
+ pathResolved.source = resolveLocalPath(input.source, cwd);
1570
+ }
1571
+ if (operation === "upload") {
1572
+ pathResolved.destination = restoreMsysConvertedRemotePath(pathResolved.destination);
1573
+ }
1574
+ if (operation === "download") {
1575
+ pathResolved.source = restoreMsysConvertedRemotePath(pathResolved.source);
1576
+ if (getPostUploadCommands(job)?.length ||
1577
+ getPostUploadCommands(cli)?.length) {
1578
+ throw new ValidationError("Post-upload commands are not supported for downloads.");
1579
+ }
1580
+ clearPostUploadCommands(pathResolved);
1581
+ }
1582
+ if (input.destination && operation === "download") {
1583
+ pathResolved.destination = resolveLocalPath(input.destination, cwd);
1584
+ }
1585
+ return pathResolved;
1620
1586
  }
1621
1587
 
1622
- // src/cli/output.ts
1623
- import { formatByteSize } from "mazey";
1624
1588
  function writeDryRun(output, config) {
1625
- const postUploadCommands = getPostUploadCommands(config) ?? [];
1626
- const destination = config.operation === "upload" ? `${config.username ?? "user"}@${config.host ?? "host"}:${config.destination}` : config.destination;
1627
- output.stdout.write(`Dry run: ${config.operation}
1628
- `);
1629
- output.stdout.write(`Source: ${config.source}
1630
- `);
1631
- output.stdout.write(`Destination: ${destination}
1632
- `);
1633
- output.stdout.write(`Recursive: ${config.recursive ? "yes" : "no"}
1634
- `);
1635
- output.stdout.write(`Overwrite: ${config.overwrite ? "yes" : "no"}
1636
- `);
1637
- if (config.operation === "upload" && postUploadCommands.length > 0) {
1638
- output.stdout.write("\nPost-upload commands:\n");
1639
- postUploadCommands.forEach((command, index) => {
1640
- output.stdout.write(
1641
- `${index + 1}. ${redactKnownSensitiveValues(command, config)}
1642
- `
1643
- );
1644
- });
1645
- }
1589
+ const postUploadCommands = getPostUploadCommands(config) ?? [];
1590
+ const destination = config.operation === "upload"
1591
+ ? `${config.username ?? "user"}@${config.host ?? "host"}:${config.destination}`
1592
+ : config.destination;
1593
+ output.stdout.write(`Dry run: ${config.operation}\n`);
1594
+ output.stdout.write(`Source: ${config.source}\n`);
1595
+ output.stdout.write(`Destination: ${destination}\n`);
1596
+ output.stdout.write(`Recursive: ${config.recursive ? "yes" : "no"}\n`);
1597
+ output.stdout.write(`Overwrite: ${config.overwrite ? "yes" : "no"}\n`);
1598
+ if (config.operation === "upload" && postUploadCommands.length > 0) {
1599
+ output.stdout.write("\nPost-upload commands:\n");
1600
+ postUploadCommands.forEach((command, index) => {
1601
+ output.stdout.write(`${index + 1}. ${redactKnownSensitiveValues(command, config)}\n`);
1602
+ });
1603
+ }
1646
1604
  }
1647
1605
  function writeCommandResults(output, results, config) {
1648
- results.forEach((result, index) => {
1649
- output.stdout.write(`Remote command ${index + 1} completed successfully.
1650
- `);
1651
- if (result.stdout) {
1652
- output.stdout.write(
1653
- redactKnownSensitiveValues(result.stdout, config)
1654
- );
1655
- if (!result.stdout.endsWith("\n")) output.stdout.write("\n");
1656
- }
1657
- if (result.stderr) {
1658
- output.stderr.write(
1659
- redactKnownSensitiveValues(result.stderr, config)
1660
- );
1661
- if (!result.stderr.endsWith("\n")) output.stderr.write("\n");
1662
- }
1663
- });
1606
+ results.forEach((result, index) => {
1607
+ output.stdout.write(`Remote command ${index + 1} completed successfully.\n`);
1608
+ if (result.stdout) {
1609
+ output.stdout.write(redactKnownSensitiveValues(result.stdout, config));
1610
+ if (!result.stdout.endsWith("\n"))
1611
+ output.stdout.write("\n");
1612
+ }
1613
+ if (result.stderr) {
1614
+ output.stderr.write(redactKnownSensitiveValues(result.stderr, config));
1615
+ if (!result.stderr.endsWith("\n"))
1616
+ output.stderr.write("\n");
1617
+ }
1618
+ });
1664
1619
  }
1665
1620
  function createProgressReporter(output, quiet) {
1666
- if (quiet || !output.stderr.isTTY) {
1667
- return void 0;
1668
- }
1669
- const minimumUpdateIntervalMs = 500;
1670
- let lastLineLength = 0;
1671
- let lastPercentage;
1672
- let lastWriteAt = 0;
1673
- let completed = false;
1674
- return (progress) => {
1675
- if (completed) {
1676
- return;
1677
- }
1678
- const now = Date.now();
1679
- const totalBytes = progress.totalBytes;
1680
- const isComplete = progress.percentage === 100 || totalBytes !== void 0 && totalBytes > 0 && progress.transferredBytes >= totalBytes;
1681
- const percentageChanged = progress.percentage !== void 0 && progress.percentage !== lastPercentage;
1682
- if (!isComplete && !percentageChanged && lastWriteAt !== 0 && now - lastWriteAt < minimumUpdateIntervalMs) {
1683
- return;
1684
- }
1685
- const total = progress.totalBytes ? formatByteSize(progress.totalBytes) : "?";
1686
- const transferred = formatByteSize(progress.transferredBytes);
1687
- const percentage2 = progress.percentage === void 0 ? "" : ` (${progress.percentage}%)`;
1688
- const currentFile = progress.currentFile ? `: ${progress.currentFile}` : "";
1689
- const line = `${progress.operation === "upload" ? "Uploading" : "Downloading"}${currentFile} ${transferred} / ${total}${percentage2}`;
1690
- const padding = " ".repeat(Math.max(0, lastLineLength - line.length));
1691
- output.stderr.write(`\r${line}${padding}`);
1692
- lastLineLength = line.length;
1693
- lastPercentage = progress.percentage;
1694
- lastWriteAt = now;
1695
- if (isComplete) {
1696
- output.stderr.write("\n");
1697
- completed = true;
1698
- }
1699
- };
1621
+ if (quiet || !output.stderr.isTTY) {
1622
+ return undefined;
1623
+ }
1624
+ const minimumUpdateIntervalMs = 500;
1625
+ let lastLineLength = 0;
1626
+ let lastPercentage;
1627
+ let lastWriteAt = 0;
1628
+ let completed = false;
1629
+ return (progress) => {
1630
+ if (completed) {
1631
+ return;
1632
+ }
1633
+ const now = Date.now();
1634
+ const totalBytes = progress.totalBytes;
1635
+ const isComplete = progress.percentage === 100 ||
1636
+ (totalBytes !== undefined && totalBytes > 0 && progress.transferredBytes >= totalBytes);
1637
+ const percentageChanged = progress.percentage !== undefined && progress.percentage !== lastPercentage;
1638
+ if (!isComplete &&
1639
+ !percentageChanged &&
1640
+ lastWriteAt !== 0 &&
1641
+ now - lastWriteAt < minimumUpdateIntervalMs) {
1642
+ return;
1643
+ }
1644
+ const total = progress.totalBytes ? formatByteSize(progress.totalBytes) : "?";
1645
+ const transferred = formatByteSize(progress.transferredBytes);
1646
+ const percentage = progress.percentage === undefined ? "" : ` (${progress.percentage}%)`;
1647
+ const currentFile = progress.currentFile ? `: ${progress.currentFile}` : "";
1648
+ const line = `${progress.operation === "upload" ? "Uploading" : "Downloading"}${currentFile} ${transferred} / ${total}${percentage}`;
1649
+ const padding = " ".repeat(Math.max(0, lastLineLength - line.length));
1650
+ output.stderr.write(`\r${line}${padding}`);
1651
+ lastLineLength = line.length;
1652
+ lastPercentage = progress.percentage;
1653
+ lastWriteAt = now;
1654
+ if (isComplete) {
1655
+ output.stderr.write("\n");
1656
+ completed = true;
1657
+ }
1658
+ };
1700
1659
  }
1701
1660
  function verbosePlan(output, config) {
1702
- const postUploadCommands = getPostUploadCommands(config);
1703
- output.stderr.write(
1704
- `${JSON.stringify(
1705
- redactSensitiveValues({
1661
+ const postUploadCommands = getPostUploadCommands(config);
1662
+ output.stderr.write(`${JSON.stringify(redactSensitiveValues({
1706
1663
  operation: config.operation,
1707
1664
  host: config.host,
1708
1665
  port: config.port,
@@ -1713,263 +1670,259 @@ function verbosePlan(output, config) {
1713
1670
  overwrite: config.overwrite,
1714
1671
  createDirectories: config.createDirectories,
1715
1672
  dryRun: config.dryRun,
1716
- postUploadCommands: postUploadCommands?.map(
1717
- (_command, index) => `command-${index + 1}`
1718
- )
1719
- }),
1720
- null,
1721
- 2
1722
- )}
1723
- `
1724
- );
1673
+ postUploadCommands: postUploadCommands?.map((_command, index) => `command-${index + 1}`)
1674
+ }), null, 2)}\n`);
1725
1675
  }
1726
1676
 
1727
- // src/cli/commands/shared.ts
1728
1677
  function addTransferOptions(command, options = {}) {
1729
- command.option("--host <host>", "SSH server host");
1730
- command.addOption(new Option("--port <port>", "SSH server port").argParser(parseInteger2));
1731
- command.option("--username <username>", "SSH username");
1732
- command.option("--password <password>", "SSH password (prefer environment variables or key files)");
1733
- command.option("--private-key <privateKey>", "Private-key content");
1734
- command.option("--private-key-file <privateKeyFile>", "Private-key file path");
1735
- command.option("--passphrase <passphrase>", "Private-key passphrase");
1736
- command.option("--config <path>", "Configuration file path");
1737
- command.option("--profile <name>", "Named server profile");
1738
- command.option("--recursive", "Transfer directories recursively");
1739
- command.option("--overwrite", "Overwrite existing destination files");
1740
- command.option("--create-directories", "Create missing destination directories");
1741
- command.option("--no-create-directories", "Do not create missing destination directories");
1742
- command.option("--dry-run", "Resolve and validate without connecting or transferring");
1743
- command.addOption(
1744
- new Option("--timeout <milliseconds>", "SSH connection ready timeout").argParser(
1745
- parseInteger2
1746
- )
1747
- );
1748
- command.option("--verbose", "Print verbose diagnostic output");
1749
- command.option("--quiet", "Disable progress and non-error output");
1750
- if (options.postUploadCommand) {
1751
- command.option(
1752
- "--post-upload-command <command>",
1753
- "Run a remote command after a successful upload (repeatable)",
1754
- collectValues
1755
- );
1756
- }
1757
- return command;
1758
- }
1759
- function parseInteger2(value) {
1760
- const parsed = Number.parseInt(value, 10);
1761
- if (!Number.isFinite(parsed)) {
1762
- throw new Error("Expected a number.");
1763
- }
1764
- return parsed;
1678
+ command.option("--host <host>", "SSH server host");
1679
+ command.addOption(new Option("--port <port>", "SSH server port").argParser(parseInteger));
1680
+ command.option("--username <username>", "SSH username");
1681
+ command.option("--password <password>", "SSH password (prefer environment variables or key files)");
1682
+ command.option("--private-key <privateKey>", "Private-key content");
1683
+ command.option("--private-key-file <privateKeyFile>", "Private-key file path");
1684
+ command.option("--passphrase <passphrase>", "Private-key passphrase");
1685
+ command.option("--config <path>", "Configuration file path");
1686
+ command.option("--profile <name>", "Named server profile");
1687
+ command.option("--recursive", "Transfer directories recursively");
1688
+ command.option("--overwrite", "Overwrite existing destination files");
1689
+ command.option("--create-directories", "Create missing destination directories");
1690
+ command.option("--no-create-directories", "Do not create missing destination directories");
1691
+ command.option("--dry-run", "Resolve and validate without connecting or transferring");
1692
+ command.addOption(new Option("--timeout <milliseconds>", "SSH connection ready timeout").argParser(parseInteger));
1693
+ command.option("--verbose", "Print verbose diagnostic output");
1694
+ command.option("--quiet", "Disable progress and non-error output");
1695
+ if (options.postUploadCommand) {
1696
+ command.option("--post-upload-command <command>", "Run a remote command after a successful upload (repeatable)", collectValues);
1697
+ }
1698
+ return command;
1699
+ }
1700
+ function parseInteger(value) {
1701
+ const parsed = Number.parseInt(value, 10);
1702
+ if (!Number.isFinite(parsed)) {
1703
+ throw new Error("Expected a number.");
1704
+ }
1705
+ return parsed;
1765
1706
  }
1766
1707
  function collectValues(value, previous) {
1767
- return [...previous ?? [], value];
1708
+ return [...(previous ?? []), value];
1768
1709
  }
1769
1710
  function toCliOptions(options) {
1770
- return {
1771
- host: options.host,
1772
- port: options.port,
1773
- username: options.username,
1774
- password: options.password,
1775
- privateKey: options.privateKey,
1776
- privateKeyFile: options.privateKeyFile,
1777
- passphrase: options.passphrase,
1778
- profile: options.profile,
1779
- recursive: options.recursive,
1780
- overwrite: options.overwrite,
1781
- createDirectories: options.createDirectories,
1782
- dryRun: options.dryRun,
1783
- timeout: options.timeout,
1784
- postUploadCommands: options.postUploadCommand
1785
- };
1711
+ return {
1712
+ host: options.host,
1713
+ port: options.port,
1714
+ username: options.username,
1715
+ password: options.password,
1716
+ privateKey: options.privateKey,
1717
+ privateKeyFile: options.privateKeyFile,
1718
+ passphrase: options.passphrase,
1719
+ profile: options.profile,
1720
+ recursive: options.recursive,
1721
+ overwrite: options.overwrite,
1722
+ createDirectories: options.createDirectories,
1723
+ dryRun: options.dryRun,
1724
+ timeout: options.timeout,
1725
+ postUploadCommands: options.postUploadCommand
1726
+ };
1786
1727
  }
1787
1728
  async function resolveForCli(input) {
1788
- if (input.options.postUploadCommand?.some((command) => !command.trim())) {
1789
- throw new ValidationError(
1790
- "Each --post-upload-command value must be a non-empty command."
1791
- );
1792
- }
1793
- const loaded = await loadConfig(input.options.config, input.cwd);
1794
- return resolveTransferConfig({
1795
- operation: input.operation,
1796
- source: input.source,
1797
- destination: input.destination,
1798
- jobName: input.jobName,
1799
- cli: toCliOptions(input.options),
1800
- env: readEnvironment(),
1801
- config: loaded.config,
1802
- configDirectory: loaded.directory,
1803
- cwd: input.cwd
1804
- });
1729
+ if (input.options.postUploadCommand?.some((command) => !command.trim())) {
1730
+ throw new ValidationError("Each --post-upload-command value must be a non-empty command.");
1731
+ }
1732
+ const loaded = await loadConfig(input.options.config, input.cwd);
1733
+ return resolveTransferConfig({
1734
+ operation: input.operation,
1735
+ source: input.source,
1736
+ destination: input.destination,
1737
+ jobName: input.jobName,
1738
+ cli: toCliOptions(input.options),
1739
+ env: readEnvironment(),
1740
+ config: loaded.config,
1741
+ configDirectory: loaded.directory,
1742
+ cwd: input.cwd
1743
+ });
1805
1744
  }
1806
1745
  async function executeResolvedTransfer(config, handlers, output, commandOptions) {
1807
- if (commandOptions.verbose) {
1808
- verbosePlan(output, config);
1809
- }
1810
- if (config.dryRun) {
1811
- if (!commandOptions.quiet) {
1812
- writeDryRun(output, config);
1813
- }
1814
- return;
1815
- }
1816
- const onProgress = createProgressReporter(output, commandOptions.quiet);
1817
- if (config.operation === "upload") {
1818
- const uploadOptions = {
1819
- ...config,
1820
- localPath: config.source,
1821
- remotePath: config.destination
1746
+ if (commandOptions.verbose) {
1747
+ verbosePlan(output, config);
1748
+ }
1749
+ if (config.dryRun) {
1750
+ if (!commandOptions.quiet) {
1751
+ writeDryRun(output, config);
1752
+ }
1753
+ return;
1754
+ }
1755
+ const onProgress = createProgressReporter(output, commandOptions.quiet);
1756
+ if (config.operation === "upload") {
1757
+ const uploadOptions = {
1758
+ ...config,
1759
+ localPath: config.source,
1760
+ remotePath: config.destination
1761
+ };
1762
+ if (onProgress)
1763
+ uploadOptions.onProgress = onProgress;
1764
+ const results = await handlers.upload(uploadOptions);
1765
+ if (!commandOptions.quiet) {
1766
+ writeCommandResults(output, results, config);
1767
+ }
1768
+ return;
1769
+ }
1770
+ const downloadOptions = {
1771
+ ...config,
1772
+ remotePath: config.source,
1773
+ localPath: config.destination
1822
1774
  };
1823
- if (onProgress) uploadOptions.onProgress = onProgress;
1824
- const results = await handlers.upload(uploadOptions);
1825
- if (!commandOptions.quiet) {
1826
- writeCommandResults(output, results, config);
1827
- }
1828
- return;
1829
- }
1830
- const downloadOptions = {
1831
- ...config,
1832
- remotePath: config.source,
1833
- localPath: config.destination
1834
- };
1835
- if (onProgress) downloadOptions.onProgress = onProgress;
1836
- await handlers.download(downloadOptions);
1775
+ if (onProgress)
1776
+ downloadOptions.onProgress = onProgress;
1777
+ await handlers.download(downloadOptions);
1837
1778
  }
1838
1779
 
1839
- // src/cli/commands/download.ts
1840
1780
  function createDownloadCommand(handlers, output, cwd) {
1841
- const command = new Command("download").description("Download a remote source path to a local destination path").usage("<source> <destination> [options]").argument("[source]", "Remote file or directory path").argument("[destination]", "Local file or directory").action(async (source, destination, options) => {
1842
- assertSourceDestination("download", source, destination);
1843
- const resolved = await resolveForCli({
1844
- operation: "download",
1845
- source,
1846
- destination,
1847
- options,
1848
- cwd
1781
+ const command = new Command("download")
1782
+ .description("Download a remote source path to a local destination path")
1783
+ .usage("<source> <destination> [options]")
1784
+ .argument("[source]", "Remote file or directory path")
1785
+ .argument("[destination]", "Local file or directory")
1786
+ .action(async (source, destination, options) => {
1787
+ assertSourceDestination("download", source, destination);
1788
+ const resolved = await resolveForCli({
1789
+ operation: "download",
1790
+ source,
1791
+ destination,
1792
+ options,
1793
+ cwd
1794
+ });
1795
+ await executeResolvedTransfer(resolved, handlers, output, options);
1849
1796
  });
1850
- await executeResolvedTransfer(resolved, handlers, output, options);
1851
- });
1852
- addTransferOptions(command);
1853
- return command;
1797
+ addTransferOptions(command);
1798
+ return command;
1854
1799
  }
1855
1800
 
1856
- // src/cli/commands/run.ts
1857
- import { Command as Command2 } from "commander";
1858
1801
  function createRunCommand(handlers, output, cwd) {
1859
- const command = new Command2("run").description("Run a configured transfer job").usage("<job> [source] [destination] [options]").argument("<job>", "Configured job name").argument("[source]", "Optional source override").argument("[destination]", "Optional destination override").action(
1860
- async (jobName, source, destination, options) => {
1861
- const resolved = await resolveForCli({
1862
- source,
1863
- destination,
1864
- jobName,
1865
- options,
1866
- cwd
1867
- });
1868
- await executeResolvedTransfer(resolved, handlers, output, options);
1869
- }
1870
- );
1871
- addTransferOptions(command, { postUploadCommand: true });
1872
- return command;
1802
+ const command = new Command("run")
1803
+ .description("Run a configured transfer job")
1804
+ .usage("<job> [source] [destination] [options]")
1805
+ .argument("<job>", "Configured job name")
1806
+ .argument("[source]", "Optional source override")
1807
+ .argument("[destination]", "Optional destination override")
1808
+ .action(async (jobName, source, destination, options) => {
1809
+ const resolved = await resolveForCli({
1810
+ source,
1811
+ destination,
1812
+ jobName,
1813
+ options,
1814
+ cwd
1815
+ });
1816
+ await executeResolvedTransfer(resolved, handlers, output, options);
1817
+ });
1818
+ addTransferOptions(command, { postUploadCommand: true });
1819
+ return command;
1873
1820
  }
1874
1821
 
1875
- // src/cli/commands/upload.ts
1876
- import { Command as Command3 } from "commander";
1877
1822
  function createUploadCommand(handlers, output, cwd) {
1878
- const command = new Command3("upload").description("Upload a local source path to a remote destination path").usage("<source> <destination> [options]").argument("[source]", "Local file or directory").argument("[destination]", "Remote file or directory path").action(async (source, destination, options) => {
1879
- assertSourceDestination("upload", source, destination);
1880
- const resolved = await resolveForCli({
1881
- operation: "upload",
1882
- source,
1883
- destination,
1884
- options,
1885
- cwd
1823
+ const command = new Command("upload")
1824
+ .description("Upload a local source path to a remote destination path")
1825
+ .usage("<source> <destination> [options]")
1826
+ .argument("[source]", "Local file or directory")
1827
+ .argument("[destination]", "Remote file or directory path")
1828
+ .action(async (source, destination, options) => {
1829
+ assertSourceDestination("upload", source, destination);
1830
+ const resolved = await resolveForCli({
1831
+ operation: "upload",
1832
+ source,
1833
+ destination,
1834
+ options,
1835
+ cwd
1836
+ });
1837
+ await executeResolvedTransfer(resolved, handlers, output, options);
1886
1838
  });
1887
- await executeResolvedTransfer(resolved, handlers, output, options);
1888
- });
1889
- addTransferOptions(command, { postUploadCommand: true });
1890
- return command;
1839
+ addTransferOptions(command, { postUploadCommand: true });
1840
+ return command;
1891
1841
  }
1892
1842
 
1893
- // src/cli/index.ts
1894
1843
  function readPackageVersion(currentModuleUrl = import.meta.url) {
1895
- const currentFile = fileURLToPath(currentModuleUrl);
1896
- const packageJsonPath = resolve(dirname(currentFile), "..", "..", "package.json");
1897
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1898
- return packageJson.version ?? "0.0.0";
1844
+ const currentFile = fileURLToPath(currentModuleUrl);
1845
+ const packageJsonPath = resolve(dirname(currentFile), "..", "..", "package.json");
1846
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1847
+ return packageJson.version ?? "0.0.0";
1899
1848
  }
1900
1849
  function isCliEntrypoint(currentModuleUrl = import.meta.url, argv = process.argv) {
1901
- const executedFile = argv[1];
1902
- if (!executedFile) {
1903
- return false;
1904
- }
1905
- try {
1906
- return realpathSync(fileURLToPath(currentModuleUrl)) === realpathSync(executedFile);
1907
- } catch {
1908
- return false;
1909
- }
1850
+ const executedFile = argv[1];
1851
+ if (!executedFile) {
1852
+ return false;
1853
+ }
1854
+ try {
1855
+ return realpathSync(fileURLToPath(currentModuleUrl)) === realpathSync(executedFile);
1856
+ }
1857
+ catch {
1858
+ return false;
1859
+ }
1910
1860
  }
1911
1861
  async function runCli(options = {}) {
1912
- const output = options.output ?? { stdout: process.stdout, stderr: process.stderr };
1913
- const cwd = options.cwd ?? process.cwd();
1914
- const handlers = options.handlers ?? { upload, download };
1915
- const program = new Command4();
1916
- program.name("scp-next").description("Secure file transfers over SSH using SFTP").version(readPackageVersion()).configureOutput({
1917
- writeOut: (message) => output.stdout.write(message),
1918
- writeErr: (message) => output.stderr.write(message)
1919
- }).configureHelp({
1920
- subcommandTerm: (command) => `${command.name()} ${command.usage()}`
1921
- }).showHelpAfterError().exitOverride();
1922
- program.addCommand(createUploadCommand(handlers, output, cwd));
1923
- program.addCommand(createDownloadCommand(handlers, output, cwd));
1924
- program.addCommand(createRunCommand(handlers, output, cwd));
1925
- try {
1926
- await program.parseAsync(options.argv ?? process.argv, { from: "node" });
1927
- return 0;
1928
- } catch (error) {
1929
- if (error instanceof CommanderError) {
1930
- if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
1862
+ const output = options.output ?? { stdout: process.stdout, stderr: process.stderr };
1863
+ const cwd = options.cwd ?? process.cwd();
1864
+ const handlers = options.handlers ?? { upload, download };
1865
+ const program = new Command();
1866
+ program
1867
+ .name("scp-next")
1868
+ .description("Secure file transfers over SSH using SFTP")
1869
+ .version(readPackageVersion())
1870
+ .configureOutput({
1871
+ writeOut: (message) => output.stdout.write(message),
1872
+ writeErr: (message) => output.stderr.write(message)
1873
+ })
1874
+ .configureHelp({
1875
+ subcommandTerm: (command) => `${command.name()} ${command.usage()}`
1876
+ })
1877
+ .showHelpAfterError()
1878
+ .exitOverride();
1879
+ program.addCommand(createUploadCommand(handlers, output, cwd));
1880
+ program.addCommand(createDownloadCommand(handlers, output, cwd));
1881
+ program.addCommand(createRunCommand(handlers, output, cwd));
1882
+ try {
1883
+ await program.parseAsync(options.argv ?? process.argv, { from: "node" });
1931
1884
  return 0;
1932
- }
1933
- output.stderr.write(`${formatErrorMessage(error.message)}
1934
- `);
1935
- return error.exitCode || 1;
1936
- }
1937
- const verbose = (options.argv ?? process.argv).includes("--verbose");
1938
- if (error instanceof RemoteCommandError) {
1939
- if (error.stdout) {
1940
- output.stdout.write(redactKnownSensitiveValues(error.stdout, {}));
1941
- if (!error.stdout.endsWith("\n")) output.stdout.write("\n");
1942
- }
1943
- if (error.stderr) {
1944
- output.stderr.write(redactKnownSensitiveValues(error.stderr, {}));
1945
- if (!error.stderr.endsWith("\n")) output.stderr.write("\n");
1946
- }
1947
1885
  }
1948
- if (error instanceof ScpNextError) {
1949
- output.stderr.write(`Error: ${formatErrorMessage(error.message)}
1950
- `);
1951
- if (verbose && error.cause instanceof Error) {
1952
- output.stderr.write(`${formatErrorMessage(error.cause.stack ?? error.cause.message)}
1953
- `);
1954
- }
1955
- return 1;
1956
- }
1957
- output.stderr.write(`Error: ${formatErrorMessage(error)}
1958
- `);
1959
- if (verbose && error instanceof Error && error.stack) {
1960
- output.stderr.write(`${formatErrorMessage(error.stack)}
1961
- `);
1962
- }
1963
- return 1;
1964
- }
1886
+ catch (error) {
1887
+ if (error instanceof CommanderError) {
1888
+ if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
1889
+ return 0;
1890
+ }
1891
+ output.stderr.write(`${formatErrorMessage(error.message)}\n`);
1892
+ return error.exitCode || 1;
1893
+ }
1894
+ const verbose = (options.argv ?? process.argv).includes("--verbose");
1895
+ if (error instanceof RemoteCommandError) {
1896
+ if (error.stdout) {
1897
+ output.stdout.write(redactKnownSensitiveValues(error.stdout, {}));
1898
+ if (!error.stdout.endsWith("\n"))
1899
+ output.stdout.write("\n");
1900
+ }
1901
+ if (error.stderr) {
1902
+ output.stderr.write(redactKnownSensitiveValues(error.stderr, {}));
1903
+ if (!error.stderr.endsWith("\n"))
1904
+ output.stderr.write("\n");
1905
+ }
1906
+ }
1907
+ if (error instanceof ScpNextError) {
1908
+ output.stderr.write(`Error: ${formatErrorMessage(error.message)}\n`);
1909
+ if (verbose && error.cause instanceof Error) {
1910
+ output.stderr.write(`${formatErrorMessage(error.cause.stack ?? error.cause.message)}\n`);
1911
+ }
1912
+ return 1;
1913
+ }
1914
+ output.stderr.write(`Error: ${formatErrorMessage(error)}\n`);
1915
+ if (verbose && error instanceof Error && error.stack) {
1916
+ output.stderr.write(`${formatErrorMessage(error.stack)}\n`);
1917
+ }
1918
+ return 1;
1919
+ }
1965
1920
  }
1966
1921
  if (isCliEntrypoint()) {
1967
- void runCli().then((exitCode) => {
1968
- process.exitCode = exitCode;
1969
- });
1922
+ void runCli().then((exitCode) => {
1923
+ process.exitCode = exitCode;
1924
+ });
1970
1925
  }
1971
- export {
1972
- isCliEntrypoint,
1973
- runCli
1974
- };
1975
- //# sourceMappingURL=index.js.map
1926
+
1927
+ export { isCliEntrypoint, runCli };
1928
+ //# sourceMappingURL=index.js.map