island-bridge 2.0.0 → 2.0.1

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.
Files changed (60) hide show
  1. package/dist/bin/cli.d.ts +2 -0
  2. package/dist/bin/cli.js +327 -0
  3. package/dist/bin/cli.js.map +1 -0
  4. package/dist/lib/args.d.ts +6 -0
  5. package/dist/lib/args.js +128 -0
  6. package/dist/lib/args.js.map +1 -0
  7. package/dist/lib/backup.d.ts +26 -0
  8. package/dist/lib/backup.js +96 -0
  9. package/dist/lib/backup.js.map +1 -0
  10. package/dist/lib/config.d.ts +25 -0
  11. package/dist/lib/config.js +216 -0
  12. package/dist/lib/config.js.map +1 -0
  13. package/dist/lib/history.d.ts +9 -0
  14. package/dist/lib/history.js +84 -0
  15. package/dist/lib/history.js.map +1 -0
  16. package/dist/lib/hooks.d.ts +11 -0
  17. package/dist/lib/hooks.js +36 -0
  18. package/dist/lib/hooks.js.map +1 -0
  19. package/dist/lib/init.d.ts +28 -0
  20. package/dist/lib/init.js +90 -0
  21. package/dist/lib/init.js.map +1 -0
  22. package/dist/lib/interactive.d.ts +9 -0
  23. package/dist/lib/interactive.js +41 -0
  24. package/dist/lib/interactive.js.map +1 -0
  25. package/dist/lib/progress.d.ts +18 -0
  26. package/dist/lib/progress.js +65 -0
  27. package/dist/lib/progress.js.map +1 -0
  28. package/dist/lib/reporter.d.ts +59 -0
  29. package/dist/lib/reporter.js +189 -0
  30. package/dist/lib/reporter.js.map +1 -0
  31. package/dist/lib/status.d.ts +41 -0
  32. package/dist/lib/status.js +123 -0
  33. package/dist/lib/status.js.map +1 -0
  34. package/dist/lib/summary.d.ts +19 -0
  35. package/dist/lib/summary.js +56 -0
  36. package/dist/lib/summary.js.map +1 -0
  37. package/dist/lib/sync.d.ts +32 -0
  38. package/dist/lib/sync.js +237 -0
  39. package/dist/lib/sync.js.map +1 -0
  40. package/dist/lib/types.d.ts +91 -0
  41. package/dist/lib/types.js +2 -0
  42. package/dist/lib/types.js.map +1 -0
  43. package/dist/lib/watch.d.ts +10 -0
  44. package/dist/lib/watch.js +73 -0
  45. package/dist/lib/watch.js.map +1 -0
  46. package/package.json +11 -5
  47. package/bin/cli.js +0 -349
  48. package/lib/args.js +0 -124
  49. package/lib/backup.js +0 -124
  50. package/lib/config.js +0 -234
  51. package/lib/history.js +0 -94
  52. package/lib/hooks.js +0 -33
  53. package/lib/init.js +0 -110
  54. package/lib/interactive.js +0 -52
  55. package/lib/progress.js +0 -67
  56. package/lib/reporter.js +0 -187
  57. package/lib/status.js +0 -127
  58. package/lib/summary.js +0 -62
  59. package/lib/sync.js +0 -267
  60. package/lib/watch.js +0 -78
