scp-next 1.0.12 → 1.0.13

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/README.md CHANGED
@@ -16,8 +16,7 @@
16
16
  [docs-url]: https://chengchuu.github.io/scp-next/
17
17
 
18
18
  `scp-next` is an SCP-style command-line tool and library for secure file transfers over SSH.
19
- The package uses SFTP internally through `ssh2-sftp-client` instead of implementing SCP or SFTP protocols manually.
20
-
19
+ It uses SFTP internally through `ssh2-sftp-client` instead of implementing SCP or SFTP protocols manually.
21
20
  Developer documentation is available at <https://chengchuu.github.io/scp-next/>.
22
21
 
23
22
  ## Features
@@ -26,19 +25,62 @@ Developer documentation is available at <https://chengchuu.github.io/scp-next/>.
26
25
  - CLI usage with clear `<source> <destination>` operands.
27
26
  - ESM `import` and CommonJS `require` support.
28
27
  - JSON configuration files, named profiles, and configured jobs.
29
- - Environment-variable-based credentials.
30
- - Dry-run mode, progress callbacks, timeouts, overwrite control, and destination directory creation.
31
- - Secret redaction for passwords, passphrases, and private-key contents.
32
28
  - Reusable transfer client.
33
29
 
34
30
  ## Installation
35
31
 
32
+ CLI:
33
+
36
34
  ```bash
37
35
  npm install --global scp-next
36
+ ```
37
+
38
+ Library:
39
+
40
+ ```bash
38
41
  npm install scp-next
39
42
  ```
40
43
 
41
- ## CLI Syntax
44
+ ## Quick Start
45
+
46
+ Upload with password authentication:
47
+
48
+ ```bash
49
+ scp-next upload ./dist /var/www/example \
50
+ --host your-host \
51
+ --username your-username \
52
+ --password your-password \
53
+ --recursive
54
+ ```
55
+
56
+ Download with private-key authentication:
57
+
58
+ ```bash
59
+ scp-next download /var/log/example.log ./logs/example.log \
60
+ --host your-host \
61
+ --username your-username \
62
+ --private-key-file ~/.ssh/id_ed25519
63
+ ```
64
+
65
+ Use the library:
66
+
67
+ ```ts
68
+ import { upload } from "scp-next";
69
+
70
+ await upload({
71
+ host: process.env.SCP_NEXT_HOST,
72
+ username: process.env.SCP_NEXT_USERNAME,
73
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
74
+ localPath: "./dist",
75
+ remotePath: "/var/www/example",
76
+ recursive: true,
77
+ overwrite: true
78
+ });
79
+ ```
80
+
81
+ ## Basic Usage
82
+
83
+ ### CLI Syntax
42
84
 
43
85
  ```text
44
86
  scp-next upload <source> <destination> [options]
@@ -49,41 +91,33 @@ scp-next run <job> [source] [destination] [options]
49
91
  CLI commands use `<source> <destination>`. Programmatic upload and download APIs use
50
92
  `localPath` and `remotePath`.
51
93
 
94
+ | Operation | Source | Destination |
95
+ | --------- | ------ | ----------- |
96
+ | Upload | Local | Remote |
97
+ | Download | Remote | Local |
98
+
52
99
  Destination paths follow familiar `cp`/`scp` behavior. If the destination exists as a directory or ends with a path separator, `scp-next` places the source inside that directory using the source basename. Missing destination directories are created by default.
53
100
 
54
- ## Upload Examples
101
+ ### Upload Examples
55
102
 
56
103
  ```bash
57
104
  scp-next upload ./dist /var/www/example \
58
- --host example.com \
59
- --username deploy \
105
+ --host your-host \
106
+ --username your-username \
60
107
  --private-key-file ~/.ssh/id_ed25519 \
61
108
  --recursive
62
109
  ```
63
110
 
64
- Password authentication:
65
-
66
- ```bash
67
- scp-next upload ./dist /var/www/example \
68
- --host example.com \
69
- --username deploy \
70
- --password "your-password" \
71
- --recursive
72
- ```
73
-
74
- Password arguments are convenient, but they may be exposed through shell history and process listings.
75
- Prefer `SCP_NEXT_PASSWORD`, SSH agents, or protected private-key files for shared or production environments.
76
-
77
- ## Download Examples
111
+ ### Download Examples
78
112
 
79
113
  ```bash
