raycast-rsync-extension 1.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 (49) hide show
  1. package/.eslintrc.js +18 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  3. package/.github/dependabot.yml +35 -0
  4. package/.github/workflows/ci.yml +105 -0
  5. package/.github/workflows/publish.yml +269 -0
  6. package/.github/workflows/update-copyright-year.yml +70 -0
  7. package/CHANGELOG.md +7 -0
  8. package/LICENSE +21 -0
  9. package/README.md +81 -0
  10. package/assets/icon.png +0 -0
  11. package/eslint.config.js +23 -0
  12. package/metadata/browse-remote-path.png +0 -0
  13. package/metadata/browse-remote.png +0 -0
  14. package/metadata/download-local-path.png +0 -0
  15. package/metadata/download-remote-path.png +0 -0
  16. package/metadata/extension.png +0 -0
  17. package/metadata/upload-local-path.png +0 -0
  18. package/metadata/upload-remote-path.png +0 -0
  19. package/metadata/upload-search-host.png +0 -0
  20. package/package.json +93 -0
  21. package/src/__mocks__/raycast-api.ts +84 -0
  22. package/src/browse.tsx +378 -0
  23. package/src/components/FileList.test.tsx +73 -0
  24. package/src/components/FileList.tsx +61 -0
  25. package/src/download.tsx +353 -0
  26. package/src/e2e/browse.e2e.test.ts +295 -0
  27. package/src/e2e/download.e2e.test.ts +193 -0
  28. package/src/e2e/error-handling.e2e.test.ts +292 -0
  29. package/src/e2e/rsync-options.e2e.test.ts +348 -0
  30. package/src/e2e/upload.e2e.test.ts +207 -0
  31. package/src/index.tsx +21 -0
  32. package/src/test-setup.ts +1 -0
  33. package/src/types/server.ts +60 -0
  34. package/src/upload.tsx +404 -0
  35. package/src/utils/__tests__/sshConfig.test.ts +352 -0
  36. package/src/utils/__tests__/validation.test.ts +139 -0
  37. package/src/utils/preferences.ts +24 -0
  38. package/src/utils/rsync.test.ts +490 -0
  39. package/src/utils/rsync.ts +517 -0
  40. package/src/utils/shellEscape.test.ts +98 -0
  41. package/src/utils/shellEscape.ts +36 -0
  42. package/src/utils/ssh.test.ts +209 -0
  43. package/src/utils/ssh.ts +187 -0
  44. package/src/utils/sshConfig.test.ts +191 -0
  45. package/src/utils/sshConfig.ts +212 -0
  46. package/src/utils/validation.test.ts +224 -0
  47. package/src/utils/validation.ts +115 -0
  48. package/tsconfig.json +27 -0
  49. package/vitest.config.ts +8 -0
