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 +7 -2
- package/README.md +194 -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/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.0.14
|
|
4
4
|
|
|
5
|
-
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
109
|
+
### Upload Examples
|
|
55
110
|
|
|
56
111
|
```bash
|
|
57
112
|
scp-next upload ./dist /var/www/example \
|
|
58
|
-
--host
|
|
59
|
-
--username
|
|
113
|
+
--host your-host \
|
|
114
|
+
--username your-username \
|
|
60
115
|
--private-key-file ~/.ssh/id_ed25519 \
|
|
61
116
|
--recursive
|
|
62
117
|
```
|
|
63
118
|
|
|
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
|
|
119
|
+
### Download Examples
|
|
78
120
|
|
|
79
121
|
```bash
|
|
80
122
|
scp-next download /var/log/example.log ./logs/example.log \
|
|
81
|
-
--host
|
|
82
|
-
--username
|
|
123
|
+
--host your-host \
|
|
124
|
+
--username your-username \
|
|
83
125
|
--private-key-file ~/.ssh/id_ed25519
|
|
84
126
|
```
|
|
85
127
|
|
|
86
|
-
|
|
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
|
-
##
|
|
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": "
|
|
264
|
+
"host": "your-host",
|
|
132
265
|
"port": 22,
|
|
133
|
-
"username": "
|
|
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
|
-
|
|
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": "
|
|
298
|
+
"host": "your-host-a",
|
|
153
299
|
"port": 22,
|
|
154
|
-
"username": "
|
|
300
|
+
"username": "your-username-a",
|
|
155
301
|
"privateKeyFile": "~/.ssh/id_ed25519"
|
|
156
302
|
},
|
|
157
303
|
"staging": {
|
|
158
|
-
"host": "
|
|
304
|
+
"host": "your-host-b",
|
|
159
305
|
"port": 22,
|
|
160
|
-
"username": "
|
|
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
|
-
|
|
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": "
|
|
180
|
-
"username": "
|
|
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
|
-
|
|
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="
|
|
229
|
-
export SCP_NEXT_USERNAME="
|
|
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 = "
|
|
238
|
-
$env:SCP_NEXT_USERNAME = "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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) {
|