syncodemayo 1.0.9 → 2.0.0
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 +81 -0
- package/README.md +91 -25
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +17 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Args.d.ts +21 -0
- package/dist/lib/Args.js +99 -0
- package/dist/lib/Args.js.map +1 -0
- package/{lib → dist/lib}/Cli.d.ts +1 -1
- package/dist/lib/Cli.js +104 -0
- package/dist/lib/Cli.js.map +1 -0
- package/{lib → dist/lib}/Client.d.ts +4 -4
- package/dist/lib/Client.js +73 -0
- package/dist/lib/Client.js.map +1 -0
- package/dist/lib/Config.d.ts +48 -0
- package/dist/lib/Config.js +92 -0
- package/dist/lib/Config.js.map +1 -0
- package/dist/lib/ConfigSchema.d.ts +3 -0
- package/dist/lib/ConfigSchema.js +132 -0
- package/dist/lib/ConfigSchema.js.map +1 -0
- package/dist/lib/Connection.d.ts +41 -0
- package/dist/lib/Connection.js +176 -0
- package/dist/lib/Connection.js.map +1 -0
- package/dist/lib/Filelist.d.ts +42 -0
- package/dist/lib/Filelist.js +156 -0
- package/dist/lib/Filelist.js.map +1 -0
- package/{lib → dist/lib}/Log.d.ts +1 -1
- package/{lib → dist/lib}/Log.js +9 -16
- package/dist/lib/Log.js.map +1 -0
- package/dist/lib/Prompt.d.ts +15 -0
- package/dist/lib/Prompt.js +31 -0
- package/dist/lib/Prompt.js.map +1 -0
- package/dist/lib/RemoteConnection.d.ts +37 -0
- package/dist/lib/RemoteConnection.js +2 -0
- package/dist/lib/RemoteConnection.js.map +1 -0
- package/dist/lib/Sync.d.ts +18 -0
- package/dist/lib/Sync.js +127 -0
- package/dist/lib/Sync.js.map +1 -0
- package/dist/lib/TargetServer.d.ts +31 -0
- package/dist/lib/TargetServer.js +76 -0
- package/dist/lib/TargetServer.js.map +1 -0
- package/dist/lib/index.d.ts +8 -0
- package/dist/lib/index.js +9 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/vendor/strip-json-comments.d.ts +23 -0
- package/dist/vendor/strip-json-comments.js +126 -0
- package/dist/vendor/strip-json-comments.js.map +1 -0
- package/package.json +24 -26
- package/.npmignore +0 -9
- package/cli.d.ts +0 -0
- package/cli.js +0 -102
- package/index.d.ts +0 -1
- package/index.js +0 -7
- package/lib/Cli.js +0 -130
- package/lib/Client.js +0 -85
- package/lib/Config.d.ts +0 -32
- package/lib/Config.js +0 -114
- package/lib/ConfigSchema.d.ts +0 -7
- package/lib/ConfigSchema.js +0 -65
- package/lib/Connection.d.ts +0 -14
- package/lib/Connection.js +0 -112
- package/lib/Filelist.d.ts +0 -11
- package/lib/Filelist.js +0 -59
- package/lib/Sync.d.ts +0 -18
- package/lib/Sync.js +0 -140
- package/lib/TargetServer.d.ts +0 -22
- package/lib/TargetServer.js +0 -74
- package/lib/index.d.ts +0 -7
- package/lib/index.js +0 -13
- package/missing-types.d.ts +0 -14
- package/tsconfig.json +0 -48
- package/yarn.lock +0 -975
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
A modernized SyncoDeMayo: ESM, Node.js built-ins in place of most dependencies, FTPS support,
|
|
6
|
+
and fixes for v1 bugs. Some behavior changes; read the migration notes below before upgrading.
|
|
7
|
+
|
|
8
|
+
### Migration from 1.x
|
|
9
|
+
|
|
10
|
+
**Before your first 2.0 sync, run `syncodemayo diff [target]`** and read the list of changes.
|
|
11
|
+
2.0 now syncs dotfiles and builds remote paths differently (see below), so the first sync may
|
|
12
|
+
upload files v1 never did. This matters most with `deleteRemoteFiles: true`: check that nothing
|
|
13
|
+
you want kept on the server is listed for deletion, and that nothing private (such as `.env`) is
|
|
14
|
+
listed for upload.
|
|
15
|
+
|
|
16
|
+
#### Requirements and packaging
|
|
17
|
+
|
|
18
|
+
- Node.js 22.17 or later is required (`engines.node` is `>=22.17`).
|
|
19
|
+
- The package is ESM. The CLI is `dist/cli.js`; the library entry is `dist/index.js`, with
|
|
20
|
+
types in `dist/index.d.ts`. CommonJS callers can still `require()` the library on Node.js
|
|
21
|
+
22.12 and later.
|
|
22
|
+
- Runtime dependencies drop from ten to one (`basic-ftp`).
|
|
23
|
+
- The library no longer exports the joi schemas `localSchema`, `targetSchema`, and
|
|
24
|
+
`configSchema`.
|
|
25
|
+
|
|
26
|
+
#### CLI
|
|
27
|
+
|
|
28
|
+
- An unknown command, extra positional arguments, and `-f` on any command other than `sync`
|
|
29
|
+
now exit 1 with an error. v1 printed the help or ignored them.
|
|
30
|
+
- `-c` now requires a value (`-c <file>`).
|
|
31
|
+
- The help text is reworded but lists the same commands and options.
|
|
32
|
+
- `--version` / `-V` prints the real package version.
|
|
33
|
+
|
|
34
|
+
#### Config
|
|
35
|
+
|
|
36
|
+
- Config validation errors keep the `Config Schema Error:` prefix, but the detail after it is
|
|
37
|
+
new.
|
|
38
|
+
- `local`, `targets`, or `exclude` given as a JSON *string* is now rejected.
|
|
39
|
+
- `.syncodemayo.json` and `sync-config.json` are now found when `syncodemayo.json` is absent.
|
|
40
|
+
- With no config, `init` creates `syncodemayo.json`, and `verify` / `ls` report that the folder
|
|
41
|
+
is not configured instead of throwing "Local config not found."
|
|
42
|
+
- Configs created by `init` exclude `**/.env*` by default. Existing configs are not changed:
|
|
43
|
+
if you have a `.env` under `local.path`, add `"**/.env*"` to your `exclude` list yourself.
|
|
44
|
+
|
|
45
|
+
#### File list
|
|
46
|
+
|
|
47
|
+
- Dotfiles and files inside dot-directories (such as `.htaccess` or `.well-known/`) are now
|
|
48
|
+
synced. v1 skipped them.
|
|
49
|
+
- `**/.git*` excludes `.gitignore` but not the files inside a `.git/` directory. Add
|
|
50
|
+
`**/.git/**` to keep one off the server.
|
|
51
|
+
- On macOS and Windows, `files` and `exclude` patterns match case-insensitively.
|
|
52
|
+
- Exclude patterns that start with `!` are matched literally, not as negations.
|
|
53
|
+
- `*.md` and `**/*.md` no longer match a file named exactly `.md`.
|
|
54
|
+
- Symlinked directories are walked for every `files` pattern, the way v1's default `**/**`
|
|
55
|
+
walked them: in full, without entering a symlinked directory inside one. Dangling symlinks
|
|
56
|
+
and symlink loops are skipped.
|
|
57
|
+
|
|
58
|
+
#### Remote and sync
|
|
59
|
+
|
|
60
|
+
- `init` no longer creates a missing remote `path`. It fails and tells you to create the
|
|
61
|
+
folder. `sync` still creates subfolders under the target path.
|
|
62
|
+
- A missing or unreadable remote file list is now a normal error (still exit code 1) instead
|
|
63
|
+
of a `process.exit` from inside the library.
|
|
64
|
+
- Remote paths are correct when `local.path` has a trailing or repeated slash, or
|
|
65
|
+
`target.path` has a trailing slash. v1 glued names together (`www` + `a.html` →
|
|
66
|
+
`wwwa.html`) or doubled slashes.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- FTPS support through two new target fields:
|
|
71
|
+
- `secure`: `false` (default, plain FTP), `true` (explicit FTPS, `AUTH TLS`), or
|
|
72
|
+
`"implicit"` (implicit FTPS, default port 990).
|
|
73
|
+
- `rejectUnauthorized`: `true` (default) requires a trusted server certificate; `false`
|
|
74
|
+
accepts a self-signed one and prints a warning naming the target on every connection.
|
|
75
|
+
|
|
76
|
+
See the FTPS section of the [README](README.md#ftps-ftp-over-tls).
|
|
77
|
+
|
|
78
|
+
### Unchanged
|
|
79
|
+
|
|
80
|
+
- A remote `.synco-filelist` written by v1 still diffs correctly: 2.0 uses the same keys and
|
|
81
|
+
CRCs, so upgrading does not re-upload everything.
|
package/README.md
CHANGED
|
@@ -7,6 +7,11 @@ It's pretty simple. SyncoDeMayo caches a list of known files, and their CRCs, on
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
Requirements: Node.js 22.17 or later.
|
|
11
|
+
|
|
12
|
+
Upgrading from 1.x? Version 2.0 changes some behavior. Read the migration notes in
|
|
13
|
+
[CHANGELOG.md](CHANGELOG.md) before your first 2.0 sync.
|
|
14
|
+
|
|
10
15
|
Install globally:
|
|
11
16
|
```
|
|
12
17
|
$ npm install -g syncodemayo
|
|
@@ -21,7 +26,7 @@ Suggested usage as NPM script:
|
|
|
21
26
|
```js
|
|
22
27
|
{
|
|
23
28
|
// ... OTHER NPM FIELDS ...
|
|
24
|
-
"
|
|
29
|
+
"scripts": {
|
|
25
30
|
"sync": "syncodemayo sync staging",
|
|
26
31
|
"sync:production": "syncodemayo sync production"
|
|
27
32
|
}
|
|
@@ -31,25 +36,26 @@ Suggested usage as NPM script:
|
|
|
31
36
|
---
|
|
32
37
|
|
|
33
38
|
```
|
|
34
|
-
$ syncodemayo
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
$ syncodemayo --help
|
|
40
|
+
Usage: syncodemayo [options] [command]
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
-c, --config <file> Specify local config <file>
|
|
44
|
+
-v, --verbose Verbose logging
|
|
45
|
+
-q, --quiet Minimal logging
|
|
46
|
+
-V, --version Output the version number
|
|
47
|
+
-h, --help Output usage information
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
init [target] Configure local folder and/or server to sync
|
|
51
|
+
changes|diff [target] Perform sync dry run and display the changes
|
|
52
|
+
verify [target] Verify server is configured
|
|
53
|
+
sync [options] [target]
|
|
54
|
+
Perform sync to server
|
|
55
|
+
-f, --force Don't prompt before uploading.
|
|
56
|
+
ls List defined targets in config
|
|
57
|
+
|
|
58
|
+
With no command, shows this help.
|
|
53
59
|
```
|
|
54
60
|
|
|
55
61
|
|
|
@@ -59,8 +65,8 @@ Example `syncodemayo.json` config:
|
|
|
59
65
|
{
|
|
60
66
|
"local": {
|
|
61
67
|
"path": "public", // Required, local path to sync
|
|
62
|
-
"files": "**/**", // Optional, default='
|
|
63
|
-
"exclude": ["**/*.map", "**/.DS_Store", "**/.git*"], // Optional, default=[], glob
|
|
68
|
+
"files": "**/**", // Optional, default='**/**', file glob pattern
|
|
69
|
+
"exclude": ["**/*.map", "**/.DS_Store", "**/.git*", "**/.env*"], // Optional, default=[] (init writes this list), glob patterns to ignore
|
|
64
70
|
"defaultTarget": "staging", // Optional, default='staging', target to use if not specified on cmd line
|
|
65
71
|
"deleteRemoteFiles": true // Optional, default=false - for now...
|
|
66
72
|
},
|
|
@@ -71,7 +77,9 @@ Example `syncodemayo.json` config:
|
|
|
71
77
|
"path": "MyApp/www/stage", // Required
|
|
72
78
|
"user": "USERNAME", // Required
|
|
73
79
|
"pass": "PASSWORD", // Optional, NOT RECOMMENDED! See note below for better way...
|
|
74
|
-
"port": 21, // Optional, default=21
|
|
80
|
+
"port": 21, // Optional, default=21 (990 when secure is "implicit")
|
|
81
|
+
"secure": false, // Optional, default=false. FTPS: false, true (explicit) or "implicit". See below.
|
|
82
|
+
"rejectUnauthorized": true, // Optional, default=true. false accepts self-signed certificates. See below.
|
|
75
83
|
"cache": ".synco-filelist", // Optional, default='.synco-filelist'
|
|
76
84
|
"prompt": false, // Optional, default=true, false will upload w/o confirming
|
|
77
85
|
"enabled": false // Optional, default=true
|
|
@@ -84,11 +92,32 @@ Example `syncodemayo.json` config:
|
|
|
84
92
|
"prompt": "This is for PRODUCTION! Do you really mean it?"
|
|
85
93
|
}
|
|
86
94
|
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
Note: Yes, comments are allowed in config files.
|
|
98
|
+
SyncoDeMayo will strip them before parsing the JSON.
|
|
99
|
+
*/
|
|
87
100
|
}
|
|
88
101
|
```
|
|
89
102
|
|
|
103
|
+
### Dotfiles
|
|
104
|
+
|
|
105
|
+
Dotfiles and dot-directories sync like any other file: `.htaccess`, for example, is uploaded.
|
|
106
|
+
(1.x skipped them.) Exclude anything that must not reach the server, such as `**/.env*` for
|
|
107
|
+
environment files and `**/.git/**` for a git directory. Note that `**/.git*` excludes
|
|
108
|
+
`.gitignore` but not the files inside a `.git/` directory.
|
|
109
|
+
|
|
110
|
+
On macOS and Windows, `files` and `exclude` patterns match case-insensitively.
|
|
111
|
+
|
|
112
|
+
### Passwords and `.env`
|
|
113
|
+
|
|
90
114
|
You can put the FTP password in the config, but if you're adding it to source control I wouldn't. Instead, add the password to an `.env` file that you set to ignored in your SCM.
|
|
91
115
|
|
|
116
|
+
SyncoDeMayo reads `.env` from the current working directory (where you run `syncodemayo`).
|
|
117
|
+
Variables already set in the environment win over the ones in `.env`. When a target has no
|
|
118
|
+
`pass`, its password is looked up as `<TARGET NAME UPPERCASED>_PWD`: `STAGING_PWD` for the
|
|
119
|
+
`staging` target.
|
|
120
|
+
|
|
92
121
|
Example `.env` for above config:
|
|
93
122
|
|
|
94
123
|
```bash
|
|
@@ -96,6 +125,43 @@ STAGING_PWD=ftpPasswordHere
|
|
|
96
125
|
PRODUCTION_PWD="Other password here"
|
|
97
126
|
```
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
**Warning:** a `.env` inside `local.path` is a dotfile like any other, so it would be uploaded
|
|
129
|
+
unless excluded. Configs created by `init` exclude `**/.env*`; if your config predates 2.0, add
|
|
130
|
+
`"**/.env*"` to its `exclude` list.
|
|
131
|
+
|
|
132
|
+
### FTPS (FTP over TLS)
|
|
133
|
+
|
|
134
|
+
If your host requires FTPS, set `secure` on the target:
|
|
135
|
+
|
|
136
|
+
| `secure` | Connection | Default `port` |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `false` (default) | Plain FTP | 21 |
|
|
139
|
+
| `true` | Explicit FTPS: connects in plain text, then upgrades with `AUTH TLS` | 21 |
|
|
140
|
+
| `"implicit"` | Implicit FTPS: TLS from the start | 990 |
|
|
141
|
+
|
|
142
|
+
An explicit `port` always wins over these defaults. `secure` takes exactly `true`, `false` or
|
|
143
|
+
`"implicit"` (not the strings `"true"`/`"false"`).
|
|
144
|
+
|
|
145
|
+
`rejectUnauthorized` (`true` or `false`, default `true`) controls whether the server's TLS
|
|
146
|
+
certificate must be trusted. With the default, a self-signed, expired or mismatched certificate
|
|
147
|
+
stops the command with an error that names the host. If you trust the server anyway, set
|
|
148
|
+
`"rejectUnauthorized": false` on the target: the connection then accepts the certificate, and
|
|
149
|
+
SyncoDeMayo prints a warning naming the target every time it makes such a connection (even
|
|
150
|
+
with `-q`).
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
"targets": {
|
|
154
|
+
"production": {
|
|
155
|
+
"host": "ftp.myapp.com",
|
|
156
|
+
"path": "MyApp/www",
|
|
157
|
+
"user": "USERNAME",
|
|
158
|
+
"secure": true,
|
|
159
|
+
"rejectUnauthorized": false // only for a server you trust with a self-signed certificate
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Changelog
|
|
100
165
|
|
|
101
|
-
|
|
166
|
+
See [CHANGELOG.md](CHANGELOG.md) for what changed in each release, including the migration
|
|
167
|
+
notes for upgrading from 1.x.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Must be the first import: loads .env before any other module reads process.env.
|
|
3
|
+
import './env.js';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import * as CLI from './lib/Cli.js';
|
|
6
|
+
import * as Log from './lib/Log.js';
|
|
7
|
+
import { CliArgsError, HELP, parseCliArgs } from './lib/Args.js';
|
|
8
|
+
let args;
|
|
9
|
+
try {
|
|
10
|
+
args = parseCliArgs(process.argv.slice(2));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (!(err instanceof CliArgsError))
|
|
14
|
+
throw err;
|
|
15
|
+
process.stderr.write(`error: ${err.message}\n\nRun 'syncodemayo --help' for usage.\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
CLI.setFlags({ logLevel: args.logLevel });
|
|
19
|
+
const { config, target } = args;
|
|
20
|
+
switch (args.command) {
|
|
21
|
+
case "changes":
|
|
22
|
+
Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\n`);
|
|
23
|
+
CLI.changes(config, target).then(showDone).catch(showError);
|
|
24
|
+
break;
|
|
25
|
+
case "check":
|
|
26
|
+
Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\n`);
|
|
27
|
+
CLI.check(config, target).then(showDone).catch(showError);
|
|
28
|
+
break;
|
|
29
|
+
case "init":
|
|
30
|
+
Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\n`);
|
|
31
|
+
CLI.init(config, target).then(showDone).catch(showError);
|
|
32
|
+
break;
|
|
33
|
+
case "ls":
|
|
34
|
+
Log.debug(`Performing '${args.command}'...\n`);
|
|
35
|
+
CLI.ls(config).then(showDone).catch(showError);
|
|
36
|
+
break;
|
|
37
|
+
case "sync":
|
|
38
|
+
if (args.force) {
|
|
39
|
+
Log.debug(`Performing (forced) 'sync' on target: ${target || '(default)'}\n...`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}\n...`);
|
|
43
|
+
}
|
|
44
|
+
CLI.sync(config, target, args.force).then(showDone).catch(showError);
|
|
45
|
+
break;
|
|
46
|
+
case "version":
|
|
47
|
+
process.stdout.write(readPackageVersion() + "\n");
|
|
48
|
+
break;
|
|
49
|
+
case "help":
|
|
50
|
+
default:
|
|
51
|
+
process.stdout.write(HELP);
|
|
52
|
+
}
|
|
53
|
+
// The version of this package, read from its package.json (one level above dist/).
|
|
54
|
+
function readPackageVersion() {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
56
|
+
return String(pkg.version);
|
|
57
|
+
}
|
|
58
|
+
function showDone() {
|
|
59
|
+
Log.log("\nDone.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
function showError(err) {
|
|
63
|
+
console.error("\n(!)", err.message, "\n\n", err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"./","sources":["cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\n// Must be the first import: loads .env before any other module reads process.env.\nimport './env.js'\n\nimport { readFileSync } from 'node:fs'\nimport * as CLI from './lib/Cli.js'\nimport * as Log from './lib/Log.js'\nimport { CliArgs, CliArgsError, HELP, parseCliArgs } from './lib/Args.js'\n\nlet args: CliArgs\ntry {\n args = parseCliArgs(process.argv.slice(2))\n}\ncatch (err) {\n if (!(err instanceof CliArgsError)) throw err\n process.stderr.write(`error: ${err.message}\\n\\nRun 'syncodemayo --help' for usage.\\n`)\n process.exit(1)\n}\n\nCLI.setFlags({ logLevel: args.logLevel })\n\nconst { config, target } = args\n\nswitch (args.command) {\n\n case \"changes\":\n Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\\n`)\n CLI.changes(config, target).then(showDone).catch(showError)\n break\n\n case \"check\":\n Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\\n`)\n CLI.check(config, target).then(showDone).catch(showError)\n break\n\n case \"init\":\n Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}...\\n`)\n CLI.init(config, target).then(showDone).catch(showError)\n break\n\n case \"ls\":\n Log.debug(`Performing '${args.command}'...\\n`)\n CLI.ls(config).then(showDone).catch(showError)\n break\n\n case \"sync\":\n if (args.force) {\n Log.debug(`Performing (forced) 'sync' on target: ${target || '(default)'}\\n...`)\n }\n else {\n Log.debug(`Performing '${args.command}' on target: ${target || '(default)'}\\n...`)\n }\n CLI.sync(config, target, args.force).then(showDone).catch(showError)\n break\n\n case \"version\":\n process.stdout.write(readPackageVersion() + \"\\n\")\n break\n\n case \"help\":\n default:\n process.stdout.write(HELP)\n}\n\n// The version of this package, read from its package.json (one level above dist/).\nfunction readPackageVersion(): string {\n const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))\n return String(pkg.version)\n}\n\nfunction showDone() {\n Log.log(\"\\nDone.\")\n process.exit(0)\n}\n\nfunction showError(err: any) {\n console.error(\"\\n(!)\", err.message, \"\\n\\n\", err)\n process.exit(1)\n}\n"]}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadDotEnv(path?: string): boolean;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Loads `.env` from the working directory into process.env, as v1 did with dotenv.
|
|
2
|
+
// cli.ts imports this module first, so the variables are set before any other module
|
|
3
|
+
// reads process.env. Variables already in the environment are not overridden.
|
|
4
|
+
// Only a missing `.env` is ignored; any other read or parse error surfaces.
|
|
5
|
+
export function loadDotEnv(path = '.env') {
|
|
6
|
+
try {
|
|
7
|
+
process.loadEnvFile(path);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
if (err?.code === 'ENOENT')
|
|
12
|
+
return false;
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
loadDotEnv();
|
|
17
|
+
//# sourceMappingURL=env.js.map
|
package/dist/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"./","sources":["env.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,qFAAqF;AACrF,8EAA8E;AAC9E,4EAA4E;AAE5E,MAAM,UAAU,UAAU,CAAC,IAAI,GAAG,MAAM;IACtC,IAAI,CAAC;QACH,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,GAAG,EAAE,CAAC;QACX,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAA;QACnE,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED,UAAU,EAAE,CAAA","sourcesContent":["// Loads `.env` from the working directory into process.env, as v1 did with dotenv.\n// cli.ts imports this module first, so the variables are set before any other module\n// reads process.env. Variables already in the environment are not overridden.\n// Only a missing `.env` is ignored; any other read or parse error surfaces.\n\nexport function loadDotEnv(path = '.env'): boolean {\n try {\n process.loadEnvFile(path)\n return true\n }\n catch (err) {\n if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return false\n throw err\n }\n}\n\nloadDotEnv()\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"./","sources":["index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA","sourcesContent":["export * from './lib/index.js'"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LogLevel } from './Log.js';
|
|
2
|
+
/** What the CLI was asked to do. `check` is the `verify` command, as in v1. */
|
|
3
|
+
export type CliCommand = 'init' | 'changes' | 'check' | 'sync' | 'ls' | 'help' | 'version';
|
|
4
|
+
export interface CliArgs {
|
|
5
|
+
command: CliCommand;
|
|
6
|
+
target?: string;
|
|
7
|
+
config?: string;
|
|
8
|
+
logLevel: LogLevel;
|
|
9
|
+
/** `sync -f/--force`: don't prompt before uploading. */
|
|
10
|
+
force: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Thrown for a command line the CLI can't run (unknown option or command, extra arguments). */
|
|
13
|
+
export declare class CliArgsError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare const HELP = "Usage: syncodemayo [options] [command]\n\nOptions:\n -c, --config <file> Specify local config <file>\n -v, --verbose Verbose logging\n -q, --quiet Minimal logging\n -V, --version Output the version number\n -h, --help Output usage information\n\nCommands:\n init [target] Configure local folder and/or server to sync\n changes|diff [target] Perform sync dry run and display the changes\n verify [target] Verify server is configured\n sync [options] [target]\n Perform sync to server\n -f, --force Don't prompt before uploading.\n ls List defined targets in config\n\nWith no command, shows this help.\n";
|
|
17
|
+
/**
|
|
18
|
+
* Parses the CLI arguments (without the node and script paths) into what to run.
|
|
19
|
+
* Pure: it reads nothing and prints nothing. Throws CliArgsError for a command line it can't run.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseCliArgs(argv: string[]): CliArgs;
|
package/dist/lib/Args.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { LogLevel } from './Log.js';
|
|
3
|
+
/** Thrown for a command line the CLI can't run (unknown option or command, extra arguments). */
|
|
4
|
+
export class CliArgsError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'CliArgsError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// Command name (and alias) -> what it runs, and whether it takes a [target].
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
init: { command: 'init', takesTarget: true },
|
|
13
|
+
changes: { command: 'changes', takesTarget: true },
|
|
14
|
+
diff: { command: 'changes', takesTarget: true },
|
|
15
|
+
verify: { command: 'check', takesTarget: true },
|
|
16
|
+
sync: { command: 'sync', takesTarget: true },
|
|
17
|
+
ls: { command: 'ls', takesTarget: false },
|
|
18
|
+
};
|
|
19
|
+
export const HELP = `Usage: syncodemayo [options] [command]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-c, --config <file> Specify local config <file>
|
|
23
|
+
-v, --verbose Verbose logging
|
|
24
|
+
-q, --quiet Minimal logging
|
|
25
|
+
-V, --version Output the version number
|
|
26
|
+
-h, --help Output usage information
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
init [target] Configure local folder and/or server to sync
|
|
30
|
+
changes|diff [target] Perform sync dry run and display the changes
|
|
31
|
+
verify [target] Verify server is configured
|
|
32
|
+
sync [options] [target]
|
|
33
|
+
Perform sync to server
|
|
34
|
+
-f, --force Don't prompt before uploading.
|
|
35
|
+
ls List defined targets in config
|
|
36
|
+
|
|
37
|
+
With no command, shows this help.
|
|
38
|
+
`;
|
|
39
|
+
/**
|
|
40
|
+
* Parses the CLI arguments (without the node and script paths) into what to run.
|
|
41
|
+
* Pure: it reads nothing and prints nothing. Throws CliArgsError for a command line it can't run.
|
|
42
|
+
*/
|
|
43
|
+
export function parseCliArgs(argv) {
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = parseArgs({
|
|
47
|
+
args: argv,
|
|
48
|
+
allowPositionals: true,
|
|
49
|
+
strict: true,
|
|
50
|
+
options: {
|
|
51
|
+
config: { type: 'string', short: 'c' },
|
|
52
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
53
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
54
|
+
version: { type: 'boolean', short: 'V' },
|
|
55
|
+
help: { type: 'boolean', short: 'h' },
|
|
56
|
+
force: { type: 'boolean', short: 'f' },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
throw new CliArgsError(err.message);
|
|
62
|
+
}
|
|
63
|
+
const { values, positionals } = parsed;
|
|
64
|
+
const force = values.force === true;
|
|
65
|
+
const logLevel = values.verbose
|
|
66
|
+
? LogLevel.Debug
|
|
67
|
+
: values.quiet
|
|
68
|
+
? LogLevel.Info
|
|
69
|
+
: LogLevel.Default;
|
|
70
|
+
// Only the keys that are set, so a result compares equal to a plain literal.
|
|
71
|
+
const result = (command, target) => {
|
|
72
|
+
const args = { command, logLevel, force };
|
|
73
|
+
if (values.config !== undefined)
|
|
74
|
+
args.config = values.config;
|
|
75
|
+
if (target)
|
|
76
|
+
args.target = target;
|
|
77
|
+
return args;
|
|
78
|
+
};
|
|
79
|
+
if (values.help)
|
|
80
|
+
return result('help');
|
|
81
|
+
if (values.version)
|
|
82
|
+
return result('version');
|
|
83
|
+
if (positionals.length === 0) {
|
|
84
|
+
if (force)
|
|
85
|
+
throw new CliArgsError("Unknown option '-f, --force': it applies only to the sync command");
|
|
86
|
+
return result('help');
|
|
87
|
+
}
|
|
88
|
+
const [name, ...rest] = positionals;
|
|
89
|
+
const spec = Object.hasOwn(COMMANDS, name) ? COMMANDS[name] : undefined;
|
|
90
|
+
if (!spec)
|
|
91
|
+
throw new CliArgsError(`Unknown command '${name}'`);
|
|
92
|
+
if (force && spec.command !== 'sync')
|
|
93
|
+
throw new CliArgsError(`Unknown option '-f, --force' for the ${name} command: it applies only to sync`);
|
|
94
|
+
const maxArgs = spec.takesTarget ? 1 : 0;
|
|
95
|
+
if (rest.length > maxArgs)
|
|
96
|
+
throw new CliArgsError(`Too many arguments for the ${name} command: ${rest.join(' ')}`);
|
|
97
|
+
return result(spec.command, rest[0]);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=Args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Args.js","sourceRoot":"./","sources":["lib/Args.ts"],"names":[],"mappings":"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","sourcesContent":["import { parseArgs } from 'node:util'\nimport { LogLevel } from './Log.js'\n\n/** What the CLI was asked to do. `check` is the `verify` command, as in v1. */\nexport type CliCommand = 'init' | 'changes' | 'check' | 'sync' | 'ls' | 'help' | 'version'\n\nexport interface CliArgs {\n command: CliCommand\n target?: string\n config?: string\n logLevel: LogLevel\n /** `sync -f/--force`: don't prompt before uploading. */\n force: boolean\n}\n\n/** Thrown for a command line the CLI can't run (unknown option or command, extra arguments). */\nexport class CliArgsError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliArgsError'\n }\n}\n\n// Command name (and alias) -> what it runs, and whether it takes a [target].\nconst COMMANDS: { [name: string]: { command: CliCommand, takesTarget: boolean } } = {\n init: { command: 'init', takesTarget: true },\n changes: { command: 'changes', takesTarget: true },\n diff: { command: 'changes', takesTarget: true },\n verify: { command: 'check', takesTarget: true },\n sync: { command: 'sync', takesTarget: true },\n ls: { command: 'ls', takesTarget: false },\n}\n\nexport const HELP = `Usage: syncodemayo [options] [command]\n\nOptions:\n -c, --config <file> Specify local config <file>\n -v, --verbose Verbose logging\n -q, --quiet Minimal logging\n -V, --version Output the version number\n -h, --help Output usage information\n\nCommands:\n init [target] Configure local folder and/or server to sync\n changes|diff [target] Perform sync dry run and display the changes\n verify [target] Verify server is configured\n sync [options] [target]\n Perform sync to server\n -f, --force Don't prompt before uploading.\n ls List defined targets in config\n\nWith no command, shows this help.\n`\n\n/**\n * Parses the CLI arguments (without the node and script paths) into what to run.\n * Pure: it reads nothing and prints nothing. Throws CliArgsError for a command line it can't run.\n */\nexport function parseCliArgs(argv: string[]): CliArgs {\n let parsed\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n strict: true,\n options: {\n config: { type: 'string', short: 'c' },\n verbose: { type: 'boolean', short: 'v' },\n quiet: { type: 'boolean', short: 'q' },\n version: { type: 'boolean', short: 'V' },\n help: { type: 'boolean', short: 'h' },\n force: { type: 'boolean', short: 'f' },\n },\n })\n }\n catch (err) {\n throw new CliArgsError((err as Error).message)\n }\n\n const { values, positionals } = parsed\n const force = values.force === true\n const logLevel = values.verbose\n ? LogLevel.Debug\n : values.quiet\n ? LogLevel.Info\n : LogLevel.Default\n // Only the keys that are set, so a result compares equal to a plain literal.\n const result = (command: CliCommand, target?: string): CliArgs => {\n const args: CliArgs = { command, logLevel, force }\n if (values.config !== undefined) args.config = values.config\n if (target) args.target = target\n return args\n }\n\n if (values.help) return result('help')\n if (values.version) return result('version')\n if (positionals.length === 0) {\n if (force) throw new CliArgsError(\"Unknown option '-f, --force': it applies only to the sync command\")\n return result('help')\n }\n\n const [name, ...rest] = positionals\n const spec = Object.hasOwn(COMMANDS, name) ? COMMANDS[name] : undefined\n if (!spec) throw new CliArgsError(`Unknown command '${name}'`)\n\n if (force && spec.command !== 'sync')\n throw new CliArgsError(`Unknown option '-f, --force' for the ${name} command: it applies only to sync`)\n\n const maxArgs = spec.takesTarget ? 1 : 0\n if (rest.length > maxArgs)\n throw new CliArgsError(`Too many arguments for the ${name} command: ${rest.join(' ')}`)\n\n return result(spec.command, rest[0])\n}\n"]}
|
package/dist/lib/Cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as Log from './Log.js';
|
|
2
|
+
import { Client } from './Client.js';
|
|
3
|
+
export function setFlags(flags) {
|
|
4
|
+
Log.setLogLevel(flags.logLevel);
|
|
5
|
+
}
|
|
6
|
+
export async function changes(configPath, target) {
|
|
7
|
+
const client = await Client.create(configPath);
|
|
8
|
+
const changeset = await client.getTargetServer(target).sync(true);
|
|
9
|
+
if (changeset == null)
|
|
10
|
+
throw new Error("Change detection failed.");
|
|
11
|
+
const added = changeset.added.length;
|
|
12
|
+
const changed = changeset.changed.length;
|
|
13
|
+
const removed = changeset.removed.length;
|
|
14
|
+
if (added + changed + removed === 0) {
|
|
15
|
+
Log.log("\nNo changes detected.");
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (added > 0) {
|
|
19
|
+
Log.info("\nNew Local Files: (%s)", added);
|
|
20
|
+
changeset.added.forEach(f => Log.log(" -", f));
|
|
21
|
+
}
|
|
22
|
+
if (changed > 0) {
|
|
23
|
+
Log.info("\nChanged Local Files: (%s)", changeset.changed.length);
|
|
24
|
+
changeset.changed.forEach(f => Log.log(" -", f));
|
|
25
|
+
}
|
|
26
|
+
if (removed > 0) {
|
|
27
|
+
Log.info(`\nRemoved Local Files${client.config.local.deleteRemoteFiles ? '' : ' (disabled)'}: (%s)`, changeset.removed.length);
|
|
28
|
+
changeset.removed.forEach(f => Log.log(" -", f));
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export async function check(configPath, target) {
|
|
33
|
+
const client = await Client.create(configPath);
|
|
34
|
+
if (client.hasError) {
|
|
35
|
+
throw client.loadError || client.parseError;
|
|
36
|
+
}
|
|
37
|
+
if (!client.hasConfig && !client.hasError) {
|
|
38
|
+
Log.urgent("This directory isn't configured for SyncoDeMayo!");
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const targetServer = client.getTargetServer(target);
|
|
42
|
+
const isServerConfigured = await targetServer.verify();
|
|
43
|
+
if (isServerConfigured) {
|
|
44
|
+
Log.info("Target", JSON.stringify(targetServer.name), "is ready for syncing.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
Log.urgent("Target", JSON.stringify(targetServer.name), "isn't configured, run init.");
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
export async function init(configPath, target) {
|
|
53
|
+
const client = await Client.create(configPath);
|
|
54
|
+
if (!client.hasConfig && client.hasError) {
|
|
55
|
+
throw client.loadError || client.parseError;
|
|
56
|
+
}
|
|
57
|
+
if (!client.hasConfig) {
|
|
58
|
+
await client.createConfig(configPath);
|
|
59
|
+
Log.info("Created local config at", configPath);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const targetServer = client.getTargetServer(target);
|
|
63
|
+
const isServerConfigured = await targetServer.verify();
|
|
64
|
+
if (!isServerConfigured) {
|
|
65
|
+
await targetServer.prepare();
|
|
66
|
+
Log.info("Created", targetServer.config.cache, "on server target", targetServer.config._name);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
Log.info("Everything seems to be configured correctly.");
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
export async function ls(configPath) {
|
|
73
|
+
const client = await Client.create(configPath);
|
|
74
|
+
const targets = await client.getTargetNames();
|
|
75
|
+
Log.urgent("Defined server targets:");
|
|
76
|
+
targets.forEach(name => {
|
|
77
|
+
const target = client.config.targets[name];
|
|
78
|
+
const bullet = client.config.local.defaultTarget === target._name
|
|
79
|
+
? " *"
|
|
80
|
+
: " -";
|
|
81
|
+
Log.urgent(bullet, name, `\t(${target.user}@${target.host}:${target.path})`);
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
export async function sync(configPath, target, forceConfirmation = false) {
|
|
86
|
+
const client = await Client.create(configPath);
|
|
87
|
+
if (!client.hasConfig) {
|
|
88
|
+
if (client.hasError)
|
|
89
|
+
throw client.loadError || client.parseError;
|
|
90
|
+
else
|
|
91
|
+
throw new Error("This directory isn't configured for SyncoDeMayo!");
|
|
92
|
+
}
|
|
93
|
+
const changes = await client.getTargetServer(target).sync(false, forceConfirmation);
|
|
94
|
+
if (changes != null) {
|
|
95
|
+
Log.info("\nSummary:");
|
|
96
|
+
Log.info(" %s files uploaded.", changes.added.length + changes.changed.length);
|
|
97
|
+
changes.added.forEach(path => Log.log(" +", path));
|
|
98
|
+
changes.changed.forEach(path => Log.log(" -", path));
|
|
99
|
+
Log.info(` %s files removed. ${client.config.local.deleteRemoteFiles ? '' : '(disabled)'}`, changes.removed.length);
|
|
100
|
+
changes.removed.forEach(path => Log.log(" x", path));
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=Cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cli.js","sourceRoot":"./","sources":["lib/Cli.ts"],"names":[],"mappings":"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","sourcesContent":["import * as Log from './Log.js'\nimport { Client } from './Client.js'\n\nexport interface CLIFlags {\n logLevel: Log.LogLevel\n}\n\nexport function setFlags(flags: CLIFlags) {\n Log.setLogLevel(flags.logLevel)\n}\n\nexport async function changes(configPath?: string, target?: string) {\n const client = await Client.create(configPath)\n const changeset = await client.getTargetServer(target).sync(true)\n if (changeset == null) throw new Error(\"Change detection failed.\")\n\n const added = changeset.added.length\n const changed = changeset.changed.length\n const removed = changeset.removed.length\n\n if (added + changed + removed === 0) {\n Log.log(\"\\nNo changes detected.\")\n return true\n }\n\n if (added > 0) {\n Log.info(\"\\nNew Local Files: (%s)\", added)\n changeset.added.forEach(f => Log.log(\" -\", f))\n }\n if (changed > 0) {\n Log.info(\"\\nChanged Local Files: (%s)\", changeset.changed.length)\n changeset.changed.forEach(f => Log.log(\" -\", f))\n }\n if (removed > 0) {\n Log.info(`\\nRemoved Local Files${client.config.local.deleteRemoteFiles ? '' : ' (disabled)'}: (%s)`, changeset.removed.length)\n changeset.removed.forEach(f => Log.log(\" -\", f))\n }\n\n return true\n}\n\nexport async function check(configPath?: string, target?: string) {\n const client = await Client.create(configPath)\n\n if (client.hasError) {\n throw client.loadError || client.parseError\n }\n\n if (!client.hasConfig && !client.hasError) {\n Log.urgent(\"This directory isn't configured for SyncoDeMayo!\")\n return false\n }\n\n const targetServer = client.getTargetServer(target)\n const isServerConfigured = await targetServer.verify()\n\n if (isServerConfigured) {\n Log.info(\"Target\", JSON.stringify(targetServer.name), \"is ready for syncing.\")\n }\n else {\n Log.urgent(\"Target\", JSON.stringify(targetServer.name), \"isn't configured, run init.\")\n return false\n }\n\n return true\n}\n\nexport async function init(configPath?: string, target?: string) {\n const client = await Client.create(configPath)\n\n if (!client.hasConfig && client.hasError) {\n throw client.loadError || client.parseError\n }\n\n if (!client.hasConfig) {\n await client.createConfig(configPath)\n Log.info(\"Created local config at\", configPath)\n return true\n }\n\n const targetServer = client.getTargetServer(target)\n const isServerConfigured = await targetServer.verify()\n\n if (!isServerConfigured) {\n await targetServer.prepare()\n Log.info(\"Created\", targetServer.config.cache, \"on server target\", targetServer.config._name)\n return true\n }\n\n Log.info(\"Everything seems to be configured correctly.\")\n return false\n}\n\nexport async function ls(configPath?: string) {\n const client = await Client.create(configPath)\n const targets = await client.getTargetNames()\n\n Log.urgent(\"Defined server targets:\")\n targets.forEach(name => {\n const target = client.config.targets[name]\n const bullet = client.config.local.defaultTarget === target._name\n ? \" *\"\n : \" -\"\n Log.urgent(bullet, name, `\\t(${target.user}@${target.host}:${target.path})`)\n })\n\n return true\n}\n\nexport async function sync(configPath?: string, target?: string, forceConfirmation: boolean = false) {\n const client = await Client.create(configPath)\n\n if (!client.hasConfig) {\n if (client.hasError) throw client.loadError || client.parseError\n else throw new Error(\"This directory isn't configured for SyncoDeMayo!\")\n }\n\n const changes = await client.getTargetServer(target).sync(false, forceConfirmation)\n\n if (changes != null) {\n Log.info(\"\\nSummary:\")\n\n Log.info(\" %s files uploaded.\", changes.added.length + changes.changed.length)\n changes.added.forEach(path => Log.log(\" +\", path))\n changes.changed.forEach(path => Log.log(\" -\", path))\n\n Log.info(` %s files removed. ${client.config.local.deleteRemoteFiles ? '' : '(disabled)'}`, changes.removed.length)\n changes.removed.forEach(path => Log.log(\" x\", path))\n }\n\n return true\n}\n"]}
|