@@ -0,0 +1,216 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { basename, resolve, dirname, join } from 'node:path';
3
+ const CONFIG_FILE = 'island-bridge.json';
4
+ /**
5
+ * Search for config file starting from startDir, walking up to root.
6
+ */
7
+ export function findConfigFile(startDir = process.cwd()) {
8
+ let dir = resolve(startDir);
9
+ while (true) {
10
+ const candidate = join(dir, CONFIG_FILE);
11
+ if (existsSync(candidate))
12
+ return candidate;
13
+ const parent = dirname(dir);
14
+ if (parent === dir)
15
+ break;
16
+ dir = parent;
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Load and validate config.
22
+ */
23
+ export function loadConfig(options = {}) {
24
+ const { configPath, env } = options;
25
+ let filePath;
26
+ if (configPath) {
27
+ filePath = resolve(configPath);
28
+ if (!existsSync(filePath)) {
29
+ throw new Error(`Config file not found: ${configPath}`);
30
+ }
31
+ }
32
+ else {
33
+ const found = findConfigFile();
34
+ if (!found) {
35
+ throw new Error(`Cannot find ${CONFIG_FILE} in current or parent directories`);
36
+ }
37
+ filePath = found;
38
+ }
39
+ let raw;
40
+ try {
41
+ raw = readFileSync(filePath, 'utf-8');
42
+ }
43
+ catch {
44
+ throw new Error(`Failed to read config: ${filePath}`);
45
+ }
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ let config;
48
+ try {
49
+ config = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ throw new Error(`Failed to parse ${filePath}: invalid JSON`);
53
+ }
54
+ // Merge profile if specified
55
+ if (env) {
56
+ if (env === '__proto__' || env === 'constructor' || env === 'prototype') {
57
+ throw new Error(`Config error: profile name '${env}' is not allowed`);
58
+ }
59
+ if (!config.profiles || !config.profiles[env]) {
60
+ const available = config.profiles ? Object.keys(config.profiles).join(', ') : 'none';
61
+ throw new Error(`Profile '${env}' not found. Available: ${available}`);
62
+ }
63
+ const profile = config.profiles[env];
64
+ config = {
65
+ ...config,
66
+ ...profile,
67
+ remote: { ...config.remote, ...profile.remote },
68
+ hooks: { ...config.hooks, ...profile.hooks },
69
+ };
70
+ }
71
+ validate(config);
72
+ // Normalize optional fields
73
+ config.exclude = config.exclude || [];
74
+ config.hooks = config.hooks || {};
75
+ config.bwlimit = config.bwlimit || null;
76
+ config.backup = { ...backupDefaults(), ...(config.backup || {}) };
77
+ config._filePath = filePath;
78
+ config._explicitConfig = !!configPath;
79
+ return config;
80
+ }
81
+ /**
82
+ * Validate config structure and values.
83
+ */
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ function validate(config) {
86
+ if (!config.remote) {
87
+ throw new Error("Config error: missing 'remote' section");
88
+ }
89
+ const { host, user, paths } = config.remote;
90
+ if (!host || typeof host !== 'string' || host.trim() === '') {
91
+ throw new Error("Config error: 'host' must be a non-empty string");
92
+ }
93
+ if (!/^[a-zA-Z0-9._:-]+$/.test(host)) {
94
+ throw new Error("Config error: 'host' contains invalid characters. Use hostname, IPv4, or IPv6 only");
95
+ }
96
+ if (!user || typeof user !== 'string' || user.trim() === '') {
97
+ throw new Error("Config error: 'user' must be a non-empty string");
98
+ }
99
+ if (!/^[a-zA-Z0-9._-]+$/.test(user)) {
100
+ throw new Error("Config error: 'user' contains invalid characters");
101
+ }
102
+ if (!Array.isArray(paths) || paths.length === 0) {
103
+ throw new Error("Config error: 'paths' must be a non-empty array of remote paths");
104
+ }
105
+ const seen = new Set();
106
+ for (const p of paths) {
107
+ if (typeof p !== 'string' || p.trim() === '') {
108
+ throw new Error(`Config error: each path must be a non-empty string`);
109
+ }
110
+ if (p.startsWith('-')) {
111
+ throw new Error(`Config error: remote path '${p}' must not start with '-'`);
112
+ }
113
+ if (/[`$;|&><(){}]/.test(p)) {
114
+ throw new Error(`Config error: remote path '${p}' contains disallowed characters`);
115
+ }
116
+ const name = extractFolderName(p);
117
+ if (seen.has(name)) {
118
+ throw new Error(`Config error: folder name collision — multiple remote paths resolve to '${name}'`);
119
+ }
120
+ seen.add(name);
121
+ }
122
+ // Validate exclude
123
+ if (config.exclude !== undefined) {
124
+ if (!Array.isArray(config.exclude)) {
125
+ throw new Error("Config error: 'exclude' must be an array of strings");
126
+ }
127
+ for (const e of config.exclude) {
128
+ if (typeof e !== 'string') {
129
+ throw new Error("Config error: each exclude pattern must be a string");
130
+ }
131
+ if (e.startsWith('-')) {
132
+ throw new Error(`Config error: exclude pattern '${e}' must not start with '-'`);
133
+ }
134
+ }
135
+ }
136
+ // Validate hooks
137
+ if (config.hooks !== undefined) {
138
+ if (typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
139
+ throw new Error("Config error: 'hooks' must be an object");
140
+ }
141
+ for (const key of Object.keys(config.hooks)) {
142
+ if (key !== 'beforeSync' && key !== 'afterSync') {
143
+ throw new Error(`Config error: unknown hook '${key}'. Supported: beforeSync, afterSync`);
144
+ }
145
+ if (typeof config.hooks[key] !== 'string') {
146
+ throw new Error(`Config error: hook '${key}' must be a string`);
147
+ }
148
+ }
149
+ }
150
+ // Validate bwlimit
151
+ if (config.bwlimit !== undefined && config.bwlimit !== null) {
152
+ if (typeof config.bwlimit !== 'number' || config.bwlimit <= 0) {
153
+ throw new Error("Config error: 'bwlimit' must be a positive number (KB/s)");
154
+ }
155
+ }
156
+ // Validate profiles
157
+ if (config.profiles !== undefined) {
158
+ if (typeof config.profiles !== 'object' || Array.isArray(config.profiles)) {
159
+ throw new Error("Config error: 'profiles' must be an object");
160
+ }
161
+ }
162
+ // Validate backup
163
+ if (config.backup !== undefined) {
164
+ validateBackupConfig(config.backup);
165
+ }
166
+ }
167
+ /**
168
+ * Return default backup config values.
169
+ */
170
+ export function backupDefaults() {
171
+ return {
172
+ enabled: true,
173
+ maxCount: 10,
174
+ localDir: '.island-bridge-backups',
175
+ remoteDir: '~/.island-bridge-backups',
176
+ };
177
+ }
178
+ /**
179
+ * Validate backup config section.
180
+ */
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ export function validateBackupConfig(backup) {
183
+ if (typeof backup !== 'object' || Array.isArray(backup) || backup === null) {
184
+ throw new Error("Config error: 'backup' must be an object");
185
+ }
186
+ if (backup.enabled !== undefined && typeof backup.enabled !== 'boolean') {
187
+ throw new Error("Config error: 'backup.enabled' must be a boolean");
188
+ }
189
+ if (backup.maxCount !== undefined) {
190
+ if (typeof backup.maxCount !== 'number' || backup.maxCount <= 0) {
191
+ throw new Error("Config error: 'backup.maxCount' must be a positive number");
192
+ }
193
+ }
194
+ if (backup.localDir !== undefined && typeof backup.localDir !== 'string') {
195
+ throw new Error("Config error: 'backup.localDir' must be a string");
196
+ }
197
+ if (backup.remoteDir !== undefined && typeof backup.remoteDir !== 'string') {
198
+ throw new Error("Config error: 'backup.remoteDir' must be a string");
199
+ }
200
+ }
201
+ /**
202
+ * Extract folder name (last path component) from a remote path.
203
+ * Strips trailing slashes. Rejects root "/" and empty strings.
204
+ */
205
+ export function extractFolderName(remotePath) {
206
+ const trimmed = remotePath.replace(/\/+$/, '');
207
+ if (trimmed === '' || trimmed === '/') {
208
+ throw new Error(`Config error: remote path '${remotePath}' resolves to root or is empty`);
209
+ }
210
+ const name = basename(trimmed);
211
+ if (!name) {
212
+ throw new Error(`Config error: cannot extract folder name from '${remotePath}'`);
213
+ }
214
+ return name;
215
+ }
216
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { SyncResult } from './types.js';
2
+ /**
3
+ * Record a sync operation to history.
4
+ */
5
+ export declare function recordSync(configPath: string | undefined, direction: string, results: SyncResult[]): void;
6
+ /**
7
+ * Display sync history.
8
+ */
9
+ export declare function showHistory(configPath?: string): void;
@@ -0,0 +1,84 @@
1
+ import { readFileSync, writeFileSync, existsSync, lstatSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ const HISTORY_FILE = '.island-bridge-history.json';
4
+ /**
5
+ * Record a sync operation to history.
6
+ */
7
+ export function recordSync(configPath, direction, results) {
8
+ const historyPath = configPath ? join(dirname(configPath), HISTORY_FILE) : HISTORY_FILE;
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ let history = [];
11
+ if (existsSync(historyPath)) {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(historyPath, 'utf-8'));
14
+ history = Array.isArray(parsed) ? parsed : [];
15
+ }
16
+ catch {
17
+ history = [];
18
+ }
19
+ }
20
+ const entry = {
21
+ timestamp: new Date().toISOString(),
22
+ direction,
23
+ folders: results.map(r => ({
24
+ name: r.folderName,
25
+ path: r.remotePath,
26
+ success: r.success,
27
+ error: r.error || null,
28
+ })),
29
+ success: results.every(r => r.success),
30
+ total: results.length,
31
+ failed: results.filter(r => !r.success).length,
32
+ };
33
+ history.push(entry);
34
+ // Keep last 100 entries
35
+ if (history.length > 100) {
36
+ history = history.slice(-100);
37
+ }
38
+ // Refuse to write through symlinks
39
+ if (existsSync(historyPath) && lstatSync(historyPath).isSymbolicLink()) {
40
+ return;
41
+ }
42
+ try {
43
+ writeFileSync(historyPath, JSON.stringify(history, null, 2) + '\n');
44
+ }
45
+ catch {
46
+ // Silently ignore write errors for history
47
+ }
48
+ }
49
+ /**
50
+ * Display sync history.
51
+ */
52
+ export function showHistory(configPath) {
53
+ const historyPath = configPath ? join(dirname(configPath), HISTORY_FILE) : HISTORY_FILE;
54
+ if (!existsSync(historyPath)) {
55
+ console.log('No sync history found.');
56
+ return;
57
+ }
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ let history;
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(historyPath, 'utf-8'));
62
+ history = Array.isArray(parsed) ? parsed : [];
63
+ }
64
+ catch {
65
+ console.log('Failed to read history file.');
66
+ return;
67
+ }
68
+ if (history.length === 0) {
69
+ console.log('No sync history found.');
70
+ return;
71
+ }
72
+ console.log('\n--- Sync History ---\n');
73
+ const recent = history.slice(-20);
74
+ for (const entry of recent) {
75
+ const date = new Date(entry.timestamp).toLocaleString();
76
+ const status = entry.success ? '\x1b[32m\u2713\x1b[0m' : '\x1b[31m\u2717\x1b[0m';
77
+ const dir = entry.direction === 'pull' ? '\u2193 pull' : '\u2191 push';
78
+ const folders = entry.folders.map((f) => f.name).join(', ');
79
+ const stats = `${entry.total - entry.failed}/${entry.total} ok`;
80
+ console.log(` ${status} ${date} ${dir} [${folders}] ${stats}`);
81
+ }
82
+ console.log(`\nShowing last ${recent.length} of ${history.length} entries.`);
83
+ }
84
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/lib/history.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Execute a hook command.
3
+ * @param name - Hook name (beforeSync, afterSync)
4
+ * @param command - Shell command to execute
5
+ * @param quiet - Suppress output
6
+ * @param reporter - Reporter instance
7
+ */
8
+ export declare function runHook(name: string, command: string, quiet?: boolean, reporter?: {
9
+ info(msg: string): void;
10
+ warn(msg: string): void;
11
+ } | null): void;
@@ -0,0 +1,36 @@
1
+ import { execSync } from 'node:child_process';
2
+ /**
3
+ * Execute a hook command.
4
+ * @param name - Hook name (beforeSync, afterSync)
5
+ * @param command - Shell command to execute
6
+ * @param quiet - Suppress output
7
+ * @param reporter - Reporter instance
8
+ */
9
+ export function runHook(name, command, quiet = false, reporter = null) {
10
+ if (!command)
11
+ return;
12
+ if (reporter) {
13
+ reporter.info(`[hook:${name}] ${command}`);
14
+ }
15
+ else if (!quiet) {
16
+ console.log(`\x1b[90m[hook:${name}] ${command}\x1b[0m`);
17
+ }
18
+ try {
19
+ execSync(command, {
20
+ stdio: quiet ? 'ignore' : 'inherit',
21
+ timeout: 30000,
22
+ shell: '/bin/sh',
23
+ });
24
+ }
25
+ catch (err) {
26
+ const error = err;
27
+ const msg = error.status ? `exited with code ${error.status}` : (error.message ?? String(err));
28
+ if (reporter) {
29
+ reporter.warn(`hook '${name}' failed: ${msg}`);
30
+ }
31
+ else {
32
+ console.error(`\x1b[33mWarning: hook '${name}' failed: ${msg}\x1b[0m`);
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CACrB,IAAY,EACZ,OAAe,EACf,QAAiB,KAAK,EACtB,WAAwE,IAAI;IAE5E,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;IAC7C,CAAC;SAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,CAAC;QACH,QAAQ,CAAC,OAAO,EAAE;YAChB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;YACnC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,GAA4C,CAAC;QAC3D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/F,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,IAAI,CAAC,SAAS,IAAI,aAAa,GAAG,EAAE,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,0BAA0B,IAAI,aAAa,GAAG,SAAS,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { ParsedArgs } from './types.js';
2
+ /**
3
+ * Build a config object from flag values.
4
+ * @param opts - { host, user, paths, exclude? }
5
+ * @returns config object ready to serialize
6
+ */
7
+ export declare function buildConfig(opts: {
8
+ host?: string;
9
+ user?: string;
10
+ paths?: string;
11
+ exclude?: string;
12
+ }): {
13
+ remote: {
14
+ host: string;
15
+ user: string;
16
+ paths: string[];
17
+ };
18
+ exclude?: string[];
19
+ };
20
+ /**
21
+ * Run the init command.
22
+ * @param args - parsed CLI args
23
+ * @param reporter - Reporter instance
24
+ */
25
+ export declare function runInit(args: ParsedArgs, reporter: {
26
+ info(msg: string): void;
27
+ error(msg: string, hint?: string | null): void;
28
+ }): Promise<boolean>;
@@ -0,0 +1,90 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const CONFIG_FILE = 'island-bridge.json';
5
+ /**
6
+ * Build a config object from flag values.
7
+ * @param opts - { host, user, paths, exclude? }
8
+ * @returns config object ready to serialize
9
+ */
10
+ export function buildConfig(opts) {
11
+ if (!opts.host)
12
+ throw new Error('host is required');
13
+ if (!opts.user)
14
+ throw new Error('user is required');
15
+ if (!opts.paths)
16
+ throw new Error('paths is required');
17
+ const remotePaths = opts.paths.split(',').map(p => p.trim()).filter(Boolean);
18
+ const config = {
19
+ remote: {
20
+ host: opts.host,
21
+ user: opts.user,
22
+ paths: remotePaths,
23
+ },
24
+ };
25
+ if (opts.exclude) {
26
+ config.exclude = opts.exclude.split(',').map(e => e.trim()).filter(Boolean);
27
+ }
28
+ return config;
29
+ }
30
+ /**
31
+ * Ask a question via readline.
32
+ */
33
+ function ask(rl, question) {
34
+ return new Promise((resolve) => {
35
+ rl.question(question, resolve);
36
+ });
37
+ }
38
+ /**
39
+ * Run the init command.
40
+ * @param args - parsed CLI args
41
+ * @param reporter - Reporter instance
42
+ */
43
+ export async function runInit(args, reporter) {
44
+ const configPath = join(process.cwd(), CONFIG_FILE);
45
+ // Non-interactive mode (--json or flags provided)
46
+ if (args.json || (args.host && args.user && args.paths)) {
47
+ if (!args.host || !args.user || !args.paths) {
48
+ reporter.error('Non-interactive init requires --host, --user, and --paths', 'Example: island-bridge init --host example.com --user deploy --paths "/var/www/app"');
49
+ return false;
50
+ }
51
+ const config = buildConfig({ host: args.host, user: args.user, paths: args.paths });
52
+ if (existsSync(configPath) && !args.force) {
53
+ reporter.error(`${CONFIG_FILE} already exists`, 'Use --force to overwrite, or delete the file first');
54
+ return false;
55
+ }
56
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
57
+ reporter.info(`Created ${CONFIG_FILE}`);
58
+ return true;
59
+ }
60
+ // Interactive mode
61
+ if (existsSync(configPath)) {
62
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
63
+ const answer = await ask(rl, `${CONFIG_FILE} already exists. Overwrite? (y/N): `);
64
+ rl.close();
65
+ if (answer.trim().toLowerCase() !== 'y') {
66
+ reporter.info('Aborted.');
67
+ return false;
68
+ }
69
+ }
70
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
71
+ const host = await ask(rl, '? Remote host: ');
72
+ const user = await ask(rl, '? SSH user: ');
73
+ const pathsRaw = await ask(rl, '? Remote paths (comma-separated): ');
74
+ const excludeRaw = await ask(rl, '? Exclude patterns (comma-separated, optional): ');
75
+ rl.close();
76
+ if (!host.trim() || !user.trim() || !pathsRaw.trim()) {
77
+ reporter.error('host, user, and paths are required');
78
+ return false;
79
+ }
80
+ const config = buildConfig({
81
+ host: host.trim(),
82
+ user: user.trim(),
83
+ paths: pathsRaw.trim(),
84
+ exclude: excludeRaw.trim() || undefined,
85
+ });
86
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
87
+ reporter.info(`✓ Created ${CONFIG_FILE}`);
88
+ return true;
89
+ }
90
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/lib/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Interactive path selection.
3
+ * Returns filtered paths array.
4
+ * @param paths
5
+ * @param reporter
6
+ */
7
+ export declare function selectPaths(paths: string[], reporter?: {
8
+ info(msg: string): void;
9
+ } | null): Promise<string[]>;
@@ -0,0 +1,41 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { extractFolderName } from './config.js';
3
+ /**
4
+ * Interactive path selection.
5
+ * Returns filtered paths array.
6
+ * @param paths
7
+ * @param reporter
8
+ */
9
+ export async function selectPaths(paths, reporter = null) {
10
+ const write = reporter
11
+ ? (s) => reporter.info(s)
12
+ : (s) => console.log(s);
13
+ write('\nAvailable folders:\n');
14
+ const folders = paths.map((p, i) => {
15
+ const name = extractFolderName(p);
16
+ write(` ${i + 1}) ${name} (${p})`);
17
+ return { index: i, name, path: p };
18
+ });
19
+ write(' a) All\n');
20
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
21
+ const answer = await new Promise((resolve) => {
22
+ rl.question('Select folders (comma-separated numbers or "a" for all): ', resolve);
23
+ });
24
+ rl.close();
25
+ const input = answer.trim().toLowerCase();
26
+ if (input === 'a' || input === 'all' || input === '') {
27
+ return paths;
28
+ }
29
+ const indices = input.split(',')
30
+ .map(s => parseInt(s.trim(), 10) - 1)
31
+ .filter(i => i >= 0 && i < paths.length);
32
+ if (indices.length === 0) {
33
+ write('No valid selection, using all folders.');
34
+ return paths;
35
+ }
36
+ const selected = indices.map(i => paths[i]);
37
+ const names = indices.map(i => folders[i].name).join(', ');
38
+ write(`\nSelected: ${names}\n`);
39
+ return selected;
40
+ }
41
+ //# sourceMappingURL=interactive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive.js","sourceRoot":"","sources":["../../src/lib/interactive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAe,EACf,WAA+C,IAAI;IAEnD,MAAM,KAAK,GAAwB,QAAQ;QACzC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE1B,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAEhC,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjC,MAAM,IAAI,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;QAClC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,YAAY,CAAC,CAAC;IAEpB,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7E,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;QACnD,EAAE,CAAC,QAAQ,CAAC,2DAA2D,EAAE,OAAO,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,KAAK,EAAE,CAAC;IAEX,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE1C,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;SAC7B,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;SACpC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAE3C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,eAAe,KAAK,IAAI,CAAC,CAAC;IAEhC,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Readable } from 'node:stream';
2
+ interface ProgressReporter {
3
+ mode: string;
4
+ _write: (s: string) => void;
5
+ syncProgress: (data: string) => void;
6
+ syncFileChange: (type: string, filename: string) => void;
7
+ }
8
+ /**
9
+ * Parse rsync stdout output in real-time and emit events to reporter.
10
+ * @param stdout - Readable stream
11
+ * @param reporter - Reporter instance (or null for legacy fallback)
12
+ * @param options
13
+ * @param options.verbose - Show extra detail
14
+ */
15
+ export declare function streamProgress(stdout: Readable, reporter: ProgressReporter | null, options?: {
16
+ verbose?: boolean;
17
+ }): void;
18
+ export {};
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Parse rsync stdout output in real-time and emit events to reporter.
3
+ * @param stdout - Readable stream
4
+ * @param reporter - Reporter instance (or null for legacy fallback)
5
+ * @param options
6
+ * @param options.verbose - Show extra detail
7
+ */
8
+ export function streamProgress(stdout, reporter, options = {}) {
9
+ if (!reporter) {
10
+ // Legacy fallback: write directly to stdout (v1 behavior)
11
+ reporter = {
12
+ mode: 'human',
13
+ _write: (s) => process.stdout.write(s),
14
+ syncProgress: (data) => process.stdout.write(`\r\x1b[K \x1b[36m${data}\x1b[0m`),
15
+ syncFileChange: (type, filename) => {
16
+ if (type === 'delete') {
17
+ process.stdout.write(`\r\x1b[K \x1b[33m- ${filename}\x1b[0m\n`);
18
+ }
19
+ else {
20
+ process.stdout.write(`\r\x1b[K \x1b[32m+ ${filename}\x1b[0m\n`);
21
+ }
22
+ },
23
+ };
24
+ }
25
+ let buffer = '';
26
+ stdout.on('data', (chunk) => {
27
+ buffer += chunk.toString();
28
+ const lines = buffer.split('\n');
29
+ buffer = lines.pop();
30
+ for (const line of lines) {
31
+ const parts = line.split('\r');
32
+ const displayLine = parts[parts.length - 1];
33
+ if (!displayLine || displayLine.trim() === '')
34
+ continue;
35
+ if (/^\s/.test(displayLine)) {
36
+ reporter.syncProgress(displayLine.trim());
37
+ }
38
+ else {
39
+ const trimmed = displayLine.trim();
40
+ if (trimmed === './' || trimmed === '')
41
+ continue;
42
+ if (trimmed.startsWith('deleting ')) {
43
+ reporter.syncFileChange('delete', trimmed.slice(9));
44
+ }
45
+ else {
46
+ reporter.syncFileChange('add', trimmed);
47
+ }
48
+ }
49
+ }
50
+ });
51
+ stdout.on('end', () => {
52
+ if (buffer.trim()) {
53
+ const parts = buffer.split('\r');
54
+ const displayLine = parts[parts.length - 1]?.trim();
55
+ if (displayLine && displayLine !== './') {
56
+ reporter.syncFileChange('add', displayLine);
57
+ }
58
+ }
59
+ // Clear progress line in human mode
60
+ if (reporter.mode === 'human') {
61
+ reporter._write('\r\x1b[K');
62
+ }
63
+ });
64
+ }
65
+ //# sourceMappingURL=progress.js.map