@@ -0,0 +1,348 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildRsyncCommand } from "../utils/rsync";
3
+ import {
4
+ TransferOptions,
5
+ TransferDirection,
6
+ SSHHostConfig,
7
+ } from "../types/server";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+
11
+ describe("Rsync Options E2E", () => {
12
+ const mockHostConfig: SSHHostConfig = {
13
+ host: "testserver",
14
+ hostName: "test.example.com",
15
+ user: "testuser",
16
+ port: 22,
17
+ };
18
+
19
+ const configPath = join(homedir(), ".ssh", "config");
20
+
21
+ describe("Rsync flags and options", () => {
22
+ it("should include base flags (-avz) by default", () => {
23
+ const options: TransferOptions = {
24
+ hostConfig: mockHostConfig,
25
+ localPath: "/local/path",
26
+ remotePath: "/remote/path",
27
+ direction: TransferDirection.UPLOAD,
28
+ };
29
+
30
+ const command = buildRsyncCommand(options);
31
+
32
+ // Base flags are combined into -avz
33
+ expect(command).toContain("-avz");
34
+ // Verify individual flags are present in the combined string
35
+ expect(command).toMatch(/-[avz]+/);
36
+ // Command now uses single quotes for escaping
37
+ const flagsMatch = command.match(/rsync -e '[^']+' (-[^ ]+)/);
38
+ expect(flagsMatch).not.toBeNull();
39
+ const flags = flagsMatch![1];
40
+ expect(flags).toContain("a"); // archive
41
+ expect(flags).toContain("v"); // verbose
42
+ expect(flags).toContain("z"); // compress
43
+ });
44
+
45
+ it("should include human-readable flag when enabled", () => {
46
+ const options: TransferOptions = {
47
+ hostConfig: mockHostConfig,
48
+ localPath: "/local/path",
49
+ remotePath: "/remote/path",
50
+ direction: TransferDirection.UPLOAD,
51
+ rsyncOptions: {
52
+ humanReadable: true,
53
+ },
54
+ };
55
+
56
+ const command = buildRsyncCommand(options);
57
+
58
+ // Flag should be included in the combined flags string (e.g., -avzh)
59
+ expect(command).toMatch(/-[avz]+h/);
60
+ });
61
+
62
+ it("should include progress flag when enabled", () => {
63
+ const options: TransferOptions = {
64
+ hostConfig: mockHostConfig,
65
+ localPath: "/local/path",
66
+ remotePath: "/remote/path",
67
+ direction: TransferDirection.UPLOAD,
68
+ rsyncOptions: {
69
+ progress: true,
70
+ },
71
+ };
72
+
73
+ const command = buildRsyncCommand(options);
74
+
75
+ // Flag should be included in the combined flags string (e.g., -avzP)
76
+ expect(command).toMatch(/-[avz]+P/);
77
+ });
78
+
79
+ it("should include delete flag when enabled", () => {
80
+ const options: TransferOptions = {
81
+ hostConfig: mockHostConfig,
82
+ localPath: "/local/path",
83
+ remotePath: "/remote/path",
84
+ direction: TransferDirection.UPLOAD,
85
+ rsyncOptions: {
86
+ delete: true,
87
+ },
88
+ };
89
+
90
+ const command = buildRsyncCommand(options);
91
+
92
+ expect(command).toContain("--delete");
93
+ });
94
+
95
+ it("should include all optional flags when all enabled", () => {
96
+ const options: TransferOptions = {
97
+ hostConfig: mockHostConfig,
98
+ localPath: "/local/path",
99
+ remotePath: "/remote/path",
100
+ direction: TransferDirection.DOWNLOAD,
101
+ rsyncOptions: {
102
+ humanReadable: true,
103
+ progress: true,
104
+ delete: true,
105
+ },
106
+ };
107
+
108
+ const command = buildRsyncCommand(options);
109
+
110
+ // Combined short flags should include h and P (e.g., -avzhP)
111
+ expect(command).toMatch(/-[avz]+hP/);
112
+ expect(command).toContain("--delete");
113
+ // Base flags should be present
114
+ expect(command).toMatch(/-[avz]+/);
115
+ });
116
+
117
+ it("should not include optional flags when disabled", () => {
118
+ const options: TransferOptions = {
119
+ hostConfig: mockHostConfig,
120
+ localPath: "/local/path",
121
+ remotePath: "/remote/path",
122
+ direction: TransferDirection.UPLOAD,
123
+ rsyncOptions: {
124
+ humanReadable: false,
125
+ progress: false,
126
+ delete: false,
127
+ },
128
+ };
129
+
130
+ const command = buildRsyncCommand(options);
131
+
132
+ // Extract the flags part (between -e and paths)
133
+ // Command now uses single quotes for escaping
134
+ const flagsMatch = command.match(/rsync -e '[^']+' (-[^ ]+)/);
135
+ expect(flagsMatch).not.toBeNull();
136
+ const flags = flagsMatch![1];
137
+
138
+ // Optional flags should not be present
139
+ expect(flags).not.toContain("h");
140
+ expect(flags).not.toContain("P");
141
+ expect(command).not.toContain("--delete");
142
+ // Base flags should still be present
143
+ expect(flags).toContain("a");
144
+ expect(flags).toContain("v");
145
+ expect(flags).toContain("z");
146
+ });
147
+ });
148
+
149
+ describe("Progress and output message handling", () => {
150
+ it("should build command with progress flag for real-time updates", () => {
151
+ const options: TransferOptions = {
152
+ hostConfig: mockHostConfig,
153
+ localPath: "/local/file.txt",
154
+ remotePath: "/remote/file.txt",
155
+ direction: TransferDirection.UPLOAD,
156
+ rsyncOptions: {
157
+ progress: true,
158
+ humanReadable: true,
159
+ },
160
+ };
161
+
162
+ const command = buildRsyncCommand(options);
163
+
164
+ // Should include both progress and human-readable flags
165
+ expect(command).toMatch(/-[avz]+hP/);
166
+ // Paths are now escaped with single quotes
167
+ expect(command).toContain("'/local/file.txt'");
168
+ expect(command).toContain("'testserver':");
169
+ expect(command).toContain("'/remote/file.txt'");
170
+ });
171
+
172
+ it("should handle upload with all options enabled", () => {
173
+ const options: TransferOptions = {
174
+ hostConfig: mockHostConfig,
175
+ localPath: "/local/directory",
176
+ remotePath: "/remote/directory",
177
+ direction: TransferDirection.UPLOAD,
178
+ rsyncOptions: {
179
+ humanReadable: true,
180
+ progress: true,
181
+ delete: true,
182
+ },
183
+ };
184
+
185
+ const command = buildRsyncCommand(options);
186
+
187
+ // Command now uses single quotes for escaping
188
+ expect(command).toMatch(
189
+ /^rsync -e 'ssh -F .+' -[avz]+hP --delete .+ 'testserver':.+$/,
190
+ );
191
+ expect(command).toContain("'/local/directory'");
192
+ expect(command).toContain("'/remote/directory'");
193
+ });
194
+
195
+ it("should handle download with all options enabled", () => {
196
+ const options: TransferOptions = {
197
+ hostConfig: mockHostConfig,
198
+ localPath: "/local/destination",
199
+ remotePath: "/remote/source",
200
+ direction: TransferDirection.DOWNLOAD,
201
+ rsyncOptions: {
202
+ humanReadable: true,
203
+ progress: true,
204
+ delete: true,
205
+ },
206
+ };
207
+
208
+ const command = buildRsyncCommand(options);
209
+
210
+ // Command now uses single quotes for escaping
211
+ expect(command).toMatch(
212
+ /^rsync -e 'ssh -F .+' -[avz]+hP --delete 'testserver':.+ .+$/,
213
+ );
214
+ expect(command).toContain("'testserver':");
215
+ expect(command).toContain("'/remote/source'");
216
+ // For downloads, local destination should have trailing slash to ensure directory is created
217
+ expect(command).toContain("'/local/destination/'");
218
+ });
219
+ });
220
+
221
+ describe("Rsync options workflow", () => {
222
+ it("should validate workflow with human-readable option", () => {
223
+ const options: TransferOptions = {
224
+ hostConfig: mockHostConfig,
225
+ localPath: "/local/path",
226
+ remotePath: "/remote/path",
227
+ direction: TransferDirection.UPLOAD,
228
+ rsyncOptions: {
229
+ humanReadable: true,
230
+ },
231
+ };
232
+
233
+ const command = buildRsyncCommand(options);
234
+ expect(command).toMatch(/-[avz]+h/);
235
+ expect(command).toContain(configPath);
236
+ });
237
+
238
+ it("should validate workflow with progress option", () => {
239
+ const options: TransferOptions = {
240
+ hostConfig: mockHostConfig,
241
+ localPath: "/local/path",
242
+ remotePath: "/remote/path",
243
+ direction: TransferDirection.DOWNLOAD,
244
+ rsyncOptions: {
245
+ progress: true,
246
+ },
247
+ };
248
+
249
+ const command = buildRsyncCommand(options);
250
+ expect(command).toMatch(/-[avz]+P/);
251
+ expect(command).toContain(configPath);
252
+ });
253
+
254
+ it("should validate workflow with delete option", () => {
255
+ const options: TransferOptions = {
256
+ hostConfig: mockHostConfig,
257
+ localPath: "/local/path",
258
+ remotePath: "/remote/path",
259
+ direction: TransferDirection.UPLOAD,
260
+ rsyncOptions: {
261
+ delete: true,
262
+ },
263
+ };
264
+
265
+ const command = buildRsyncCommand(options);
266
+ expect(command).toContain("--delete");
267
+ expect(command).toContain(configPath);
268
+ });
269
+
270
+ it("should handle partial options (only some enabled)", () => {
271
+ const options: TransferOptions = {
272
+ hostConfig: mockHostConfig,
273
+ localPath: "/local/path",
274
+ remotePath: "/remote/path",
275
+ direction: TransferDirection.UPLOAD,
276
+ rsyncOptions: {
277
+ humanReadable: true,
278
+ progress: false,
279
+ delete: false,
280
+ },
281
+ };
282
+
283
+ const command = buildRsyncCommand(options);
284
+ expect(command).toMatch(/-[avz]+h/);
285
+ expect(command).not.toContain("P");
286
+ expect(command).not.toContain("--delete");
287
+ });
288
+ });
289
+
290
+ describe("Command structure with options", () => {
291
+ it("should maintain correct command structure with options", () => {
292
+ const options: TransferOptions = {
293
+ hostConfig: mockHostConfig,
294
+ localPath: "/local/file.txt",
295
+ remotePath: "/remote/file.txt",
296
+ direction: TransferDirection.UPLOAD,
297
+ rsyncOptions: {
298
+ humanReadable: true,
299
+ progress: true,
300
+ },
301
+ };
302
+
303
+ const command = buildRsyncCommand(options);
304
+
305
+ // Verify command structure: rsync -e 'ssh -F config' flags source dest
306
+ // Command now uses single quotes for escaping
307
+ expect(command).toMatch(
308
+ /^rsync -e 'ssh -F .+' -[avz]+hP .+ 'testserver':.+$/,
309
+ );
310
+ });
311
+
312
+ it("should handle long-form flags correctly", () => {
313
+ const options: TransferOptions = {
314
+ hostConfig: mockHostConfig,
315
+ localPath: "/local/path",
316
+ remotePath: "/remote/path",
317
+ direction: TransferDirection.UPLOAD,
318
+ rsyncOptions: {
319
+ delete: true,
320
+ },
321
+ };
322
+
323
+ const command = buildRsyncCommand(options);
324
+
325
+ // --delete should appear after short flags
326
+ expect(command).toMatch(/-avz --delete/);
327
+ });
328
+
329
+ it("should combine short and long flags correctly", () => {
330
+ const options: TransferOptions = {
331
+ hostConfig: mockHostConfig,
332
+ localPath: "/local/path",
333
+ remotePath: "/remote/path",
334
+ direction: TransferDirection.DOWNLOAD,
335
+ rsyncOptions: {
336
+ humanReadable: true,
337
+ progress: true,
338
+ delete: true,
339
+ },
340
+ };
341
+
342
+ const command = buildRsyncCommand(options);
343
+
344
+ // Should have short flags combined, then long flags
345
+ expect(command).toMatch(/-[avz]+hP --delete/);
346
+ });
347
+ });
348
+ });
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import {
3
+ validateLocalPath,
4
+ validateRemotePath,
5
+ validateHostConfig,
6
+ } from "../utils/validation";
7
+ import { buildRsyncCommand } from "../utils/rsync";
8
+ import {
9
+ TransferDirection,
10
+ TransferOptions,
11
+ SSHHostConfig,
12
+ } from "../types/server";
13
+ import * as fs from "fs";
14
+ import * as os from "os";
15
+ import * as path from "path";
16
+
17
+ describe("Upload E2E Flow", () => {
18
+ let testLocalFile: string;
19
+ let testDir: string;
20
+
21
+ beforeAll(() => {
22
+ // Create test directory
23
+ testDir = path.join(os.tmpdir(), "rsync-e2e-test-" + Date.now());
24
+ fs.mkdirSync(testDir, { recursive: true });
25
+
26
+ // Create test local file
27
+ testLocalFile = path.join(testDir, "test-file.txt");
28
+ fs.writeFileSync(testLocalFile, "Test content for upload");
29
+ });
30
+
31
+ afterAll(() => {
32
+ // Clean up test directory
33
+ if (fs.existsSync(testDir)) {
34
+ fs.rmSync(testDir, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ it("should complete full upload workflow with valid inputs", () => {
39
+ // This test verifies the complete upload workflow
40
+ // After successful execution, the UI will call popToRoot() to close the extension
41
+
42
+ // Step 1: Create mock host config
43
+ const testHost: SSHHostConfig = {
44
+ host: "testserver",
45
+ hostName: "test.example.com",
46
+ user: "testuser",
47
+ port: 2222,
48
+ identityFile: "~/.ssh/test_key",
49
+ };
50
+
51
+ // Step 2: Validate local path
52
+ const localValidation = validateLocalPath(testLocalFile);
53
+ expect(localValidation.valid).toBe(true);
54
+ expect(localValidation.error).toBeUndefined();
55
+
56
+ // Step 3: Validate remote path
57
+ const remotePath = "/remote/destination/file.txt";
58
+ const remoteValidation = validateRemotePath(remotePath);
59
+ expect(remoteValidation.valid).toBe(true);
60
+ expect(remoteValidation.error).toBeUndefined();
61
+
62
+ // Step 4: Validate host config
63
+ const hostValidation = validateHostConfig(testHost);
64
+ expect(hostValidation.valid).toBe(true);
65
+ expect(hostValidation.error).toBeUndefined();
66
+
67
+ // Step 5: Build rsync command
68
+ const options: TransferOptions = {
69
+ hostConfig: testHost,
70
+ localPath: testLocalFile,
71
+ remotePath: remotePath,
72
+ direction: TransferDirection.UPLOAD,
73
+ };
74
+
75
+ const command = buildRsyncCommand(options);
76
+ expect(command).toContain("rsync");
77
+ expect(command).toContain("-a");
78
+ // Host alias is now escaped with single quotes
79
+ expect(command).toContain("'testserver':");
80
+ // Paths are now escaped with single quotes
81
+ expect(command).toContain(`'${testLocalFile}'`);
82
+ expect(command).toContain(`'${remotePath}'`);
83
+ });
84
+
85
+ it("should handle missing local file error in upload workflow", () => {
86
+ // Try to validate non-existent local path
87
+ const nonExistentPath = "/path/that/does/not/exist.txt";
88
+ const localValidation = validateLocalPath(nonExistentPath);
89
+
90
+ // Should fail validation
91
+ expect(localValidation.valid).toBe(false);
92
+ expect(localValidation.error).toBeDefined();
93
+ expect(localValidation.error).toContain("File not found");
94
+ });
95
+
96
+ it("should handle invalid remote path in upload workflow", () => {
97
+ // Validate local path (should pass)
98
+ const localValidation = validateLocalPath(testLocalFile);
99
+ expect(localValidation.valid).toBe(true);
100
+
101
+ // Try to validate invalid remote path (empty)
102
+ const remoteValidation = validateRemotePath("");
103
+
104
+ // Should fail validation
105
+ expect(remoteValidation.valid).toBe(false);
106
+ expect(remoteValidation.error).toBeDefined();
107
+ expect(remoteValidation.error).toContain("cannot be empty");
108
+ });
109
+
110
+ it("should handle invalid port in host config", () => {
111
+ // Create host with invalid port
112
+ const invalidHost: SSHHostConfig = {
113
+ host: "testserver",
114
+ hostName: "test.example.com",
115
+ port: 99999,
116
+ };
117
+
118
+ // Validate host config
119
+ const hostValidation = validateHostConfig(invalidHost);
120
+
121
+ // Should fail validation
122
+ expect(hostValidation.valid).toBe(false);
123
+ expect(hostValidation.error).toBeDefined();
124
+ expect(hostValidation.error).toContain("must be between 1 and 65535");
125
+ });
126
+
127
+ it("should validate all inputs before allowing transfer", () => {
128
+ const testHost: SSHHostConfig = {
129
+ host: "testserver",
130
+ hostName: "test.example.com",
131
+ user: "testuser",
132
+ port: 22,
133
+ };
134
+
135
+ // All validations should pass
136
+ const localValidation = validateLocalPath(testLocalFile);
137
+ const remoteValidation = validateRemotePath("/remote/path");
138
+ const hostValidation = validateHostConfig(testHost);
139
+
140
+ expect(localValidation.valid).toBe(true);
141
+ expect(remoteValidation.valid).toBe(true);
142
+ expect(hostValidation.valid).toBe(true);
143
+
144
+ // Should be able to create transfer options
145
+ const options: TransferOptions = {
146
+ hostConfig: testHost,
147
+ localPath: testLocalFile,
148
+ remotePath: "/remote/path",
149
+ direction: TransferDirection.UPLOAD,
150
+ };
151
+
152
+ expect(options).toBeDefined();
153
+ expect(options.direction).toBe(TransferDirection.UPLOAD);
154
+ });
155
+
156
+ it("should handle directory upload", () => {
157
+ // Create test directory with files
158
+ const testSubDir = path.join(testDir, "test-dir");
159
+ fs.mkdirSync(testSubDir, { recursive: true });
160
+ fs.writeFileSync(path.join(testSubDir, "file1.txt"), "Content 1");
161
+ fs.writeFileSync(path.join(testSubDir, "file2.txt"), "Content 2");
162
+
163
+ // Validate directory path
164
+ const localValidation = validateLocalPath(testSubDir);
165
+ expect(localValidation.valid).toBe(true);
166
+
167
+ // Create options for directory upload
168
+ const testHost: SSHHostConfig = {
169
+ host: "testserver",
170
+ hostName: "test.example.com",
171
+ };
172
+
173
+ const options: TransferOptions = {
174
+ hostConfig: testHost,
175
+ localPath: testSubDir,
176
+ remotePath: "/remote/dir",
177
+ direction: TransferDirection.UPLOAD,
178
+ };
179
+
180
+ const command = buildRsyncCommand(options);
181
+ expect(command).toContain("-a");
182
+ expect(command).toContain(testSubDir);
183
+ });
184
+
185
+ it("should build correct rsync command for upload", () => {
186
+ const testHost: SSHHostConfig = {
187
+ host: "production",
188
+ hostName: "prod.example.com",
189
+ user: "produser",
190
+ port: 2222,
191
+ };
192
+
193
+ const options: TransferOptions = {
194
+ hostConfig: testHost,
195
+ localPath: testLocalFile,
196
+ remotePath: "/var/www/file.txt",
197
+ direction: TransferDirection.UPLOAD,
198
+ };
199
+
200
+ const command = buildRsyncCommand(options);
201
+
202
+ // Verify command structure (now uses single quotes for escaping)
203
+ expect(command).toMatch(/^rsync -e 'ssh -F .+' -avz .+ 'production':.+$/);
204
+ expect(command).toContain(`'${testLocalFile}'`);
205
+ expect(command).toContain("'/var/www/file.txt'");
206
+ });
207
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Main entry point for the Raycast Rsync Extension
3
+ *
4
+ * This file serves as the central export point for all commands in the extension.
5
+ * The extension provides three main commands:
6
+ * 1. Upload Files via Rsync - Transfer files from local system to remote servers
7
+ * 2. Download Files via Rsync - Transfer files from remote servers to local system
8
+ * 3. Browse Remote Files - Browse and list files on remote servers
9
+ *
10
+ * All commands integrate with the user's SSH config file (~/.ssh/config) to
11
+ * provide a seamless experience for selecting and connecting to remote servers.
12
+ */
13
+
14
+ // Export upload command
15
+ export { default as upload } from "./upload";
16
+
17
+ // Export download command
18
+ export { default as download } from "./download";
19
+
20
+ // Export browse command
21
+ export { default as browse } from "./browse";
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * SSH host configuration parsed from ~/.ssh/config
3
+ */
4
+ export interface SSHHostConfig {
5
+ host: string; // Host alias from config
6
+ hostName?: string; // Actual hostname or IP
7
+ user?: string; // SSH username
8
+ port?: number; // SSH port (default 22)
9
+ identityFile?: string; // Path to SSH key
10
+ proxyJump?: string; // Jump host configuration
11
+ }
12
+
13
+ /**
14
+ * Direction of file transfer
15
+ */
16
+ export enum TransferDirection {
17
+ UPLOAD = "upload",
18
+ DOWNLOAD = "download",
19
+ }
20
+
21
+ /**
22
+ * Rsync-specific options
23
+ */
24
+ export interface RsyncOptions {
25
+ humanReadable?: boolean; // -h: human-readable file sizes
26
+ delete?: boolean; // --delete: delete extraneous files from destination
27
+ progress?: boolean; // -P: show progress and support partial transfers
28
+ }
29
+
30
+ /**
31
+ * Options for rsync transfer operation
32
+ */
33
+ export interface TransferOptions {
34
+ hostConfig: SSHHostConfig;
35
+ localPath: string;
36
+ remotePath: string;
37
+ direction: TransferDirection;
38
+ rsyncOptions?: RsyncOptions;
39
+ }
40
+
41
+ /**
42
+ * Result of rsync command execution
43
+ */
44
+ export interface RsyncResult {
45
+ success: boolean;
46
+ message: string;
47
+ stdout?: string; // rsync output messages
48
+ stderr?: string;
49
+ }
50
+
51
+ /**
52
+ * Remote file information from ls command
53
+ */
54
+ export interface RemoteFile {
55
+ name: string;
56
+ isDirectory: boolean;
57
+ size?: string;
58
+ permissions?: string;
59
+ modifiedDate?: string;
60
+ }