ssh-keyman 1.0.2 → 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.
@@ -0,0 +1,369 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { mockConsole, mockInquirer } = require("./helpers");
5
+
6
+ // Mock modules before importing commands
7
+ jest.mock("inquirer");
8
+ jest.mock("inquirer-autocomplete-prompt", () => jest.fn());
9
+
10
+ describe("commands", () => {
11
+ let testHomeDir;
12
+ let originalHomedir;
13
+ let consoleMock;
14
+ let SSH_PATH;
15
+ let KEYMAN_DIR_PATH;
16
+ let KEYMAN_PATH;
17
+
18
+ beforeAll(() => {
19
+ // Mock os.homedir to return test directory
20
+ originalHomedir = os.homedir;
21
+ testHomeDir = path.join(os.tmpdir(), `ssh-keyman-test-home-${Date.now()}`);
22
+ os.homedir = jest.fn(() => testHomeDir);
23
+ });
24
+
25
+ afterAll(() => {
26
+ os.homedir = originalHomedir;
27
+ });
28
+
29
+ beforeEach(() => {
30
+ // Clean up test directory
31
+ if (fs.existsSync(testHomeDir)) {
32
+ fs.rmSync(testHomeDir, { recursive: true, force: true });
33
+ }
34
+ fs.mkdirSync(testHomeDir, { recursive: true });
35
+
36
+ SSH_PATH = path.join(testHomeDir, ".ssh");
37
+ KEYMAN_DIR_PATH = path.join(testHomeDir, ".sshkeyman");
38
+ KEYMAN_PATH = path.join(KEYMAN_DIR_PATH, ".sshkeyman");
39
+
40
+ // Create mock SSH directory
41
+ fs.mkdirSync(SSH_PATH, { recursive: true });
42
+ fs.writeFileSync(path.join(SSH_PATH, "id_rsa"), "mock private key");
43
+ fs.writeFileSync(path.join(SSH_PATH, "id_rsa.pub"), "mock public key");
44
+
45
+ consoleMock = mockConsole();
46
+
47
+ // Clear module cache to get fresh imports with mocked homedir
48
+ jest.resetModules();
49
+ });
50
+
51
+ afterEach(() => {
52
+ consoleMock.restore();
53
+ if (fs.existsSync(testHomeDir)) {
54
+ fs.rmSync(testHomeDir, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ describe("init", () => {
59
+ it("should initialize ssh-keyman directory structure", () => {
60
+ const commands = require("../commands");
61
+
62
+ commands.init();
63
+
64
+ expect(fs.existsSync(KEYMAN_DIR_PATH)).toBe(true);
65
+ expect(fs.existsSync(KEYMAN_PATH)).toBe(true);
66
+ expect(fs.existsSync(path.join(KEYMAN_DIR_PATH, "default"))).toBe(true);
67
+
68
+ const keymanContent = JSON.parse(fs.readFileSync(KEYMAN_PATH, "utf8"));
69
+ expect(keymanContent).toEqual({
70
+ active: "default",
71
+ available: ["default"],
72
+ });
73
+ });
74
+
75
+ it("should not reinitialize if already initialized", () => {
76
+ const commands = require("../commands");
77
+
78
+ commands.init();
79
+ const logs1 = [...consoleMock.getLogs()];
80
+ consoleMock.clear();
81
+
82
+ commands.init();
83
+ const logs2 = consoleMock.getLogs();
84
+
85
+ expect(logs2.some(log => log.includes("already initialized"))).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe("help", () => {
90
+ it("should display help information", () => {
91
+ const commands = require("../commands");
92
+
93
+ commands.help();
94
+
95
+ const logs = consoleMock.getLogs().join("\n");
96
+ expect(logs).toContain("SSH KeyMan");
97
+ expect(logs).toContain("Usage:");
98
+ expect(logs).toContain("Commands:");
99
+ });
100
+ });
101
+
102
+ describe("version", () => {
103
+ it("should display version information", () => {
104
+ const commands = require("../commands");
105
+
106
+ commands.version();
107
+
108
+ const logs = consoleMock.getLogs().join("\n");
109
+ expect(logs).toContain("ssh-keyman");
110
+ expect(logs).toContain("version");
111
+ });
112
+ });
113
+
114
+ describe("list", () => {
115
+ it("should show error when not initialized", () => {
116
+ const commands = require("../commands");
117
+
118
+ commands.list();
119
+
120
+ const logs = consoleMock.getLogs().join("\n");
121
+ expect(logs).toContain("not initialized");
122
+ });
123
+
124
+ it("should list available environments when initialized", () => {
125
+ const commands = require("../commands");
126
+
127
+ commands.init();
128
+
129
+ // Reload module to pick up new keyman content
130
+ jest.resetModules();
131
+ const commandsReloaded = require("../commands");
132
+ consoleMock.clear();
133
+
134
+ commandsReloaded.list();
135
+
136
+ const logs = consoleMock.getLogs().join("\n");
137
+ expect(logs).toContain("Available environments");
138
+ expect(logs).toContain("default");
139
+ expect(logs).toContain("active");
140
+ });
141
+ });
142
+
143
+ describe("create", () => {
144
+ it("should show error when not initialized", async () => {
145
+ const commands = require("../commands");
146
+
147
+ await commands.create("test-env");
148
+
149
+ const logs = consoleMock.getLogs().join("\n");
150
+ expect(logs).toContain("not initialized");
151
+ });
152
+
153
+ it("should create new environment with name provided", async () => {
154
+ let inquirer = require("inquirer");
155
+ inquirer.prompt = jest.fn(() =>
156
+ Promise.resolve({ switchNow: false })
157
+ );
158
+
159
+ const commands = require("../commands");
160
+ commands.init();
161
+
162
+ // Reload to pick up initialized state
163
+ jest.resetModules();
164
+ inquirer = require("inquirer");
165
+ inquirer.prompt = jest.fn(() =>
166
+ Promise.resolve({ switchNow: false })
167
+ );
168
+ const commandsReloaded = require("../commands");
169
+ consoleMock.clear();
170
+
171
+ await commandsReloaded.create("production");
172
+
173
+ expect(fs.existsSync(path.join(KEYMAN_DIR_PATH, "production"))).toBe(true);
174
+
175
+ const keymanContent = JSON.parse(fs.readFileSync(KEYMAN_PATH, "utf8"));
176
+ expect(keymanContent.available).toContain("production");
177
+ });
178
+
179
+ it("should prompt for name when not provided", async () => {
180
+ let inquirer = require("inquirer");
181
+ let callCount = 0;
182
+ inquirer.prompt = jest.fn((questions) => {
183
+ callCount++;
184
+ const firstQuestion = questions[0];
185
+ if (firstQuestion.name === "envName") {
186
+ return Promise.resolve({ envName: "prompted-env" });
187
+ }
188
+ return Promise.resolve({ switchNow: false });
189
+ });
190
+
191
+ const commands = require("../commands");
192
+ commands.init();
193
+
194
+ // Reload to pick up initialized state
195
+ jest.resetModules();
196
+ inquirer = require("inquirer");
197
+ callCount = 0;
198
+ inquirer.prompt = jest.fn((questions) => {
199
+ callCount++;
200
+ const firstQuestion = questions[0];
201
+ if (firstQuestion.name === "envName") {
202
+ return Promise.resolve({ envName: "prompted-env" });
203
+ }
204
+ return Promise.resolve({ switchNow: false });
205
+ });
206
+ const commandsReloaded = require("../commands");
207
+ consoleMock.clear();
208
+
209
+ await commandsReloaded.create();
210
+
211
+ expect(callCount).toBeGreaterThan(0);
212
+ expect(fs.existsSync(path.join(KEYMAN_DIR_PATH, "prompted-env"))).toBe(true);
213
+ });
214
+ });
215
+
216
+ describe("switch", () => {
217
+ it("should show error when not initialized", async () => {
218
+ const commands = require("../commands");
219
+
220
+ await commands.switch("production");
221
+
222
+ const logs = consoleMock.getLogs().join("\n");
223
+ expect(logs).toContain("not initialized");
224
+ });
225
+
226
+ it("should switch to existing environment", async () => {
227
+ let inquirer = require("inquirer");
228
+ inquirer.prompt = jest.fn(() =>
229
+ Promise.resolve({ switchNow: false })
230
+ );
231
+
232
+ const commands = require("../commands");
233
+ commands.init();
234
+
235
+ // Reload to get initialized state
236
+ jest.resetModules();
237
+ inquirer = require("inquirer");
238
+ inquirer.prompt = jest.fn(() =>
239
+ Promise.resolve({ switchNow: false })
240
+ );
241
+ let commandsReloaded = require("../commands");
242
+ await commandsReloaded.create("production");
243
+
244
+ // Reload again to get updated environment list
245
+ jest.resetModules();
246
+ inquirer = require("inquirer");
247
+ inquirer.prompt = jest.fn(() =>
248
+ Promise.resolve({ switchNow: false })
249
+ );
250
+ commandsReloaded = require("../commands");
251
+ consoleMock.clear();
252
+
253
+ await commandsReloaded.switch("production");
254
+
255
+ const keymanContent = JSON.parse(fs.readFileSync(KEYMAN_PATH, "utf8"));
256
+ expect(keymanContent.active).toBe("production");
257
+ });
258
+
259
+ it("should show warning when already on selected environment", async () => {
260
+ const commands = require("../commands");
261
+ commands.init();
262
+
263
+ // Reload to pick up initialized state
264
+ jest.resetModules();
265
+ const commandsReloaded = require("../commands");
266
+ consoleMock.clear();
267
+
268
+ await commandsReloaded.switch("default");
269
+
270
+ const logs = consoleMock.getLogs().join("\n");
271
+ expect(logs).toContain("already");
272
+ });
273
+ });
274
+
275
+ describe("delete", () => {
276
+ it("should show error when not initialized", async () => {
277
+ const commands = require("../commands");
278
+
279
+ await commands.delete("test-env");
280
+
281
+ const logs = consoleMock.getLogs().join("\n");
282
+ expect(logs).toContain("not initialized");
283
+ });
284
+
285
+ it("should not delete default environment", async () => {
286
+ const commands = require("../commands");
287
+ commands.init();
288
+
289
+ // Reload to get initialized state
290
+ jest.resetModules();
291
+ const commandsReloaded = require("../commands");
292
+ consoleMock.clear();
293
+
294
+ await commandsReloaded.delete("default");
295
+
296
+ const logs = consoleMock.getLogs().join("\n");
297
+ expect(logs).toContain("Default environment cannot be deleted");
298
+ });
299
+
300
+ it("should not delete active environment", async () => {
301
+ let inquirer = require("inquirer");
302
+ inquirer.prompt = jest.fn(() =>
303
+ Promise.resolve({ switchNow: true })
304
+ );
305
+
306
+ const commands = require("../commands");
307
+ commands.init();
308
+
309
+ // Reload and create new env
310
+ jest.resetModules();
311
+ inquirer = require("inquirer");
312
+ inquirer.prompt = jest.fn(() =>
313
+ Promise.resolve({ switchNow: true })
314
+ );
315
+ let commandsReloaded = require("../commands");
316
+ await commandsReloaded.create("production");
317
+
318
+ // Reload to get updated content
319
+ jest.resetModules();
320
+ inquirer = require("inquirer");
321
+ inquirer.prompt = jest.fn(() =>
322
+ Promise.resolve({ switchNow: true })
323
+ );
324
+ commandsReloaded = require("../commands");
325
+ consoleMock.clear();
326
+
327
+ await commandsReloaded.delete("production");
328
+
329
+ const logs = consoleMock.getLogs().join("\n");
330
+ expect(logs).toContain("currently active");
331
+ });
332
+
333
+ it("should delete non-active environment", async () => {
334
+ let inquirer = require("inquirer");
335
+ inquirer.prompt = jest.fn(() =>
336
+ Promise.resolve({ switchNow: false })
337
+ );
338
+
339
+ const commands = require("../commands");
340
+ commands.init();
341
+
342
+ // Reload and create new env
343
+ jest.resetModules();
344
+ inquirer = require("inquirer");
345
+ inquirer.prompt = jest.fn(() =>
346
+ Promise.resolve({ switchNow: false })
347
+ );
348
+ let commandsReloaded = require("../commands");
349
+ await commandsReloaded.create("production");
350
+
351
+ // Reload to get updated content
352
+ jest.resetModules();
353
+ inquirer = require("inquirer");
354
+ inquirer.prompt = jest.fn(() =>
355
+ Promise.resolve({ switchNow: false })
356
+ );
357
+ commandsReloaded = require("../commands");
358
+ consoleMock.clear();
359
+
360
+ await commandsReloaded.delete("production");
361
+
362
+ expect(fs.existsSync(path.join(KEYMAN_DIR_PATH, "production"))).toBe(false);
363
+
364
+ const keymanContent = JSON.parse(fs.readFileSync(KEYMAN_PATH, "utf8"));
365
+ expect(keymanContent.available).not.toContain("production");
366
+ });
367
+ });
368
+ });
369
+
@@ -0,0 +1,44 @@
1
+ const { cliOptions, options } = require("../constants");
2
+
3
+ describe("constants", () => {
4
+ describe("cliOptions", () => {
5
+ it("should be an array of CLI option definitions", () => {
6
+ expect(Array.isArray(cliOptions)).toBe(true);
7
+ expect(cliOptions.length).toBeGreaterThan(0);
8
+ });
9
+
10
+ it("should have correct structure for each option", () => {
11
+ cliOptions.forEach((option) => {
12
+ expect(Array.isArray(option)).toBe(true);
13
+ expect(option.length).toBe(3);
14
+ expect(typeof option[0]).toBe("string"); // short option
15
+ expect(typeof option[1]).toBe("string"); // long option name
16
+ expect(typeof option[2]).toBe("string"); // help text
17
+ });
18
+ });
19
+
20
+ it("should include all required commands", () => {
21
+ const commands = cliOptions.map((opt) => opt[0]);
22
+ expect(commands).toContain("i"); // init
23
+ expect(commands).toContain("c"); // create
24
+ expect(commands).toContain("s"); // switch
25
+ expect(commands).toContain("d"); // delete
26
+ expect(commands).toContain("ls"); // list
27
+ expect(commands).toContain("h"); // help
28
+ expect(commands).toContain("v"); // version
29
+ });
30
+
31
+ it("should have unique option names", () => {
32
+ const shortNames = cliOptions.map((opt) => opt[0]);
33
+ const uniqueNames = new Set(shortNames);
34
+ expect(uniqueNames.size).toBe(shortNames.length);
35
+ });
36
+ });
37
+
38
+ describe("options", () => {
39
+ it("should be an array", () => {
40
+ expect(Array.isArray(options)).toBe(true);
41
+ });
42
+ });
43
+ });
44
+
@@ -0,0 +1,105 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { delDirSync, delAndCopySync } = require("../extendFs");
5
+
6
+ describe("extendFs", () => {
7
+ let testDir;
8
+
9
+ beforeEach(() => {
10
+ testDir = path.join(os.tmpdir(), `ssh-keyman-test-${Date.now()}`);
11
+ fs.mkdirSync(testDir, { recursive: true });
12
+ });
13
+
14
+ afterEach(() => {
15
+ if (fs.existsSync(testDir)) {
16
+ fs.rmSync(testDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ describe("delDirSync", () => {
21
+ it("should delete an empty directory", () => {
22
+ const emptyDir = path.join(testDir, "empty");
23
+ fs.mkdirSync(emptyDir);
24
+
25
+ delDirSync(emptyDir);
26
+
27
+ expect(fs.existsSync(emptyDir)).toBe(false);
28
+ });
29
+
30
+ it("should delete a directory with files", () => {
31
+ const dirWithFiles = path.join(testDir, "withFiles");
32
+ fs.mkdirSync(dirWithFiles);
33
+ fs.writeFileSync(path.join(dirWithFiles, "file1.txt"), "content1");
34
+ fs.writeFileSync(path.join(dirWithFiles, "file2.txt"), "content2");
35
+
36
+ delDirSync(dirWithFiles);
37
+
38
+ expect(fs.existsSync(dirWithFiles)).toBe(false);
39
+ });
40
+
41
+ it("should delete a directory with nested directories", () => {
42
+ const dirWithNested = path.join(testDir, "withNested");
43
+ const nestedDir = path.join(dirWithNested, "nested");
44
+ fs.mkdirSync(nestedDir, { recursive: true });
45
+ fs.writeFileSync(path.join(nestedDir, "file.txt"), "content");
46
+
47
+ delDirSync(dirWithNested);
48
+
49
+ expect(fs.existsSync(dirWithNested)).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe("delAndCopySync", () => {
54
+ it("should delete target directory and copy from source", () => {
55
+ const sourceDir = path.join(testDir, "source");
56
+ const targetDir = path.join(testDir, "target");
57
+
58
+ // Create source directory with files
59
+ fs.mkdirSync(sourceDir);
60
+ fs.writeFileSync(path.join(sourceDir, "file1.txt"), "source content 1");
61
+ fs.writeFileSync(path.join(sourceDir, "file2.txt"), "source content 2");
62
+
63
+ // Create target directory with different files
64
+ fs.mkdirSync(targetDir);
65
+ fs.writeFileSync(path.join(targetDir, "oldfile.txt"), "old content");
66
+
67
+ delAndCopySync(sourceDir, targetDir);
68
+
69
+ // Check that target exists
70
+ expect(fs.existsSync(targetDir)).toBe(true);
71
+
72
+ // Check that source files were copied
73
+ expect(fs.existsSync(path.join(targetDir, "file1.txt"))).toBe(true);
74
+ expect(fs.existsSync(path.join(targetDir, "file2.txt"))).toBe(true);
75
+ expect(fs.readFileSync(path.join(targetDir, "file1.txt"), "utf8")).toBe(
76
+ "source content 1"
77
+ );
78
+
79
+ // Check that old target file is gone
80
+ expect(fs.existsSync(path.join(targetDir, "oldfile.txt"))).toBe(false);
81
+ });
82
+
83
+ it("should handle nested directories in copy", () => {
84
+ const sourceDir = path.join(testDir, "source");
85
+ const targetDir = path.join(testDir, "target");
86
+ const nestedSource = path.join(sourceDir, "nested");
87
+
88
+ // Create source with nested structure
89
+ fs.mkdirSync(nestedSource, { recursive: true });
90
+ fs.writeFileSync(path.join(nestedSource, "nested.txt"), "nested content");
91
+
92
+ // Create empty target
93
+ fs.mkdirSync(targetDir);
94
+
95
+ delAndCopySync(sourceDir, targetDir);
96
+
97
+ // Check nested structure was copied
98
+ expect(fs.existsSync(path.join(targetDir, "nested", "nested.txt"))).toBe(true);
99
+ expect(
100
+ fs.readFileSync(path.join(targetDir, "nested", "nested.txt"), "utf8")
101
+ ).toBe("nested content");
102
+ });
103
+ });
104
+ });
105
+
@@ -0,0 +1,113 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ /**
6
+ * Create a mock file system structure for testing
7
+ */
8
+ function createMockFileSystem(baseDir) {
9
+ const sshPath = path.join(baseDir, ".ssh");
10
+ const keymanPath = path.join(baseDir, ".sshkeyman");
11
+ const keymanFile = path.join(keymanPath, ".sshkeyman");
12
+ const defaultEnvPath = path.join(keymanPath, "default");
13
+
14
+ return {
15
+ sshPath,
16
+ keymanPath,
17
+ keymanFile,
18
+ defaultEnvPath,
19
+ setup: () => {
20
+ if (!fs.existsSync(baseDir)) {
21
+ fs.mkdirSync(baseDir, { recursive: true });
22
+ }
23
+ if (!fs.existsSync(sshPath)) {
24
+ fs.mkdirSync(sshPath, { recursive: true });
25
+ fs.writeFileSync(path.join(sshPath, "id_rsa"), "mock private key");
26
+ fs.writeFileSync(path.join(sshPath, "id_rsa.pub"), "mock public key");
27
+ }
28
+ },
29
+ cleanup: () => {
30
+ if (fs.existsSync(baseDir)) {
31
+ fs.rmSync(baseDir, { recursive: true, force: true });
32
+ }
33
+ },
34
+ initializeKeyman: () => {
35
+ if (!fs.existsSync(keymanPath)) {
36
+ fs.mkdirSync(keymanPath, { recursive: true });
37
+ }
38
+ if (!fs.existsSync(defaultEnvPath)) {
39
+ fs.mkdirSync(defaultEnvPath, { recursive: true });
40
+ fs.copySync(sshPath, defaultEnvPath);
41
+ }
42
+ fs.writeFileSync(
43
+ keymanFile,
44
+ JSON.stringify({ active: "default", available: ["default"] })
45
+ );
46
+ },
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Mock console methods
52
+ */
53
+ function mockConsole() {
54
+ const originalLog = console.log;
55
+ const originalError = console.error;
56
+ const logs = [];
57
+ const errors = [];
58
+
59
+ console.log = (...args) => {
60
+ logs.push(args.join(" "));
61
+ };
62
+ console.error = (...args) => {
63
+ errors.push(args.join(" "));
64
+ };
65
+
66
+ return {
67
+ logs,
68
+ errors,
69
+ restore: () => {
70
+ console.log = originalLog;
71
+ console.error = originalError;
72
+ },
73
+ getLogs: () => logs,
74
+ getErrors: () => errors,
75
+ clear: () => {
76
+ logs.length = 0;
77
+ errors.length = 0;
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Mock inquirer prompts
84
+ */
85
+ function mockInquirer(answers = {}) {
86
+ const inquirer = require("inquirer");
87
+ const originalPrompt = inquirer.prompt;
88
+
89
+ inquirer.prompt = jest.fn((questions) => {
90
+ const responses = {};
91
+ questions.forEach((q) => {
92
+ if (answers[q.name] !== undefined) {
93
+ responses[q.name] = answers[q.name];
94
+ } else if (q.default !== undefined) {
95
+ responses[q.name] = q.default;
96
+ }
97
+ });
98
+ return Promise.resolve(responses);
99
+ });
100
+
101
+ return {
102
+ restore: () => {
103
+ inquirer.prompt = originalPrompt;
104
+ },
105
+ };
106
+ }
107
+
108
+ module.exports = {
109
+ createMockFileSystem,
110
+ mockConsole,
111
+ mockInquirer,
112
+ };
113
+