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 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 { printSummary } from '../lib/summary.js';
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
- Config (island-bridge.json):
43
- {
44
- "remote": { "host": "...", "user": "...", "paths": [...] },
45
- "exclude": ["node_modules", "*.log"],
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
- console.log(`island-bridge v${pkg.version}`);
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
- console.error(`Unknown command: ${args.command}`);
74
- console.error(`Valid commands: ${validCommands.join(', ')}`);
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
- // History doesn't need rsync
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
- showHistory(config?._filePath);
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
- // Pre-flight: check rsync
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
- console.error('\x1b[31mError: rsync is required but not found in PATH. Install rsync and try again.\x1b[0m');
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
- console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
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 (!args.quiet && args.env) {
107
- console.log(`\x1b[90mUsing profile: ${args.env}\x1b[0m`);
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
- if (!args.quiet && config._filePath) {
111
- console.log(`\x1b[90mConfig: ${config._filePath}\x1b[0m`);
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
- return; // watch runs indefinitely
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 (diffs.length === 0) {
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 (planted-config protection)
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
- console.warn(`\x1b[33mWarning: hooks ignored from inherited config ${config._filePath}\x1b[0m`);
168
- console.warn(`\x1b[33mUse --config ${config._filePath} to explicitly enable hooks.\x1b[0m`);
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 (unless quiet)
189
- if (!args.quiet) {
190
- printSummary(results);
191
- if (args.dryRun) {
192
- console.log('\n\x1b[90m(dry-run mode \u2014 no changes were made)\x1b[0m');
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('-') && !args.command) {
64
- args.command = arg;
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
  }