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.
- package/.eslintrc.js +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +105 -0
- package/.github/workflows/publish.yml +269 -0
- package/.github/workflows/update-copyright-year.yml +70 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/icon.png +0 -0
- package/eslint.config.js +23 -0
- package/metadata/browse-remote-path.png +0 -0
- package/metadata/browse-remote.png +0 -0
- package/metadata/download-local-path.png +0 -0
- package/metadata/download-remote-path.png +0 -0
- package/metadata/extension.png +0 -0
- package/metadata/upload-local-path.png +0 -0
- package/metadata/upload-remote-path.png +0 -0
- package/metadata/upload-search-host.png +0 -0
- package/package.json +93 -0
- package/src/__mocks__/raycast-api.ts +84 -0
- package/src/browse.tsx +378 -0
- package/src/components/FileList.test.tsx +73 -0
- package/src/components/FileList.tsx +61 -0
- package/src/download.tsx +353 -0
- package/src/e2e/browse.e2e.test.ts +295 -0
- package/src/e2e/download.e2e.test.ts +193 -0
- package/src/e2e/error-handling.e2e.test.ts +292 -0
- package/src/e2e/rsync-options.e2e.test.ts +348 -0
- package/src/e2e/upload.e2e.test.ts +207 -0
- package/src/index.tsx +21 -0
- package/src/test-setup.ts +1 -0
- package/src/types/server.ts +60 -0
- package/src/upload.tsx +404 -0
- package/src/utils/__tests__/sshConfig.test.ts +352 -0
- package/src/utils/__tests__/validation.test.ts +139 -0
- package/src/utils/preferences.ts +24 -0
- package/src/utils/rsync.test.ts +490 -0
- package/src/utils/rsync.ts +517 -0
- package/src/utils/shellEscape.test.ts +98 -0
- package/src/utils/shellEscape.ts +36 -0
- package/src/utils/ssh.test.ts +209 -0
- package/src/utils/ssh.ts +187 -0
- package/src/utils/sshConfig.test.ts +191 -0
- package/src/utils/sshConfig.ts +212 -0
- package/src/utils/validation.test.ts +224 -0
- package/src/utils/validation.ts +115 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import {
|
|
4
|
+
getSSHConfigPath,
|
|
5
|
+
parseSSHConfig,
|
|
6
|
+
getHostConfig,
|
|
7
|
+
clearCache,
|
|
8
|
+
parseConfigContent,
|
|
9
|
+
} from "../sshConfig";
|
|
10
|
+
|
|
11
|
+
// Mock os module
|
|
12
|
+
vi.mock("node:os", async () => {
|
|
13
|
+
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
homedir: vi.fn(() => "/home/user"),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Mock fs module
|
|
21
|
+
// Define state variables that will be accessed by mocks
|
|
22
|
+
let mockFileExists = false;
|
|
23
|
+
let mockFileContent = "";
|
|
24
|
+
let mockFileStats: { mtimeMs: number } | null = null;
|
|
25
|
+
|
|
26
|
+
// Create mock functions inside the factory to avoid hoisting issues
|
|
27
|
+
vi.mock("node:fs", async () => {
|
|
28
|
+
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
29
|
+
const mockExistsSync = vi.fn(() => mockFileExists);
|
|
30
|
+
const mockReadFileSync = vi.fn(() => mockFileContent);
|
|
31
|
+
const mockStatSync = vi.fn(() => {
|
|
32
|
+
if (!mockFileStats) {
|
|
33
|
+
throw new Error("File not found");
|
|
34
|
+
}
|
|
35
|
+
return mockFileStats as any;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
existsSync: mockExistsSync,
|
|
41
|
+
readFileSync: mockReadFileSync,
|
|
42
|
+
statSync: mockStatSync,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Import fs after mocking to get the mocked functions
|
|
47
|
+
import * as fs from "node:fs";
|
|
48
|
+
|
|
49
|
+
describe("sshConfig", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
clearCache();
|
|
53
|
+
mockFileExists = false;
|
|
54
|
+
mockFileContent = "";
|
|
55
|
+
mockFileStats = null;
|
|
56
|
+
vi.mocked(fs.existsSync).mockClear();
|
|
57
|
+
vi.mocked(fs.readFileSync).mockClear();
|
|
58
|
+
vi.mocked(fs.statSync).mockClear();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("getSSHConfigPath", () => {
|
|
66
|
+
it("should return the correct SSH config path", () => {
|
|
67
|
+
const path = getSSHConfigPath();
|
|
68
|
+
expect(path).toBe(join("/home/user", ".ssh", "config"));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("parseConfigContent", () => {
|
|
73
|
+
it("should parse simple host configuration", () => {
|
|
74
|
+
const content = `
|
|
75
|
+
Host myserver
|
|
76
|
+
HostName example.com
|
|
77
|
+
User myuser
|
|
78
|
+
Port 22
|
|
79
|
+
`;
|
|
80
|
+
const hosts = parseConfigContent(content);
|
|
81
|
+
expect(hosts).toHaveLength(1);
|
|
82
|
+
expect(hosts[0]).toMatchObject({
|
|
83
|
+
host: "myserver",
|
|
84
|
+
hostName: "example.com",
|
|
85
|
+
user: "myuser",
|
|
86
|
+
port: 22,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should parse multiple hosts", () => {
|
|
91
|
+
const content = `
|
|
92
|
+
Host server1
|
|
93
|
+
HostName server1.example.com
|
|
94
|
+
User user1
|
|
95
|
+
|
|
96
|
+
Host server2
|
|
97
|
+
HostName server2.example.com
|
|
98
|
+
User user2
|
|
99
|
+
Port 2222
|
|
100
|
+
`;
|
|
101
|
+
const hosts = parseConfigContent(content);
|
|
102
|
+
expect(hosts).toHaveLength(2);
|
|
103
|
+
expect(hosts[0].host).toBe("server1");
|
|
104
|
+
expect(hosts[1].host).toBe("server2");
|
|
105
|
+
expect(hosts[1].port).toBe(2222);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should skip wildcard hosts", () => {
|
|
109
|
+
const content = `
|
|
110
|
+
Host *
|
|
111
|
+
User default
|
|
112
|
+
|
|
113
|
+
Host myserver
|
|
114
|
+
HostName example.com
|
|
115
|
+
`;
|
|
116
|
+
const hosts = parseConfigContent(content);
|
|
117
|
+
expect(hosts).toHaveLength(1);
|
|
118
|
+
expect(hosts[0].host).toBe("myserver");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should handle multiple host aliases", () => {
|
|
122
|
+
const content = `
|
|
123
|
+
Host server1 server2 server3
|
|
124
|
+
HostName example.com
|
|
125
|
+
User myuser
|
|
126
|
+
`;
|
|
127
|
+
const hosts = parseConfigContent(content);
|
|
128
|
+
expect(hosts).toHaveLength(3);
|
|
129
|
+
expect(hosts.map((h) => h.host)).toEqual([
|
|
130
|
+
"server1",
|
|
131
|
+
"server2",
|
|
132
|
+
"server3",
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should parse identity file and expand ~", () => {
|
|
137
|
+
const content = `
|
|
138
|
+
Host myserver
|
|
139
|
+
HostName example.com
|
|
140
|
+
IdentityFile ~/.ssh/id_rsa
|
|
141
|
+
`;
|
|
142
|
+
const hosts = parseConfigContent(content);
|
|
143
|
+
expect(hosts[0].identityFile).toBe("/home/user/.ssh/id_rsa");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should parse proxy jump", () => {
|
|
147
|
+
const content = `
|
|
148
|
+
Host myserver
|
|
149
|
+
HostName example.com
|
|
150
|
+
ProxyJump jumpbox
|
|
151
|
+
`;
|
|
152
|
+
const hosts = parseConfigContent(content);
|
|
153
|
+
expect(hosts[0].proxyJump).toBe("jumpbox");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should skip comments and empty lines", () => {
|
|
157
|
+
const content = `
|
|
158
|
+
# This is a comment
|
|
159
|
+
Host myserver
|
|
160
|
+
# Another comment
|
|
161
|
+
HostName example.com
|
|
162
|
+
User myuser
|
|
163
|
+
|
|
164
|
+
Host server2
|
|
165
|
+
HostName server2.com
|
|
166
|
+
`;
|
|
167
|
+
const hosts = parseConfigContent(content);
|
|
168
|
+
expect(hosts).toHaveLength(2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle global configuration", () => {
|
|
172
|
+
// Note: The current implementation doesn't support global configuration
|
|
173
|
+
// This test documents the expected behavior if global config support is added
|
|
174
|
+
const content = `
|
|
175
|
+
User globaluser
|
|
176
|
+
Port 2222
|
|
177
|
+
|
|
178
|
+
Host myserver
|
|
179
|
+
HostName example.com
|
|
180
|
+
`;
|
|
181
|
+
const hosts = parseConfigContent(content);
|
|
182
|
+
// Global config is not currently supported, so user and port will be undefined
|
|
183
|
+
// This test verifies the current behavior
|
|
184
|
+
expect(hosts[0].hostName).toBe("example.com");
|
|
185
|
+
// If global config support is added, uncomment these:
|
|
186
|
+
// expect(hosts[0].user).toBe("globaluser");
|
|
187
|
+
// expect(hosts[0].port).toBe(2222);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should handle case-insensitive properties", () => {
|
|
191
|
+
const content = `
|
|
192
|
+
Host myserver
|
|
193
|
+
HOSTNAME example.com
|
|
194
|
+
USER myuser
|
|
195
|
+
PORT 2222
|
|
196
|
+
`;
|
|
197
|
+
const hosts = parseConfigContent(content);
|
|
198
|
+
expect(hosts[0].hostName).toBe("example.com");
|
|
199
|
+
expect(hosts[0].user).toBe("myuser");
|
|
200
|
+
expect(hosts[0].port).toBe(2222);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should handle invalid port gracefully", () => {
|
|
204
|
+
const content = `
|
|
205
|
+
Host myserver
|
|
206
|
+
HostName example.com
|
|
207
|
+
Port invalid
|
|
208
|
+
`;
|
|
209
|
+
const hosts = parseConfigContent(content);
|
|
210
|
+
expect(hosts[0].port).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should return empty array for empty config", () => {
|
|
214
|
+
const hosts = parseConfigContent("");
|
|
215
|
+
expect(hosts).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should handle tabs and spaces for indentation", () => {
|
|
219
|
+
const content = `
|
|
220
|
+
Host myserver
|
|
221
|
+
HostName example.com
|
|
222
|
+
User myuser
|
|
223
|
+
`;
|
|
224
|
+
const hosts = parseConfigContent(content);
|
|
225
|
+
expect(hosts).toHaveLength(1);
|
|
226
|
+
expect(hosts[0].hostName).toBe("example.com");
|
|
227
|
+
expect(hosts[0].user).toBe("myuser");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("parseSSHConfig", () => {
|
|
232
|
+
it("should return empty array if config file does not exist", () => {
|
|
233
|
+
mockFileExists = false;
|
|
234
|
+
clearCache();
|
|
235
|
+
const hosts = parseSSHConfig();
|
|
236
|
+
expect(hosts).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should parse existing config file", () => {
|
|
240
|
+
const configContent = `
|
|
241
|
+
Host myserver
|
|
242
|
+
HostName example.com
|
|
243
|
+
User myuser
|
|
244
|
+
`;
|
|
245
|
+
clearCache();
|
|
246
|
+
mockFileExists = true;
|
|
247
|
+
mockFileContent = configContent;
|
|
248
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
249
|
+
// Reset the mock implementation to return the content
|
|
250
|
+
vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent);
|
|
251
|
+
|
|
252
|
+
const hosts = parseSSHConfig();
|
|
253
|
+
expect(hosts).toHaveLength(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should cache parsed config", () => {
|
|
257
|
+
const configContent = `
|
|
258
|
+
Host myserver
|
|
259
|
+
HostName example.com
|
|
260
|
+
`;
|
|
261
|
+
clearCache();
|
|
262
|
+
mockFileExists = true;
|
|
263
|
+
mockFileContent = configContent;
|
|
264
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
265
|
+
vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent);
|
|
266
|
+
vi.mocked(fs.readFileSync).mockClear();
|
|
267
|
+
|
|
268
|
+
parseSSHConfig();
|
|
269
|
+
parseSSHConfig();
|
|
270
|
+
|
|
271
|
+
// Should only read file once due to caching
|
|
272
|
+
expect(vi.mocked(fs.readFileSync)).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle read errors gracefully", () => {
|
|
276
|
+
// Suppress console.error for this test
|
|
277
|
+
const consoleSpy = vi
|
|
278
|
+
.spyOn(console, "error")
|
|
279
|
+
.mockImplementation(() => {});
|
|
280
|
+
clearCache();
|
|
281
|
+
|
|
282
|
+
mockFileExists = true;
|
|
283
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
284
|
+
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
285
|
+
throw new Error("Permission denied");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const hosts = parseSSHConfig();
|
|
289
|
+
expect(hosts).toHaveLength(0);
|
|
290
|
+
|
|
291
|
+
consoleSpy.mockRestore();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("getHostConfig", () => {
|
|
296
|
+
it("should return host config by alias", () => {
|
|
297
|
+
const configContent = `
|
|
298
|
+
Host myserver
|
|
299
|
+
HostName example.com
|
|
300
|
+
User myuser
|
|
301
|
+
`;
|
|
302
|
+
clearCache();
|
|
303
|
+
mockFileExists = true;
|
|
304
|
+
mockFileContent = configContent;
|
|
305
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
306
|
+
// Reset the mock implementation to return the content
|
|
307
|
+
vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent);
|
|
308
|
+
|
|
309
|
+
const host = getHostConfig("myserver");
|
|
310
|
+
expect(host).toBeDefined();
|
|
311
|
+
expect(host?.host).toBe("myserver");
|
|
312
|
+
expect(host?.hostName).toBe("example.com");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should return null for non-existent host", () => {
|
|
316
|
+
const configContent = `
|
|
317
|
+
Host myserver
|
|
318
|
+
HostName example.com
|
|
319
|
+
`;
|
|
320
|
+
clearCache();
|
|
321
|
+
mockFileExists = true;
|
|
322
|
+
mockFileContent = configContent;
|
|
323
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
324
|
+
vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent);
|
|
325
|
+
|
|
326
|
+
const host = getHostConfig("nonexistent");
|
|
327
|
+
expect(host).toBeNull();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("clearCache", () => {
|
|
332
|
+
it("should clear the cache", () => {
|
|
333
|
+
const configContent = `
|
|
334
|
+
Host myserver
|
|
335
|
+
HostName example.com
|
|
336
|
+
`;
|
|
337
|
+
clearCache();
|
|
338
|
+
mockFileExists = true;
|
|
339
|
+
mockFileContent = configContent;
|
|
340
|
+
mockFileStats = { mtimeMs: 1234567890 };
|
|
341
|
+
vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent);
|
|
342
|
+
vi.mocked(fs.readFileSync).mockClear();
|
|
343
|
+
|
|
344
|
+
parseSSHConfig();
|
|
345
|
+
clearCache();
|
|
346
|
+
parseSSHConfig();
|
|
347
|
+
|
|
348
|
+
// After clearing cache, should read file again
|
|
349
|
+
expect(vi.mocked(fs.readFileSync)).toHaveBeenCalledTimes(2);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
validatePort,
|
|
5
|
+
validateLocalPath,
|
|
6
|
+
validateRemotePath,
|
|
7
|
+
validateHostConfig,
|
|
8
|
+
} from "../validation";
|
|
9
|
+
import { SSHHostConfig } from "../../types/server";
|
|
10
|
+
|
|
11
|
+
// Type definitions for mocked fs module
|
|
12
|
+
type MockedFS = typeof fs & {
|
|
13
|
+
__setMockFileExists: (exists: boolean) => void;
|
|
14
|
+
__setMockStats: (stats: fs.Stats | null) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Mock fs module
|
|
18
|
+
vi.mock("node:fs", async () => {
|
|
19
|
+
const actual = await vi.importActual<typeof fs>("node:fs");
|
|
20
|
+
let mockFileExists = false;
|
|
21
|
+
let mockStats: fs.Stats | null = null;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
existsSync: vi.fn(() => mockFileExists),
|
|
26
|
+
statSync: vi.fn(() => {
|
|
27
|
+
if (!mockStats) {
|
|
28
|
+
throw new Error("File not found");
|
|
29
|
+
}
|
|
30
|
+
return mockStats;
|
|
31
|
+
}),
|
|
32
|
+
__setMockFileExists: (exists: boolean) => {
|
|
33
|
+
mockFileExists = exists;
|
|
34
|
+
},
|
|
35
|
+
__setMockStats: (stats: fs.Stats | null) => {
|
|
36
|
+
mockStats = stats;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("validation", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
(fs as unknown as MockedFS).__setMockFileExists(false);
|
|
45
|
+
(fs as unknown as MockedFS).__setMockStats(null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("validatePort", () => {
|
|
53
|
+
it("should return valid for valid ports", () => {
|
|
54
|
+
expect(validatePort(22).valid).toBe(true);
|
|
55
|
+
expect(validatePort(80).valid).toBe(true);
|
|
56
|
+
expect(validatePort(443).valid).toBe(true);
|
|
57
|
+
expect(validatePort(8080).valid).toBe(true);
|
|
58
|
+
expect(validatePort(65535).valid).toBe(true);
|
|
59
|
+
expect(validatePort(1).valid).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return invalid for invalid ports", () => {
|
|
63
|
+
expect(validatePort(0).valid).toBe(false);
|
|
64
|
+
expect(validatePort(-1).valid).toBe(false);
|
|
65
|
+
expect(validatePort(65536).valid).toBe(false);
|
|
66
|
+
expect(validatePort(100000).valid).toBe(false);
|
|
67
|
+
expect(validatePort(22.5).valid).toBe(false);
|
|
68
|
+
expect(validatePort(NaN).valid).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("validateLocalPath", () => {
|
|
73
|
+
it("should return valid if path exists", () => {
|
|
74
|
+
(fs as unknown as MockedFS).__setMockFileExists(true);
|
|
75
|
+
const result = validateLocalPath("/valid/path");
|
|
76
|
+
expect(result.valid).toBe(true);
|
|
77
|
+
expect(result.error).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should return invalid if path does not exist", () => {
|
|
81
|
+
(fs as unknown as MockedFS).__setMockFileExists(false);
|
|
82
|
+
const result = validateLocalPath("/invalid/path");
|
|
83
|
+
expect(result.valid).toBe(false);
|
|
84
|
+
expect(result.error).toBe("File not found");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return invalid for empty paths", () => {
|
|
88
|
+
const result = validateLocalPath("");
|
|
89
|
+
expect(result.valid).toBe(false);
|
|
90
|
+
expect(result.error).toBe("Local path cannot be empty");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle whitespace", () => {
|
|
94
|
+
(fs as unknown as MockedFS).__setMockFileExists(true);
|
|
95
|
+
const result = validateLocalPath(" /path ");
|
|
96
|
+
expect(result.valid).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("validateRemotePath", () => {
|
|
101
|
+
it("should return valid for non-empty paths", () => {
|
|
102
|
+
expect(validateRemotePath("/home/user").valid).toBe(true);
|
|
103
|
+
expect(validateRemotePath("file.txt").valid).toBe(true);
|
|
104
|
+
expect(validateRemotePath("/path/to/file").valid).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should return invalid for empty paths", () => {
|
|
108
|
+
expect(validateRemotePath("").valid).toBe(false);
|
|
109
|
+
expect(validateRemotePath(" ").valid).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should handle whitespace", () => {
|
|
113
|
+
expect(validateRemotePath(" /path ").valid).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("validateHostConfig", () => {
|
|
118
|
+
it("should return valid for complete config", () => {
|
|
119
|
+
const config: SSHHostConfig = {
|
|
120
|
+
host: "server1",
|
|
121
|
+
hostName: "example.com",
|
|
122
|
+
user: "admin",
|
|
123
|
+
port: 22,
|
|
124
|
+
};
|
|
125
|
+
const result = validateHostConfig(config);
|
|
126
|
+
expect(result.valid).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return invalid for missing host alias", () => {
|
|
130
|
+
const config: SSHHostConfig = {
|
|
131
|
+
host: "",
|
|
132
|
+
hostName: "example.com",
|
|
133
|
+
};
|
|
134
|
+
const result = validateHostConfig(config);
|
|
135
|
+
expect(result.valid).toBe(false);
|
|
136
|
+
expect(result.error).toBe("Host alias is required");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getPreferenceValues } from "@raycast/api";
|
|
2
|
+
import { RsyncOptions } from "../types/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Preferences interface matching package.json preferences
|
|
6
|
+
*/
|
|
7
|
+
export interface RsyncPreferences {
|
|
8
|
+
rsyncHumanReadable: boolean;
|
|
9
|
+
rsyncProgress: boolean;
|
|
10
|
+
rsyncDelete: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get rsync preferences from Raycast preferences
|
|
15
|
+
* @returns RsyncOptions object derived from preferences
|
|
16
|
+
*/
|
|
17
|
+
export function getRsyncPreferences(): RsyncOptions {
|
|
18
|
+
const preferences = getPreferenceValues<RsyncPreferences>();
|
|
19
|
+
return {
|
|
20
|
+
humanReadable: preferences.rsyncHumanReadable,
|
|
21
|
+
progress: preferences.rsyncProgress,
|
|
22
|
+
delete: preferences.rsyncDelete,
|
|
23
|
+
};
|
|
24
|
+
}
|