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 +143 -107
- package/dist/cli/index.js +31 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +6 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
101
|
+
### Upload Examples
|
|
55
102
|
|
|
56
103
|
```bash
|
|
57
104
|
scp-next upload ./dist /var/www/example \
|
|
58
|
-
--host
|
|
59
|
-
--username
|
|
105
|
+
--host your-host \
|
|
106
|
+
--username your-username \
|
|
60
107
|
--private-key-file ~/.ssh/id_ed25519 \
|
|
61
108
|
--recursive
|
|
62
109
|
```
|
|
63
110
|
|
|
64
|
-
|
|
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
|
|
82
|
-
--username
|
|
115
|
+
--host your-host \
|
|
116
|
+
--username your-username \
|
|
83
117
|
--private-key-file ~/.ssh/id_ed25519
|
|
84
118
|
```
|
|
85
119
|
|
|
86
|
-
|
|
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
|
-
##
|
|
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": "
|
|
226
|
+
"host": "your-host",
|
|
132
227
|
"port": 22,
|
|
133
|
-
"username": "
|
|
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
|
-
|
|
240
|
+
### Named Profiles
|
|
146
241
|
|
|
147
242
|
```json
|
|
148
243
|
{
|
|
149
244
|
"defaultProfile": "production",
|
|
150
245
|
"profiles": {
|
|
151
246
|
"production": {
|
|
152
|
-
"host": "
|
|
247
|
+
"host": "your-host-a",
|
|
153
248
|
"port": 22,
|
|
154
|
-
"username": "
|
|
249
|
+
"username": "your-username-a",
|
|
155
250
|
"privateKeyFile": "~/.ssh/id_ed25519"
|
|
156
251
|
},
|
|
157
252
|
"staging": {
|
|
158
|
-
"host": "
|
|
253
|
+
"host": "your-host-b",
|
|
159
254
|
"port": 22,
|
|
160
|
-
"username": "
|
|
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
|
-
|
|
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": "
|
|
180
|
-
"username": "
|
|
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
|
-
|
|
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="
|
|
229
|
-
export SCP_NEXT_USERNAME="
|
|
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 = "
|
|
238
|
-
$env:SCP_NEXT_USERNAME = "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
923
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
output.stderr.write(
|
|
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) {
|