island-bridge 1.1.0 → 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/README.md +120 -1
- package/bin/cli.js +209 -60
- package/lib/args.js +51 -2
- package/lib/backup.js +124 -0
- package/lib/config.js +41 -0
- package/lib/hooks.js +10 -3
- package/lib/init.js +110 -0
- package/lib/interactive.js +12 -6
- package/lib/progress.js +28 -17
- package/lib/reporter.js +187 -0
- package/lib/status.js +127 -0
- package/lib/summary.js +27 -0
- package/lib/sync.js +43 -11
- package/lib/watch.js +24 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,9 +21,28 @@ npx island-bridge push
|
|
|
21
21
|
- `rsync` installed on both local and remote machines
|
|
22
22
|
- SSH access to remote server (uses system `~/.ssh/config`)
|
|
23
23
|
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Create config interactively
|
|
28
|
+
island-bridge init
|
|
29
|
+
|
|
30
|
+
# Or create config via flags (AI/script friendly)
|
|
31
|
+
island-bridge init --host 192.168.1.100 --user deploy --paths "/var/www/app,/etc/nginx/conf.d"
|
|
32
|
+
|
|
33
|
+
# Check environment is ready
|
|
34
|
+
island-bridge status
|
|
35
|
+
|
|
36
|
+
# Pull remote folders to local
|
|
37
|
+
island-bridge pull
|
|
38
|
+
|
|
39
|
+
# Push local changes back
|
|
40
|
+
island-bridge push
|
|
41
|
+
```
|
|
42
|
+
|
|
24
43
|
## Usage
|
|
25
44
|
|
|
26
|
-
1. Create `island-bridge.json` in your working directory:
|
|
45
|
+
1. Create `island-bridge.json` in your working directory (or run `island-bridge init`):
|
|
27
46
|
|
|
28
47
|
```json
|
|
29
48
|
{
|
|
@@ -68,6 +87,9 @@ island-bridge push
|
|
|
68
87
|
| `watch` | Watch local folders and auto-push on changes |
|
|
69
88
|
| `diff` | Preview changes without syncing |
|
|
70
89
|
| `history` | Show sync history |
|
|
90
|
+
| `init` | Create config file interactively or via flags |
|
|
91
|
+
| `status` | Show config, SSH, rsync, and paths status |
|
|
92
|
+
| `backup` | Manage sync backups (list, restore, clean) |
|
|
71
93
|
|
|
72
94
|
## Options
|
|
73
95
|
|
|
@@ -80,9 +102,80 @@ island-bridge push
|
|
|
80
102
|
| `--env <name>` | Use named profile from config |
|
|
81
103
|
| `--bwlimit <KB/s>` | Limit transfer bandwidth |
|
|
82
104
|
| `-s, --select` | Interactively select folders to sync |
|
|
105
|
+
| `--path <name>` | Sync specific folder by name (repeatable) |
|
|
106
|
+
| `--json` | Output in JSON format (for scripts/AI) |
|
|
107
|
+
| `--no-backup` | Skip backup for this sync |
|
|
83
108
|
| `-V, --version` | Show version |
|
|
84
109
|
| `-h, --help` | Show help |
|
|
85
110
|
|
|
111
|
+
## Backup & Restore
|
|
112
|
+
|
|
113
|
+
Backups are **enabled by default**. Before each sync, files that will be overwritten are automatically saved to a timestamped backup directory.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# List available backups
|
|
117
|
+
island-bridge backup list
|
|
118
|
+
|
|
119
|
+
# Restore a specific backup
|
|
120
|
+
island-bridge backup restore 2026-04-07T14-30-00
|
|
121
|
+
|
|
122
|
+
# Clean old backups, keep 5 most recent
|
|
123
|
+
island-bridge backup clean --keep 5
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Backup Config
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"backup": {
|
|
131
|
+
"enabled": true,
|
|
132
|
+
"maxCount": 10,
|
|
133
|
+
"localDir": ".island-bridge-backups",
|
|
134
|
+
"remoteDir": "~/.island-bridge-backups"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Use `--no-backup` to skip backup for a single sync operation.
|
|
140
|
+
|
|
141
|
+
## JSON Output (AI/Script Friendly)
|
|
142
|
+
|
|
143
|
+
Add `--json` to any command for structured JSON output — no colors, no progress bars, no interactive prompts:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Check environment before syncing
|
|
147
|
+
island-bridge status --json
|
|
148
|
+
|
|
149
|
+
# Sync and parse results programmatically
|
|
150
|
+
island-bridge pull --json
|
|
151
|
+
|
|
152
|
+
# Create config non-interactively
|
|
153
|
+
island-bridge init --json --host example.com --user deploy --paths "/var/www/app"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Example JSON output:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"version": "2.0.0",
|
|
161
|
+
"command": "pull",
|
|
162
|
+
"success": true,
|
|
163
|
+
"results": [
|
|
164
|
+
{
|
|
165
|
+
"folder": "app",
|
|
166
|
+
"remotePath": "/var/www/app",
|
|
167
|
+
"success": true,
|
|
168
|
+
"changes": [
|
|
169
|
+
{ "type": "add", "file": "index.js" },
|
|
170
|
+
{ "type": "delete", "file": "old.css" }
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"messages": [],
|
|
175
|
+
"errors": []
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
86
179
|
## Advanced Config
|
|
87
180
|
|
|
88
181
|
```json
|
|
@@ -94,6 +187,10 @@ island-bridge push
|
|
|
94
187
|
},
|
|
95
188
|
"exclude": ["node_modules", ".DS_Store", "*.log"],
|
|
96
189
|
"bwlimit": 1000,
|
|
190
|
+
"backup": {
|
|
191
|
+
"enabled": true,
|
|
192
|
+
"maxCount": 10
|
|
193
|
+
},
|
|
97
194
|
"hooks": {
|
|
98
195
|
"beforeSync": "echo 'Starting sync...'",
|
|
99
196
|
"afterSync": "pm2 restart app"
|
|
@@ -162,6 +259,12 @@ island-bridge pull --config /path/to/my-config.json
|
|
|
162
259
|
## Examples
|
|
163
260
|
|
|
164
261
|
```bash
|
|
262
|
+
# Create config interactively
|
|
263
|
+
island-bridge init
|
|
264
|
+
|
|
265
|
+
# Check everything is working
|
|
266
|
+
island-bridge status
|
|
267
|
+
|
|
165
268
|
# Preview what would change
|
|
166
269
|
island-bridge pull --dry-run
|
|
167
270
|
|
|
@@ -171,6 +274,9 @@ island-bridge diff
|
|
|
171
274
|
# Sync with bandwidth limit
|
|
172
275
|
island-bridge pull --bwlimit 500
|
|
173
276
|
|
|
277
|
+
# Sync only specific folder
|
|
278
|
+
island-bridge pull --path app
|
|
279
|
+
|
|
174
280
|
# Auto-push on file changes
|
|
175
281
|
island-bridge watch
|
|
176
282
|
|
|
@@ -183,8 +289,15 @@ island-bridge pull --env staging
|
|
|
183
289
|
# Quiet mode for CI/scripts
|
|
184
290
|
island-bridge push --quiet
|
|
185
291
|
|
|
292
|
+
# JSON output for AI/scripts
|
|
293
|
+
island-bridge pull --json
|
|
294
|
+
|
|
186
295
|
# View sync history
|
|
187
296
|
island-bridge history
|
|
297
|
+
|
|
298
|
+
# List and restore backups
|
|
299
|
+
island-bridge backup list
|
|
300
|
+
island-bridge backup restore 2026-04-07T14-30-00
|
|
188
301
|
```
|
|
189
302
|
|
|
190
303
|
## Features
|
|
@@ -207,6 +320,12 @@ island-bridge history
|
|
|
207
320
|
- **Config search** — finds config in parent directories
|
|
208
321
|
- **Fault tolerant** — skips failed transfers, reports summary at end
|
|
209
322
|
- **Zero dependencies** — pure Node.js, no npm dependencies
|
|
323
|
+
- **Auto backup** — files backed up before overwrite, with restore support
|
|
324
|
+
- **JSON output** — structured output for AI agents and scripts
|
|
325
|
+
- **Init command** — interactive or flag-based config setup
|
|
326
|
+
- **Status check** — diagnose SSH, rsync, and path issues
|
|
327
|
+
- **Path filter** — sync specific folders with `--path`
|
|
328
|
+
- **Error hints** — actionable suggestions on every error
|
|
210
329
|
|
|
211
330
|
## License
|
|
212
331
|
|
package/bin/cli.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from '../lib/args.js';
|
|
4
|
-
import { loadConfig } from '../lib/config.js';
|
|
4
|
+
import { loadConfig, extractFolderName } from '../lib/config.js';
|
|
5
5
|
import { checkRsync, syncAll, diffPreview } from '../lib/sync.js';
|
|
6
|
-
import {
|
|
6
|
+
import { Reporter } from '../lib/reporter.js';
|
|
7
7
|
import { startWatch } from '../lib/watch.js';
|
|
8
8
|
import { recordSync, showHistory } from '../lib/history.js';
|
|
9
9
|
import { runHook } from '../lib/hooks.js';
|
|
10
10
|
import { selectPaths } from '../lib/interactive.js';
|
|
11
|
+
import { runInit } from '../lib/init.js';
|
|
12
|
+
import { runStatus } from '../lib/status.js';
|
|
13
|
+
import { listBackups, restoreBackup, cleanBackups } from '../lib/backup.js';
|
|
14
|
+
import { getErrorHint } from '../lib/summary.js';
|
|
11
15
|
import { readFileSync } from 'node:fs';
|
|
12
16
|
import { fileURLToPath } from 'node:url';
|
|
13
17
|
import { dirname, join } from 'node:path';
|
|
@@ -27,6 +31,9 @@ Commands:
|
|
|
27
31
|
watch Watch local folders and auto-push on changes
|
|
28
32
|
diff Preview changes without syncing
|
|
29
33
|
history Show sync history
|
|
34
|
+
init Create island-bridge.json config interactively
|
|
35
|
+
status Show config, SSH, rsync, and paths status
|
|
36
|
+
backup Manage sync backups (list, restore, clean)
|
|
30
37
|
|
|
31
38
|
Options:
|
|
32
39
|
-n, --dry-run Preview sync without making changes
|
|
@@ -36,17 +43,16 @@ Options:
|
|
|
36
43
|
--env <name> Use named profile from config
|
|
37
44
|
--bwlimit <KB/s> Limit transfer bandwidth
|
|
38
45
|
-s, --select Interactively select folders to sync
|
|
46
|
+
--path <name> Sync specific folder by name (repeatable)
|
|
47
|
+
--json Output in JSON format (for scripts/AI)
|
|
48
|
+
--no-backup Skip backup for this sync
|
|
39
49
|
-V, --version Show version
|
|
40
50
|
-h, --help Show this help message
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"bwlimit": 1000,
|
|
47
|
-
"hooks": { "beforeSync": "...", "afterSync": "..." },
|
|
48
|
-
"profiles": { "staging": { "remote": { ... } } }
|
|
49
|
-
}
|
|
52
|
+
Backup subcommands:
|
|
53
|
+
backup list List available backups
|
|
54
|
+
backup restore <timestamp> Restore files from a backup
|
|
55
|
+
backup clean --keep <N> Remove old backups, keep N most recent
|
|
50
56
|
`.trim();
|
|
51
57
|
|
|
52
58
|
async function main() {
|
|
@@ -58,24 +64,44 @@ async function main() {
|
|
|
58
64
|
process.exit(1);
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
const reporter = new Reporter(args.json ? 'json' : 'human');
|
|
68
|
+
|
|
61
69
|
if (args.help || (!args.command && !args.version)) {
|
|
62
70
|
console.log(USAGE);
|
|
63
71
|
process.exit(0);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
if (args.version) {
|
|
67
|
-
|
|
75
|
+
if (args.json) {
|
|
76
|
+
reporter.setCommand('version');
|
|
77
|
+
reporter.info(pkg.version);
|
|
78
|
+
reporter.flush();
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`island-bridge v${pkg.version}`);
|
|
81
|
+
}
|
|
68
82
|
process.exit(0);
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
const validCommands = ['pull', 'push', 'watch', 'diff', 'history'];
|
|
85
|
+
const validCommands = ['pull', 'push', 'watch', 'diff', 'history', 'init', 'status', 'backup'];
|
|
72
86
|
if (!validCommands.includes(args.command)) {
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
reporter.error(
|
|
88
|
+
`Unknown command: ${args.command}`,
|
|
89
|
+
`Valid commands: ${validCommands.join(', ')}`
|
|
90
|
+
);
|
|
91
|
+
if (args.json) reporter.flush();
|
|
75
92
|
process.exit(1);
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
|
|
95
|
+
reporter.setCommand(args.command);
|
|
96
|
+
|
|
97
|
+
// === Init ===
|
|
98
|
+
if (args.command === 'init') {
|
|
99
|
+
const ok = await runInit(args, reporter);
|
|
100
|
+
if (args.json) reporter.flush();
|
|
101
|
+
process.exit(ok ? 0 : 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// === History (doesn't need rsync) ===
|
|
79
105
|
if (args.command === 'history') {
|
|
80
106
|
let config;
|
|
81
107
|
try {
|
|
@@ -83,116 +109,239 @@ async function main() {
|
|
|
83
109
|
} catch {
|
|
84
110
|
// Show history from cwd if no config found
|
|
85
111
|
}
|
|
86
|
-
|
|
112
|
+
if (args.json) {
|
|
113
|
+
const { readFileSync: rfs, existsSync: efs } = await import('node:fs');
|
|
114
|
+
const { dirname: dn, join: jn } = await import('node:path');
|
|
115
|
+
const historyFile = config?._filePath
|
|
116
|
+
? jn(dn(config._filePath), '.island-bridge-history.json')
|
|
117
|
+
: '.island-bridge-history.json';
|
|
118
|
+
let entries = [];
|
|
119
|
+
if (efs(historyFile)) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(rfs(historyFile, 'utf-8'));
|
|
122
|
+
entries = Array.isArray(parsed) ? parsed : [];
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
reporter.historyReport(entries);
|
|
126
|
+
reporter.flush();
|
|
127
|
+
} else {
|
|
128
|
+
showHistory(config?._filePath);
|
|
129
|
+
}
|
|
87
130
|
process.exit(0);
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
//
|
|
133
|
+
// === Backup ===
|
|
134
|
+
if (args.command === 'backup') {
|
|
135
|
+
let config;
|
|
136
|
+
try {
|
|
137
|
+
config = loadConfig({ configPath: args.config, env: args.env });
|
|
138
|
+
} catch {
|
|
139
|
+
config = { backup: { enabled: true, localDir: '.island-bridge-backups', remoteDir: '~/.island-bridge-backups', maxCount: 10 } };
|
|
140
|
+
}
|
|
141
|
+
const backupConfig = config.backup;
|
|
142
|
+
|
|
143
|
+
if (args.subcommand === 'list') {
|
|
144
|
+
const backups = listBackups(backupConfig.localDir);
|
|
145
|
+
if (args.json) {
|
|
146
|
+
reporter.historyReport(backups.map(b => ({ timestamp: b.name, date: b.date.toISOString() })));
|
|
147
|
+
reporter.flush();
|
|
148
|
+
} else {
|
|
149
|
+
if (backups.length === 0) {
|
|
150
|
+
console.log('No backups found.');
|
|
151
|
+
} else {
|
|
152
|
+
console.log('\n--- Backups ---\n');
|
|
153
|
+
for (const b of backups) {
|
|
154
|
+
console.log(` ${b.name} (${b.date.toLocaleString()})`);
|
|
155
|
+
}
|
|
156
|
+
console.log(`\n${backups.length} backup(s) found.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (args.subcommand === 'restore') {
|
|
163
|
+
if (!args.backupTimestamp) {
|
|
164
|
+
reporter.error('backup restore requires a timestamp', 'Run "island-bridge backup list" to see available backups');
|
|
165
|
+
if (args.json) reporter.flush();
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const results = restoreBackup(backupConfig.localDir, args.backupTimestamp);
|
|
170
|
+
if (args.json) {
|
|
171
|
+
for (const r of results) {
|
|
172
|
+
reporter.syncEnd(r.folder, r);
|
|
173
|
+
}
|
|
174
|
+
reporter.flush();
|
|
175
|
+
} else {
|
|
176
|
+
for (const r of results) {
|
|
177
|
+
if (r.success) {
|
|
178
|
+
console.log(` \x1b[32m\u2713\x1b[0m ${r.folder} restored`);
|
|
179
|
+
} else {
|
|
180
|
+
console.log(` \x1b[31m\u2717\x1b[0m ${r.folder} \u2014 ${r.error}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
reporter.error(err.message, 'Run "island-bridge backup list" to see available backups');
|
|
186
|
+
if (args.json) reporter.flush();
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (args.subcommand === 'clean') {
|
|
193
|
+
const keep = args.keep || backupConfig.maxCount || 10;
|
|
194
|
+
const removed = cleanBackups(backupConfig.localDir, keep);
|
|
195
|
+
if (args.json) {
|
|
196
|
+
reporter.info(`Removed ${removed.length} backup(s), keeping ${keep}`);
|
|
197
|
+
reporter.flush();
|
|
198
|
+
} else {
|
|
199
|
+
if (removed.length === 0) {
|
|
200
|
+
console.log(`No backups to remove (keeping ${keep}).`);
|
|
201
|
+
} else {
|
|
202
|
+
for (const name of removed) {
|
|
203
|
+
console.log(` Removed: ${name}`);
|
|
204
|
+
}
|
|
205
|
+
console.log(`\nRemoved ${removed.length} backup(s), keeping ${keep}.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
reporter.error(
|
|
212
|
+
'Unknown backup subcommand',
|
|
213
|
+
'Usage: island-bridge backup <list|restore|clean>'
|
|
214
|
+
);
|
|
215
|
+
if (args.json) reporter.flush();
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// === Pre-flight: check rsync ===
|
|
91
220
|
const rsyncOk = await checkRsync();
|
|
92
221
|
if (!rsyncOk) {
|
|
93
|
-
|
|
222
|
+
reporter.error(
|
|
223
|
+
'rsync is required but not found in PATH',
|
|
224
|
+
getErrorHint('rsync-not-found')
|
|
225
|
+
);
|
|
226
|
+
if (args.json) reporter.flush();
|
|
94
227
|
process.exit(1);
|
|
95
228
|
}
|
|
96
229
|
|
|
97
|
-
// Load config
|
|
230
|
+
// === Load config ===
|
|
98
231
|
let config;
|
|
99
232
|
try {
|
|
100
233
|
config = loadConfig({ configPath: args.config, env: args.env });
|
|
101
234
|
} catch (err) {
|
|
102
|
-
|
|
235
|
+
const hint = err.message.includes('Cannot find')
|
|
236
|
+
? getErrorHint('config-not-found')
|
|
237
|
+
: null;
|
|
238
|
+
reporter.error(err.message, hint);
|
|
239
|
+
if (args.json) reporter.flush();
|
|
103
240
|
process.exit(1);
|
|
104
241
|
}
|
|
105
242
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
243
|
+
if (args.env) {
|
|
244
|
+
reporter.info(`Using profile: ${args.env}`);
|
|
245
|
+
}
|
|
246
|
+
if (config._filePath) {
|
|
247
|
+
reporter.info(`Config: ${config._filePath}`);
|
|
108
248
|
}
|
|
109
249
|
|
|
110
|
-
|
|
111
|
-
|
|
250
|
+
// === Status ===
|
|
251
|
+
if (args.command === 'status') {
|
|
252
|
+
await runStatus(config, reporter);
|
|
253
|
+
if (args.json) reporter.flush();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// === --path filter ===
|
|
258
|
+
if (args.path.length > 0) {
|
|
259
|
+
const allNames = config.remote.paths.map(p => extractFolderName(p));
|
|
260
|
+
const invalid = args.path.filter(name => !allNames.includes(name));
|
|
261
|
+
if (invalid.length > 0) {
|
|
262
|
+
reporter.error(
|
|
263
|
+
`Unknown folder name(s): ${invalid.join(', ')}`,
|
|
264
|
+
`Available folders: ${allNames.join(', ')}`
|
|
265
|
+
);
|
|
266
|
+
if (args.json) reporter.flush();
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
config.remote.paths = config.remote.paths.filter(p =>
|
|
270
|
+
args.path.includes(extractFolderName(p))
|
|
271
|
+
);
|
|
112
272
|
}
|
|
113
273
|
|
|
114
|
-
// Interactive selection
|
|
115
|
-
if (args.select && ['pull', 'push', 'diff'].includes(args.command)) {
|
|
116
|
-
config.remote.paths = await selectPaths(config.remote.paths);
|
|
274
|
+
// === Interactive selection ===
|
|
275
|
+
if (args.select && !args.json && ['pull', 'push', 'diff'].includes(args.command)) {
|
|
276
|
+
config.remote.paths = await selectPaths(config.remote.paths, reporter);
|
|
117
277
|
}
|
|
118
278
|
|
|
119
|
-
// Watch mode
|
|
279
|
+
// === Watch mode ===
|
|
120
280
|
if (args.command === 'watch') {
|
|
121
281
|
startWatch(config, {
|
|
122
282
|
dryRun: args.dryRun,
|
|
123
283
|
verbose: args.verbose,
|
|
124
284
|
quiet: args.quiet,
|
|
125
285
|
bwlimit: args.bwlimit,
|
|
126
|
-
|
|
127
|
-
|
|
286
|
+
noBackup: args.noBackup,
|
|
287
|
+
}, reporter);
|
|
288
|
+
return;
|
|
128
289
|
}
|
|
129
290
|
|
|
130
|
-
// Diff preview
|
|
291
|
+
// === Diff preview ===
|
|
131
292
|
if (args.command === 'diff') {
|
|
132
293
|
const diffs = await diffPreview(config, 'pull', {
|
|
133
294
|
bwlimit: args.bwlimit,
|
|
134
295
|
exclude: config.exclude,
|
|
135
296
|
});
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
console.log('\nNo changes detected.');
|
|
139
|
-
} else {
|
|
140
|
-
for (const d of diffs) {
|
|
141
|
-
console.log(`\n\x1b[1m${d.folderName}\x1b[0m (${d.remotePath}):`);
|
|
142
|
-
for (const line of d.changes) {
|
|
143
|
-
if (line.startsWith('*deleting')) {
|
|
144
|
-
console.log(` \x1b[31m- ${line}\x1b[0m`);
|
|
145
|
-
} else if (line.startsWith('>') || line.startsWith('<')) {
|
|
146
|
-
console.log(` \x1b[32m+ ${line}\x1b[0m`);
|
|
147
|
-
} else {
|
|
148
|
-
console.log(` \x1b[33m~ ${line}\x1b[0m`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
297
|
+
reporter.diffReport(diffs);
|
|
298
|
+
if (args.json) reporter.flush();
|
|
153
299
|
process.exit(0);
|
|
154
300
|
}
|
|
155
301
|
|
|
156
|
-
// Pull or Push
|
|
302
|
+
// === Pull or Push ===
|
|
157
303
|
const options = {
|
|
158
304
|
dryRun: args.dryRun,
|
|
159
305
|
verbose: args.verbose,
|
|
160
306
|
quiet: args.quiet,
|
|
161
307
|
bwlimit: args.bwlimit,
|
|
308
|
+
noBackup: args.noBackup,
|
|
162
309
|
};
|
|
163
310
|
|
|
164
|
-
// Disable hooks from configs found via upward search
|
|
311
|
+
// Disable hooks from configs found via upward search
|
|
165
312
|
if (!config._explicitConfig && config._filePath !== join(process.cwd(), 'island-bridge.json')) {
|
|
166
313
|
if (config.hooks.beforeSync || config.hooks.afterSync) {
|
|
167
|
-
|
|
168
|
-
|
|
314
|
+
reporter.warn(`hooks ignored from inherited config ${config._filePath}`);
|
|
315
|
+
reporter.warn(`Use --config ${config._filePath} to explicitly enable hooks.`);
|
|
169
316
|
config.hooks = {};
|
|
170
317
|
}
|
|
171
318
|
}
|
|
172
319
|
|
|
173
320
|
// Run beforeSync hook
|
|
174
321
|
if (config.hooks.beforeSync) {
|
|
175
|
-
runHook('beforeSync', config.hooks.beforeSync, args.quiet);
|
|
322
|
+
runHook('beforeSync', config.hooks.beforeSync, args.quiet, reporter);
|
|
176
323
|
}
|
|
177
324
|
|
|
178
|
-
const results = await syncAll(config, args.command, options);
|
|
325
|
+
const results = await syncAll(config, args.command, options, reporter);
|
|
179
326
|
|
|
180
327
|
// Run afterSync hook
|
|
181
328
|
if (config.hooks.afterSync) {
|
|
182
|
-
runHook('afterSync', config.hooks.afterSync, args.quiet);
|
|
329
|
+
runHook('afterSync', config.hooks.afterSync, args.quiet, reporter);
|
|
183
330
|
}
|
|
184
331
|
|
|
185
332
|
// Record history
|
|
186
333
|
recordSync(config._filePath, args.command, results);
|
|
187
334
|
|
|
188
|
-
// Print summary
|
|
189
|
-
if (!args.quiet) {
|
|
190
|
-
|
|
191
|
-
if (args.dryRun) {
|
|
192
|
-
|
|
335
|
+
// Print summary
|
|
336
|
+
if (!args.quiet || args.json) {
|
|
337
|
+
reporter.summary(results);
|
|
338
|
+
if (!args.json && args.dryRun) {
|
|
339
|
+
reporter.info('(dry-run mode \u2014 no changes were made)');
|
|
193
340
|
}
|
|
194
341
|
}
|
|
195
342
|
|
|
343
|
+
if (args.json) reporter.flush();
|
|
344
|
+
|
|
196
345
|
const hasFailed = results.some(r => !r.success);
|
|
197
346
|
process.exit(hasFailed ? 1 : 0);
|
|
198
347
|
}
|
package/lib/args.js
CHANGED
|
@@ -5,15 +5,26 @@
|
|
|
5
5
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
6
6
|
const args = {
|
|
7
7
|
command: null,
|
|
8
|
+
subcommand: null,
|
|
8
9
|
dryRun: false,
|
|
9
10
|
verbose: false,
|
|
10
11
|
quiet: false,
|
|
12
|
+
json: false,
|
|
11
13
|
config: null,
|
|
12
14
|
env: null,
|
|
13
15
|
bwlimit: null,
|
|
14
16
|
select: false,
|
|
17
|
+
noBackup: false,
|
|
18
|
+
path: [],
|
|
15
19
|
help: false,
|
|
16
20
|
version: false,
|
|
21
|
+
// init-specific
|
|
22
|
+
host: null,
|
|
23
|
+
user: null,
|
|
24
|
+
paths: null,
|
|
25
|
+
// backup-specific
|
|
26
|
+
backupTimestamp: null,
|
|
27
|
+
keep: null,
|
|
17
28
|
};
|
|
18
29
|
|
|
19
30
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -31,6 +42,9 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
31
42
|
case '-q':
|
|
32
43
|
args.quiet = true;
|
|
33
44
|
break;
|
|
45
|
+
case '--json':
|
|
46
|
+
args.json = true;
|
|
47
|
+
break;
|
|
34
48
|
case '--config':
|
|
35
49
|
case '-c':
|
|
36
50
|
args.config = argv[++i];
|
|
@@ -51,6 +65,35 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
51
65
|
case '-s':
|
|
52
66
|
args.select = true;
|
|
53
67
|
break;
|
|
68
|
+
case '--no-backup':
|
|
69
|
+
args.noBackup = true;
|
|
70
|
+
break;
|
|
71
|
+
case '--path':
|
|
72
|
+
{
|
|
73
|
+
const val = argv[++i];
|
|
74
|
+
if (!val || val.startsWith('-')) throw new Error('--path requires a folder name');
|
|
75
|
+
args.path.push(val);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case '--host':
|
|
79
|
+
args.host = argv[++i];
|
|
80
|
+
if (!args.host) throw new Error('--host requires a value');
|
|
81
|
+
break;
|
|
82
|
+
case '--user':
|
|
83
|
+
args.user = argv[++i];
|
|
84
|
+
if (!args.user) throw new Error('--user requires a value');
|
|
85
|
+
break;
|
|
86
|
+
case '--paths':
|
|
87
|
+
args.paths = argv[++i];
|
|
88
|
+
if (!args.paths) throw new Error('--paths requires a value');
|
|
89
|
+
break;
|
|
90
|
+
case '--keep':
|
|
91
|
+
args.keep = argv[++i];
|
|
92
|
+
if (!args.keep || isNaN(Number(args.keep))) {
|
|
93
|
+
throw new Error('--keep requires a numeric value');
|
|
94
|
+
}
|
|
95
|
+
args.keep = Number(args.keep);
|
|
96
|
+
break;
|
|
54
97
|
case '--help':
|
|
55
98
|
case '-h':
|
|
56
99
|
args.help = true;
|
|
@@ -60,8 +103,14 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
60
103
|
args.version = true;
|
|
61
104
|
break;
|
|
62
105
|
default:
|
|
63
|
-
if (!arg.startsWith('-')
|
|
64
|
-
args.command
|
|
106
|
+
if (!arg.startsWith('-')) {
|
|
107
|
+
if (!args.command) {
|
|
108
|
+
args.command = arg;
|
|
109
|
+
} else if (args.command === 'backup' && !args.subcommand) {
|
|
110
|
+
args.subcommand = arg;
|
|
111
|
+
} else if (args.command === 'backup' && args.subcommand === 'restore' && !args.backupTimestamp) {
|
|
112
|
+
args.backupTimestamp = arg;
|
|
113
|
+
}
|
|
65
114
|
}
|
|
66
115
|
break;
|
|
67
116
|
}
|