80
114
  scp-next download /var/log/example.log ./logs/example.log \
81
- --host example.com \
82
- --username deploy \
115
+ --host your-host \
116
+ --username your-username \
83
117
  --private-key-file ~/.ssh/id_ed25519
84
118
  ```
85
119
 
86
- ## CLI Options
120
+ ### CLI Options
87
121
 
88
122
  | Option | Description |
89
123
  | ------------------------------------- | ---------------------------------------------------------------------- |
@@ -107,7 +141,68 @@ scp-next download /var/log/example.log ./logs/example.log \
107
141
  | `--help` | Show command help. |
108
142
  | `--version` | Show the package version. |
109
143
 
110
- ## Configuration Files
144
+ ## Library Usage
145
+
146
+ ### ESM Upload
147
+
148
+ ```ts
149
+ import { upload } from "scp-next";
150
+
151
+ await upload({
152
+ host: process.env.SCP_NEXT_HOST,
153
+ username: process.env.SCP_NEXT_USERNAME,
154
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
155
+ localPath: "./dist",
156
+ remotePath: "/var/www/example",
157
+ recursive: true,
158
+ overwrite: true
159
+ });
160
+ ```
161
+
162
+ ### CommonJS Download
163
+
164
+ ```js
165
+ const { download } = require("scp-next");
166
+
167
+ async function main() {
168
+ await download({
169
+ host: process.env.SCP_NEXT_HOST,
170
+ username: process.env.SCP_NEXT_USERNAME,
171
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
172
+ remotePath: "/var/log/example.log",
173
+ localPath: "./logs/example.log"
174
+ });
175
+ }
176
+
177
+ main().catch((error) => {
178
+ console.error(error.message);
179
+ process.exitCode = 1;
180
+ });
181
+ ```
182
+
183
+ ### Reusable Client
184
+
185
+ ```ts
186
+ import { createClient } from "scp-next";
187
+
188
+ const client = createClient({
189
+ host: process.env.SCP_NEXT_HOST,
190
+ username: process.env.SCP_NEXT_USERNAME,
191
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE
192
+ });
193
+
194
+ try {
195
+ await client.connect();
196
+ await client.upload("./dist", "/var/www/example", { recursive: true });
197
+ await client.download("/var/log/example.log", "./logs/example.log");
198
+ } finally {
199
+ await client.close();
200
+ }
201
+ ```
202
+
203
+ ## Advanced Usage
204
+
205
+ ### Configuration Files
111
206
 
112
207
  `scp-next` searches for these files in the current directory:
113
208
 
@@ -128,9 +223,9 @@ Example:
128
223
  ```json
