scp-next 1.0.12 → 1.0.14

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/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0
3
+ ## 1.0.14
4
4
 
5
- - Initial `scp-next` TypeScript CLI and library.
5
+ - Production-ready `scp-next` TypeScript CLI and library.
6
6
  - Added upload, download, recursive directory transfer, dry-run mode, configuration files,
7
7
  profiles, jobs, environment-variable support, typed errors, and dual ESM/CommonJS builds.
8
+ - Added GitHub Pages developer documentation and expanded README usage sections.
9
+ - Improved SCP-style destination handling for files and directories.
10
+ - Enabled destination directory creation by default with `--no-create-directories` opt-out.
11
+ - Improved host verification guidance, Git Bash remote path handling, and progress output behavior.
12
+ - Removed unused legacy tooling and Docker configuration files.
package/README.md CHANGED
@@ -16,29 +16,79 @@
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
 
22
+ ## Contents
23
+
24
+ - [Installation](#installation)
25
+ - [Quick Start](#quick-start)
26
+ - [Basic Usage](#basic-usage)
27
+ - [Library Usage](#library-usage)
28
+ - [Advanced Usage](#advanced-usage)
29
+
23
30
  ## Features
24
31
 
25
32
  - Upload and download files or directories recursively.
26
33
  - CLI usage with clear `<source> <destination>` operands.
27
34
  - ESM `import` and CommonJS `require` support.
28
35
  - 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
36
  - Reusable transfer client.
33
37
 
34
38
  ## Installation
35
39
 
40
+ CLI:
41
+
36
42
  ```bash
37
43
  npm install --global scp-next
44
+ ```
45
+
46
+ Library:
47
+
48
+ ```bash
38
49
  npm install scp-next
39
50
  ```
40
51
 
41
- ## CLI Syntax
52
+ ## Quick Start
53
+
54
+ Upload with password authentication:
55
+
56
+ ```bash
57
+ scp-next upload ./dist /var/www/example \
58
+ --host your-host \
59
+ --username your-username \
60
+ --password your-password \
61
+ --recursive
62
+ ```
63
+
64
+ Download with private-key authentication:
65
+
66
+ ```bash
67
+ scp-next download /var/log/example.log ./logs/example.log \
68
+ --host your-host \
69
+ --username your-username \
70
+ --private-key-file ~/.ssh/id_ed25519
71
+ ```
72
+
73
+ Use the library:
74
+
75
+ ```ts
76
+ import { upload } from "scp-next";
77
+
78
+ await upload({
79
+ host: process.env.SCP_NEXT_HOST,
80
+ username: process.env.SCP_NEXT_USERNAME,
81
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
82
+ localPath: "./dist",
83
+ remotePath: "/var/www/example",
84
+ recursive: true,
85
+ overwrite: true
86
+ });
87
+ ```
88
+
89
+ ## Basic Usage
90
+
91
+ ### CLI Syntax
42
92
 
43
93
  ```text
44
94
  scp-next upload <source> <destination> [options]
@@ -49,41 +99,33 @@ scp-next run <job> [source] [destination] [options]
49
99
  CLI commands use `<source> <destination>`. Programmatic upload and download APIs use
50
100
  `localPath` and `remotePath`.
51
101
 
102
+ | Operation | Source | Destination |
103
+ | --------- | ------ | ----------- |
104
+ | Upload | Local | Remote |
105
+ | Download | Remote | Local |
106
+
52
107
  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
108
 
54
- ## Upload Examples
109
+ ### Upload Examples
55
110
 
56
111
  ```bash
57
112
  scp-next upload ./dist /var/www/example \
58
- --host example.com \
59
- --username deploy \
113
+ --host your-host \
114
+ --username your-username \
60
115
  --private-key-file ~/.ssh/id_ed25519 \
61
116
  --recursive
62
117
  ```
63
118
 
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
119
+ ### Download Examples
78
120
 
79
121
  ```bash
80
122
  scp-next download /var/log/example.log ./logs/example.log \
81
- --host example.com \
82
- --username deploy \
123
+ --host your-host \
124
+ --username your-username \
83
125
  --private-key-file ~/.ssh/id_ed25519
84
126
  ```
85
127
 
86
- ## CLI Options
128
+ ### CLI Options
87
129
 
88
130
  | Option | Description |
89
131
  | ------------------------------------- | ---------------------------------------------------------------------- |
@@ -107,7 +149,90 @@ scp-next download /var/log/example.log ./logs/example.log \
107
149
  | `--help` | Show command help. |
108
150
  | `--version` | Show the package version. |
109
151
 
110
- ## Configuration Files
152
+ ## Library Usage
153
+
154
+ ### ESM Upload
155
+
156
+ ```ts
157
+ import { upload } from "scp-next";
158
+
159
+ await upload({
160
+ host: process.env.SCP_NEXT_HOST,
161
+ username: process.env.SCP_NEXT_USERNAME,
162
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
163
+ localPath: "./dist",
164
+ remotePath: "/var/www/example",
165
+ recursive: true,
166
+ overwrite: true
167
+ });
168
+ ```
169
+
170
+ ### CommonJS Download
171
+
172
+ ```js
173
+ const { download } = require("scp-next");
174
+
175
+ async function main() {
176
+ await download({
177
+ host: process.env.SCP_NEXT_HOST,
178
+ username: process.env.SCP_NEXT_USERNAME,
179
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE,
180
+ remotePath: "/var/log/example.log",
181
+ localPath: "./logs/example.log"
182
+ });
183
+ }
184
+
185
+ main().catch((error) => {
186
+ console.error(error.message);
187
+ process.exitCode = 1;
188
+ });
189
+ ```
190
+
191
+ ### Reusable Client
192
+
193
+ ```ts
194
+ import { createClient } from "scp-next";
195
+
196
+ const client = createClient({
197
+ host: process.env.SCP_NEXT_HOST,
198
+ username: process.env.SCP_NEXT_USERNAME,
199
+ privateKeyFile: process.env.SCP_NEXT_PRIVATE_KEY_FILE
200
+ });
201
+
202
+ try {
203
+ await client.connect();
204
+ await client.upload("./dist", "/var/www/example", { recursive: true });
205
+ await client.download("/var/log/example.log", "./logs/example.log");
206
+ } finally {
207
+ await client.close();
208
+ }
209
+ ```
210
+
211
+ ### Use a Configuration File
212
+
213
+ The CLI loads configuration files automatically. The library API receives options directly,
214
+ so load JSON in your application and pass the relevant `server` and `transfer` values.
215
+
216
+ ```ts
217
+ import { readFile } from "node:fs/promises";
218
+
219
+ import { upload, type ScpNextConfig } from "scp-next";
220
+
221
+ const config = JSON.parse(
222
+ await readFile(new URL("./scp-next.config.json", import.meta.url), "utf8")
223
+ ) as ScpNextConfig;
224
+
225
+ await upload({
226
+ ...config.server,
227
+ ...config.transfer,
228
+ localPath: "./dist",
229
+ remotePath: "/var/www/example"
230
+ });
231
+ ```
232
+
233
+ ## Advanced Usage
234
+
235
+ ### Configuration Files
111
236
 
112
237
  `scp-next` searches for these files in the current directory:
113
238
 
@@ -123,14 +248,22 @@ Use `--config` for an explicit path:
123
248
  scp-next upload ./dist /var/www/example --config ./deploy/scp-next.json
124
249
  ```
125
250
 
251
+ Use `--profile` with a configuration file:
252
+
253
+ ```bash
254
+ scp-next upload ./dist /var/www/example \
255
+ --config ./scp-next.config.json \
256
+ --profile production
257
+ ```
258
+
126
259
  Example:
127
260
 
128
261
  ```json
129
262
  {
130
263
  "server": {
131
- "host": "example.com",
264
+ "host": "your-host",
132
265
  "port": 22,
133
- "username": "deploy",
266
+ "username": "your-username",
134
267
  "privateKeyFile": "~/.ssh/id_ed25519"
135
268
  },
136
269
  "transfer": {
@@ -142,22 +275,35 @@ Example:
142
275
  }
143
276
  ```
144
277
 
145
- ## Named Profiles
278
+ Configuration file options:
279
+
280
+ | Key | Description |
281
+ | ---------------- | ------------------------------------------------------------------- |
282
+ | `server` | SSH connection options such as `host`, `port`, and `username`. |
283
+ | `transfer` | Transfer defaults such as `recursive`, `overwrite`, and `timeout`. |
284
+ | `defaultProfile` | Profile name used when `--profile` or `SCP_NEXT_PROFILE` is absent. |
285
+ | `profiles` | Named server connection profiles. |
286
+ | `jobs` | Reusable upload or download jobs for `scp-next run <job>`. |
287
+
288
+ Root-level server options such as `host`, `username`, `privateKeyFile`, `knownHostsFile`,
289
+ and `hostFingerprint` are also supported for simple configuration files, but `server` keeps connection values grouped and easier to read.
290
+
291
+ ### Named Profiles
146
292
 
147
293
  ```json
148
294
  {
149
295
  "defaultProfile": "production",
150
296
  "profiles": {
151
297
  "production": {
152
- "host": "production.example.com",
298
+ "host": "your-host-a",
153
299
  "port": 22,
154
- "username": "deploy",
300
+ "username": "your-username-a",
155
301
  "privateKeyFile": "~/.ssh/id_ed25519"
156
302
  },
157
303
  "staging": {
158
- "host": "staging.example.com",
304
+ "host": "your-host-b",
159
305
  "port": 22,
160
- "username": "deploy",
306
+ "username": "your-username-b",
161
307
  "privateKeyFile": "~/.ssh/id_ed25519"
162
308
  }
163
309
  }
@@ -168,7 +314,7 @@ Example:
168
314
  scp-next upload ./dist /var/www/example --profile production
169
315
  ```
170
316
 
171
- ## Configured Jobs
317
+ ### Configured Jobs
172
318
 
173
319
  Jobs use `source` and `destination`; the `operation` determines which path is local or remote.
174
320
 
@@ -176,8 +322,8 @@ Jobs use `source` and `destination`; the `operation` determines which path is lo
176
322
  {
177
323
  "profiles": {
178
324
  "production": {
179
- "host": "production.example.com",
180
- "username": "deploy",
325
+ "host": "your-host",
326
+ "username": "your-username",
181
327
  "privateKeyFile": "~/.ssh/id_ed25519"
182
328
  }
183
329
  },
@@ -208,7 +354,7 @@ scp-next run download-logs
208
354
 
209
355
  `scp-next run <job> [source] [destination]` permits explicit path overrides.
210
356
 
211
- ## Environment Variables
357
+ ### Environment Variables
212
358
 
213
359
  ```text
214
360
  SCP_NEXT_HOST
@@ -225,8 +371,8 @@ SCP_NEXT_PROFILE
225
371
  Bash:
226
372
 
227
373
  ```bash
228
- export SCP_NEXT_HOST="example.com"
229
- export SCP_NEXT_USERNAME="deploy"
374
+ export SCP_NEXT_HOST="your-host"
375
+ export SCP_NEXT_USERNAME="your-username"
230
376
  export SCP_NEXT_PRIVATE_KEY_FILE="$HOME/.ssh/id_ed25519"
231
377
  scp-next upload ./dist /var/www/example --recursive
232
378
  ```
@@ -234,13 +380,13 @@ scp-next upload ./dist /var/www/example --recursive
234
380
  PowerShell:
235
381
 
236
382
  ```powershell
237
- $env:SCP_NEXT_HOST = "example.com"
238
- $env:SCP_NEXT_USERNAME = "deploy"
383
+ $env:SCP_NEXT_HOST = "your-host"
384
+ $env:SCP_NEXT_USERNAME = "your-username"
239
385
  $env:SCP_NEXT_PRIVATE_KEY_FILE = "$HOME\.ssh\id_ed25519"
240
386
  scp-next upload .\dist /var/www/example --recursive
241
387
  ```
242
388
 
243
- ## Configuration Precedence
389
+ ### Configuration Precedence
244
390
 
245
391
  Highest to lowest:
246
392
 
@@ -254,66 +400,7 @@ Highest to lowest:
254
400
 
255
401
  For `run`, job values are loaded first, then root configuration, selected profile, environment variables, explicit positional overrides, and CLI options.
256
402
 
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
403
+ ### Authentication
317
404
 
318
405
  Supported methods:
319
406
 
@@ -323,24 +410,24 @@ Supported methods:
323
410
  - Private-key passphrase
324
411
  - SSH agent authentication through `agent` or `SSH_AUTH_SOCK`
325
412
 
326
- ## Host Verification
413
+ ### Host Verification
327
414
 
328
415
  `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
416
 
330
417
  `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
418
 
332
- ## Progress Reporting
419
+ ### Progress Reporting
333
420
 
334
421
  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
422
 
336
- ## Dry Run
423
+ ### Dry Run
337
424
 
338
425
  `--dry-run` resolves configuration and validates local paths and transfer direction without connecting to the remote server or modifying local/remote files.
339
426
 
340
427
  ```text
341
428
  Dry run: upload
342
429
  Source: ./dist
343
- Destination: deploy@example.com:/var/www/example
430
+ Destination: your-username@your-host:/var/www/example
344
431
  Recursive: yes
345
432
  Overwrite: yes
346
433
  ```
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) {