129
224
  {
130
225
  "server": {
131
- "host": "example.com",
226
+ "host": "your-host",
132
227
  "port": 22,
133
- "username": "deploy",
228
+ "username": "your-username",
134
229
  "privateKeyFile": "~/.ssh/id_ed25519"
135
230
  },
136
231
  "transfer": {
@@ -142,22 +237,22 @@ Example:
142
237
  }
143
238
  ```
144
239
 
145
- ## Named Profiles
240
+ ### Named Profiles
146
241
 
147
242
  ```json
148
243
  {
149
244
  "defaultProfile": "production",
150
245
  "profiles": {
151
246
  "production": {
152
- "host": "production.example.com",
247
+ "host": "your-host-a",
153
248
  "port": 22,
154
- "username": "deploy",
249
+ "username": "your-username-a",
155
250
  "privateKeyFile": "~/.ssh/id_ed25519"
156
251
  },
157
252
  "staging": {
158
- "host": "staging.example.com",
253
+ "host": "your-host-b",
159
254
  "port": 22,
160
- "username": "deploy",
255
+ "username": "your-username-b",
161
256
  "privateKeyFile": "~/.ssh/id_ed25519"
162
257
  }
163
258
  }
@@ -168,7 +263,7 @@ Example:
168
263
  scp-next upload ./dist /var/www/example --profile production
169
264
  ```
170
265
 
171
- ## Configured Jobs
266
+ ### Configured Jobs
172
267
 
173
268
  Jobs use `source` and `destination`; the `operation` determines which path is local or remote.
174
269
 
@@ -176,8 +271,8 @@ Jobs use `source` and `destination`; the `operation` determines which path is lo
176
271
  {
177
272
  "profiles": {
178
273
  "production": {
179
- "host": "production.example.com",
180
- "username": "deploy",
274
+ "host": "your-host",
275
+ "username": "your-username",
181
276
  "privateKeyFile": "~/.ssh/id_ed25519"
182
277
  }
183
278
  },
@@ -208,7 +303,7 @@ scp-next run download-logs
208
303
 
209
304
  `scp-next run <job> [source] [destination]` permits explicit path overrides.
210
305
 
211
- ## Environment Variables
306
+ ### Environment Variables
212
307
 
213
308
  ```text
214
309
  SCP_NEXT_HOST
@@ -225,8 +320,8 @@ SCP_NEXT_PROFILE
225
320
  Bash:
226
321
 
227
322
  ```bash
228
- export SCP_NEXT_HOST="example.com"
229
- export SCP_NEXT_USERNAME="deploy"
323
+ export SCP_NEXT_HOST="your-host"
324
+ export SCP_NEXT_USERNAME="your-username"
230
325
  export SCP_NEXT_PRIVATE_KEY_FILE="$HOME/.ssh/id_ed25519"
231
326
  scp-next upload ./dist /var/www/example --recursive
232
327
  ```
@@ -234,13 +329,13 @@ scp-next upload ./dist /var/www/example --recursive
234
329
  PowerShell:
235
330
 
236
331
  ```powershell
237
- $env:SCP_NEXT_HOST = "example.com"
238
- $env:SCP_NEXT_USERNAME = "deploy"
332
+ $env:SCP_NEXT_HOST = "your-host"
333
+ $env:SCP_NEXT_USERNAME = "your-username"
239
334
  $env:SCP_NEXT_PRIVATE_KEY_FILE = "$HOME\.ssh\id_ed25519"
240
335
  scp-next upload .\dist /var/www/example --recursive
241
336
  ```
242
337
 
243
- ## Configuration Precedence
338
+ ### Configuration Precedence
244
339
 
245
340
  Highest to lowest:
246
341
 
@@ -254,66 +349,7 @@ Highest to lowest:
254
349
 
255
350
  For `run`, job values are loaded first, then root configuration, selected profile, environment variables, explicit positional overrides, and CLI options.
256
351
 
257
- ## Library Usage
258
-
259
- ESM upload:
260
-
261
- ```ts
262
- import { upload } from "scp-next";
263
-
264
- await upload({
265
- host: process.env.SCP_NEXT_HOST,
266
- username: process.env.SCP_NEXT_USERNAME,
267
- privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
268
- localPath: "./dist",
269
- remotePath: "/var/www/example",
270
- recursive: true,
271
- overwrite: true
272
- });
273
- ```
274
-
275
- CommonJS download:
276
-
277
- ```js
278
- const { download } = require("scp-next");
279
-
280
- async function main() {
281
- await download({
282
- host: process.env.SCP_NEXT_HOST,
283
- username: process.env.SCP_NEXT_USERNAME,
284
- privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
285
- remotePath: "/var/log/example.log",
286
- localPath: "./logs/example.log"
287
- });
288
- }
289
-
290
- main().catch((error) => {
291
- console.error(error.message);
292
- process.exitCode = 1;
293
- });
294
- ```
295
-
296
- Reusable client:
297
-
298
- ```ts
299
- import { createClient } from "scp-next";
300
-
301
- const client = createClient({
302
- host: process.env.SCP_NEXT_HOST,
303
- username: process.env.SCP_NEXT_USERNAME,
304
- privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE
305
- });
306
-
307
- try {
308
- await client.connect();
309
- await client.upload("./dist", "/var/www/example", { recursive: true });
310
- await client.download("/var/log/example.log", "./logs/example.log");
311
- } finally {
312
- await client.close();
313
- }
314
- ```
315
-
316
- ## Authentication
352
+ ### Authentication
317
353
 
318
354
  Supported methods:
319
355
 
@@ -323,24 +359,24 @@ Supported methods:
323
359
  - Private-key passphrase
324
360
  - SSH agent authentication through `agent` or `SSH_AUTH_SOCK`
325
361
 
326
- ## Host Verification
362
+ ### Host Verification
327
363
 
328
364
  `hostFingerprint` compares the server host key SHA-256 fingerprint. `knownHostsFile` supports plain OpenSSH `known_hosts` entries for exact host names. When neither is supplied, `scp-next` reads `~/.ssh/known_hosts`. Hashed host names and every OpenSSH marker variant are not currently parsed.
329
365
 
330
366
  `scp-next` fails closed if it cannot establish a host verifier. Use `hostFingerprint` for CI or deployments where a known-hosts file is not available.
331
367
 
332
- ## Progress Reporting
368
+ ### Progress Reporting
333
369
 
334
370
  Interactive terminals display concise progress. Progress is disabled with `--quiet` or when stderr is not a TTY, which keeps CI logs readable. Library users can pass `onProgress`.
335
371
 
336
- ## Dry Run
372
+ ### Dry Run
337
373
 
338
374
  `--dry-run` resolves configuration and validates local paths and transfer direction without connecting to the remote server or modifying local/remote files.
339
375
 
340
376
  ```text
341
377
  Dry run: upload
342
378
  Source: ./dist
343
- Destination: deploy@example.com:/var/www/example
379
+ Destination: your-username@your-host:/var/www/example
344
380
  Recursive: yes
345
381
  Overwrite: yes
346
382
  ```
package/dist/cli/index.js CHANGED
@@ -919,9 +919,12 @@ var ScpNextClientImpl = class {
919
919
  }
920
920
  const exists = await this.transport.exists(remotePath);
921
921
  if (exists) {
922
- throw new TransferError(`Remote destination already exists: ${remotePath}`, {
923
- context: { remotePath }
924
- });
922
+ throw new TransferError(
923
+ `Remote destination already exists: ${remotePath}. Use --overwrite to overwrite it.`,
924
+ {
925
+ context: { remotePath }
926
+ }
927
+ );
925
928
  }
926
929
  }
927
930
  async assertLocalOverwrite(localPath, overwrite = false) {
@@ -1279,15 +1282,36 @@ function createProgressReporter(output, quiet) {
1279
1282
  if (quiet || !output.stderr.isTTY) {
1280
1283
  return void 0;
1281
1284
  }
1285
+ const minimumUpdateIntervalMs = 500;
1286
+ let lastLineLength = 0;
1287
+ let lastPercentage;
1288
+ let lastWriteAt = 0;
1289
+ let completed = false;
1282
1290
  return (progress) => {
1291
+ if (completed) {
1292
+ return;
1293
+ }
1294
+ const now = Date.now();
1295
+ const totalBytes = progress.totalBytes;
1296
+ const isComplete = progress.percentage === 100 || totalBytes !== void 0 && totalBytes > 0 && progress.transferredBytes >= totalBytes;
1297
+ const percentageChanged = progress.percentage !== void 0 && progress.percentage !== lastPercentage;
1298
+ if (!isComplete && !percentageChanged && lastWriteAt !== 0 && now - lastWriteAt < minimumUpdateIntervalMs) {
1299
+ return;
1300
+ }
1283
1301
  const total = progress.totalBytes ? formatBytes(progress.totalBytes) : "?";
1284
1302
  const transferred = formatBytes(progress.transferredBytes);
1285
1303
  const percentage2 = progress.percentage === void 0 ? "" : ` (${progress.percentage}%)`;
1286
1304
  const currentFile = progress.currentFile ? `: ${progress.currentFile}` : "";
1287
- output.stderr.write(`\r${progress.operation === "upload" ? "Uploading" : "Downloading"}${currentFile}
1288
- `);
1289
- output.stderr.write(`${transferred} / ${total}${percentage2}
1290
- `);
1305
+ const line = `${progress.operation === "upload" ? "Uploading" : "Downloading"}${currentFile} ${transferred} / ${total}${percentage2}`;
1306
+ const padding = " ".repeat(Math.max(0, lastLineLength - line.length));
1307
+ output.stderr.write(`\r${line}${padding}`);
1308
+ lastLineLength = line.length;
1309
+ lastPercentage = progress.percentage;
1310
+ lastWriteAt = now;
1311
+ if (isComplete) {
1312
+ output.stderr.write("\n");
1313
+ completed = true;
1314
+ }
1291
1315
  };
1292
1316
  }
1293
1317
  function verbosePlan(output, config) {