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,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
+
package/src/commands.js CHANGED
@@ -1,10 +1,15 @@
1
1
  const fs = require("fs-extra");
2
2
  const os = require("os");
3
3
  const path = require("path");
4
- const readline = require("readline");
5
- const { options, consoleColors } = require("./constants");
4
+ const inquirer = require("inquirer");
5
+ const chalk = require("chalk");
6
+ const autocompletePrompt = require("inquirer-autocomplete-prompt");
7
+ const { options } = require("./constants");
6
8
  const { delAndCopySync, delDirSync } = require("./extendFs");
7
9
 
10
+ // Register autocomplete prompt
11
+ inquirer.registerPrompt("autocomplete", autocompletePrompt);
12
+
8
13
  const SSH_PATH = path.join(os.homedir(), ".ssh");
9
14
  const KEYMAN_DIR_PATH = path.join(os.homedir(), ".sshkeyman");
10
15
  const KEYMAN_PATH = path.join(KEYMAN_DIR_PATH, ".sshkeyman");
@@ -14,191 +19,260 @@ let KEYMAN_CONTENT = IS_INITIALIZED
14
19
  ? JSON.parse(fs.readFileSync(KEYMAN_PATH))
15
20
  : undefined;
16
21
 
17
- const rl = readline.createInterface({
18
- input: process.stdin,
19
- output: process.stdout,
20
- });
21
-
22
- const question = (q) =>
23
- new Promise((resolve) => {
24
- rl.question(q, (a) => {
25
- resolve(a);
26
- });
27
- });
28
-
29
- rl.on("close", () => {
30
- console.log("closed");
31
- });
32
-
33
22
  const logger = (type, message) => {
34
23
  if (Array.isArray(message)) {
35
24
  message = message.join(" ");
36
25
  }
37
- let color;
38
26
  switch (type) {
39
- case "success": {
40
- color = consoleColors.FgGreen;
27
+ case "success":
28
+ console.log(chalk.green(message ? message : ""));
41
29
  break;
42
- }
43
- case "error": {
44
- color = consoleColors.FgRed;
30
+ case "error":
31
+ console.log(chalk.red(message ? message : ""));
45
32
  break;
46
- }
47
- default: {
48
- color = consoleColors.FgWhite;
33
+ case "warning":
34
+ console.log(chalk.yellow(message ? message : ""));
35
+ break;
36
+ case "info":
37
+ console.log(chalk.cyan(message ? message : ""));
38
+ break;
39
+ default:
40
+ console.log(message ? message : "");
49
41
  break;
50
- }
51
42
  }
52
- console.log(color, message ? message : "");
53
43
  };
54
44
 
55
45
  const help = function () {
56
- logger(null, "\n");
57
- logger(null, "Usage : ssh-keyman <command>", "\n");
58
- logger(null, [
59
- "Where <command> is one of: ",
60
- options.map((op) => `-${op.option}`).join(", "),
61
- "\n",
62
- ]);
63
- logger(null, "Commands:");
46
+ console.log();
47
+ console.log(chalk.bold.cyan("SSH KeyMan") + chalk.gray(" - SSH Key Environment Manager"));
48
+ console.log();
49
+ console.log(chalk.bold("Usage:") + " ssh-keyman <command> [options]");
50
+ console.log();
51
+ console.log(chalk.bold("Commands:"));
64
52
  for (let option of options) {
65
- logger(null, option.help);
53
+ console.log(chalk.gray(option.help));
66
54
  }
67
- logger(null, "\n");
55
+ console.log();
56
+ console.log(chalk.dim("Tip: Run commands without arguments for interactive mode"));
57
+ console.log();
68
58
  };
69
59
 
70
60
  const init = function () {
71
61
  if (!fs.existsSync(KEYMAN_DIR_PATH)) {
62
+ console.log(chalk.cyan("\n🔑 Initializing SSH KeyMan...\n"));
72
63
  fs.mkdirSync(KEYMAN_DIR_PATH);
73
- logger("success", "Initialized ssh-keyman directory " + KEYMAN_DIR_PATH);
64
+ logger("success", " Created ssh-keyman directory: " + KEYMAN_DIR_PATH);
74
65
  fs.mkdirSync(KEYMAN_DEFAULT_ENV_PATH);
75
66
  fs.copySync(SSH_PATH, KEYMAN_DEFAULT_ENV_PATH);
76
- logger(
77
- "success",
78
- "Initialized default environment " + KEYMAN_DEFAULT_ENV_PATH
79
- );
67
+ logger("success", "✓ Created default environment");
80
68
  fs.writeFileSync(
81
69
  KEYMAN_PATH,
82
70
  JSON.stringify({ active: "default", available: ["default"] })
83
71
  );
84
- logger("success", "Activated 'default' environment");
72
+ logger("success", "Activated 'default' environment");
73
+ console.log(chalk.green("\n✨ SSH KeyMan initialized successfully!\n"));
85
74
  return;
86
75
  }
87
- logger("success", "ssh-keyman already initialized");
76
+ logger("info", "ssh-keyman is already initialized");
88
77
  };
89
78
 
90
79
  const create = async function (name) {
91
80
  if (!IS_INITIALIZED) {
92
- logger("error", "ssh-keyman is not initilized \n");
93
- logger(null, [
94
- "Please initialize ssh-keyman using:",
95
- "\n",
96
- "ssh-keyman -i",
97
- "\n",
98
- ]);
81
+ logger("error", "ssh-keyman is not initialized\n");
82
+ logger(null, "Please initialize ssh-keyman using: ssh-keyman -i\n");
99
83
  return;
100
84
  }
101
85
  if (!name) {
102
- name = await question("Enter name for the new enviroment: ");
86
+ const answer = await inquirer.prompt([
87
+ {
88
+ type: "input",
89
+ name: "envName",
90
+ message: "Enter name for the new environment:",
91
+ validate: (input) => {
92
+ if (!input || input.trim() === "") {
93
+ return "Environment name cannot be empty";
94
+ }
95
+ if (fs.existsSync(path.join(KEYMAN_DIR_PATH, input))) {
96
+ return "An environment with this name already exists";
97
+ }
98
+ return true;
99
+ },
100
+ },
101
+ ]);
102
+ name = answer.envName;
103
103
  }
104
104
  let { active, available } = KEYMAN_CONTENT || {};
105
105
  const exist = fs.existsSync(path.join(KEYMAN_DIR_PATH, name));
106
106
  if (exist) {
107
- return logger("error", "An environment with similar name already eixsts");
107
+ return logger("error", "An environment with similar name already exists");
108
108
  }
109
109
  if (active) {
110
110
  delAndCopySync(SSH_PATH, path.join(KEYMAN_DIR_PATH, active));
111
- logger("success", [`Saved current ssh config to ${active}`]);
111
+ logger("success", `Saved current ssh config to ${active}`);
112
112
  const NEW_ENV_PATH = path.join(KEYMAN_DIR_PATH, name);
113
113
  fs.mkdirSync(NEW_ENV_PATH);
114
- logger("success", [
115
- "Created directory for new environment : ",
116
- NEW_ENV_PATH,
114
+ logger("success", `Created directory for new environment: ${NEW_ENV_PATH}`);
115
+
116
+ const { switchNow } = await inquirer.prompt([
117
+ {
118
+ type: "confirm",
119
+ name: "switchNow",
120
+ message: `Do you want to switch to newly created environment (${name})?`,
121
+ default: true,
122
+ },
117
123
  ]);
118
- const ans = await question(
119
- ` Do you want to switch to newly created environment (${name})? `
120
- );
121
- if (ans.toLowerCase() === "y" || ans.toLowerCase() === 'yes') {
122
- available.push(name);
124
+
125
+ available.push(name);
126
+ if (switchNow) {
123
127
  fs.writeFileSync(
124
128
  KEYMAN_PATH,
125
129
  JSON.stringify({ active: name, available })
126
130
  );
127
131
  delDirSync(SSH_PATH);
128
132
  fs.mkdirSync(SSH_PATH);
129
- return logger("success", [`Activated environment '${name}'`]);
133
+ return logger("success", `Activated environment '${name}'`);
130
134
  }
131
- available.push(name);
132
135
  fs.writeFileSync(
133
136
  KEYMAN_PATH,
134
137
  JSON.stringify({ active: active, available })
135
138
  );
136
- return logger("success", [`Successfully created environment ${name}`]);
139
+ return logger("success", `Successfully created environment ${name}`);
137
140
  }
138
141
  };
139
142
 
140
143
  const list = function () {
141
144
  if (!IS_INITIALIZED) {
142
- logger("error", "ssh-keyman is not initilized \n");
143
- logger(null, [
144
- "Please initialize ssh-keyman using:",
145
- "\n",
146
- "ssh-keyman -i",
147
- "\n",
148
- ]);
145
+ logger("error", "ssh-keyman is not initialized\n");
146
+ logger(null, "Please initialize ssh-keyman using: ssh-keyman -i\n");
149
147
  return;
150
148
  }
151
- logger(null, "\nAvailable environments:");
149
+ console.log(chalk.bold("\nAvailable environments:"));
152
150
  if (KEYMAN_CONTENT) {
153
151
  const { active, available } = KEYMAN_CONTENT;
154
152
  available.forEach((env) => {
155
153
  if (env === active) {
156
- logger("success", `*${env}`);
154
+ console.log(chalk.green(` ✓ ${env}`) + chalk.gray(" (active)"));
157
155
  } else {
158
- logger(null, env);
156
+ console.log(chalk.white(` • ${env}`));
159
157
  }
160
158
  });
161
159
  }
162
- logger(null);
160
+ console.log();
163
161
  };
164
162
 
165
- const switchEnv = function (name) {
163
+ const switchEnv = async function (name) {
166
164
  if (!IS_INITIALIZED) {
167
- logger("error", "ssh-keyman is not initilized \n");
168
- logger(null, [
169
- "Please initialize ssh-keyman using:",
170
- "\n",
171
- "ssh-keyman -i",
172
- "\n",
173
- ]);
165
+ logger("error", "ssh-keyman is not initialized\n");
166
+ logger(null, "Please initialize ssh-keyman using: ssh-keyman -i\n");
174
167
  return;
175
168
  }
176
169
  if (KEYMAN_CONTENT) {
177
170
  const { active, available } = KEYMAN_CONTENT;
171
+
172
+ // If no name provided, show interactive autocomplete menu
173
+ if (!name) {
174
+ const otherEnvs = available.filter((env) => env !== active);
175
+
176
+ if (otherEnvs.length === 0) {
177
+ return logger("warning", "No other environments available to switch to");
178
+ }
179
+
180
+ const answer = await inquirer.prompt([
181
+ {
182
+ type: "autocomplete",
183
+ name: "envName",
184
+ message: "Select environment to switch to:",
185
+ source: async (answersSoFar, input) => {
186
+ const filtered = otherEnvs.filter((env) =>
187
+ env.toLowerCase().includes((input || "").toLowerCase())
188
+ );
189
+ return filtered.map((env) => ({
190
+ name: env,
191
+ value: env,
192
+ }));
193
+ },
194
+ pageSize: 10,
195
+ },
196
+ ]);
197
+ name = answer.envName;
198
+ }
199
+
178
200
  const env = available.find((env) => env === name);
201
+
202
+ if (!env) {
203
+ return logger("error", `Environment '${name}' not found`);
204
+ }
205
+
179
206
  if (env === active) {
180
- return logger(
181
- "error",
182
- `${name} is already selected as existing environment`
183
- );
207
+ return logger("warning", `${name} is already the active environment`);
184
208
  }
185
209
 
186
210
  delAndCopySync(SSH_PATH, path.join(KEYMAN_DIR_PATH, active));
187
- logger("success", [`Saved current ssh config to '${active}'`]);
211
+ logger("success", `Saved current ssh config to '${active}'`);
188
212
  const NEW_ENV_PATH = path.join(KEYMAN_DIR_PATH, name);
189
213
  delAndCopySync(NEW_ENV_PATH, SSH_PATH);
190
214
  fs.writeFileSync(KEYMAN_PATH, JSON.stringify({ available, active: name }));
191
215
  logger("success", `Activated environment '${name}'`);
192
216
  } else {
193
- logger("error", `Data directory is currupt, Please try uninstalling and reinstalling the package.`);
217
+ logger("error", "Data directory is corrupt. Please try uninstalling and reinstalling the package.");
194
218
  }
195
219
  };
196
220
 
197
- const deleteEnv = function (name) {
221
+ const deleteEnv = async function (name) {
222
+ if (!IS_INITIALIZED) {
223
+ logger("error", "ssh-keyman is not initialized\n");
224
+ logger(null, "Please initialize ssh-keyman using: ssh-keyman -i\n");
225
+ return;
226
+ }
227
+
228
+ let { active, available } = KEYMAN_CONTENT || {};
229
+
230
+ // If no name provided, show interactive autocomplete menu
231
+ if (!name) {
232
+ const deletableEnvs = available.filter((env) => env !== "default" && env !== active);
233
+
234
+ if (deletableEnvs.length === 0) {
235
+ return logger("warning", "No environments available to delete");
236
+ }
237
+
238
+ const answer = await inquirer.prompt([
239
+ {
240
+ type: "autocomplete",
241
+ name: "envName",
242
+ message: "Select environment to delete:",
243
+ source: async (answersSoFar, input) => {
244
+ const filtered = deletableEnvs.filter((env) =>
245
+ env.toLowerCase().includes((input || "").toLowerCase())
246
+ );
247
+ return filtered.map((env) => ({
248
+ name: env,
249
+ value: env,
250
+ }));
251
+ },
252
+ pageSize: 10,
253
+ },
254
+ ]);
255
+ name = answer.envName;
256
+
257
+ // Confirm deletion
258
+ const { confirmDelete } = await inquirer.prompt([
259
+ {
260
+ type: "confirm",
261
+ name: "confirmDelete",
262
+ message: `Are you sure you want to delete environment '${name}'?`,
263
+ default: false,
264
+ },
265
+ ]);
266
+
267
+ if (!confirmDelete) {
268
+ return logger("info", "Deletion cancelled");
269
+ }
270
+ }
271
+
198
272
  if (name === "default") {
199
273
  return logger("error", "Default environment cannot be deleted");
200
274
  }
201
- let { active, available } = KEYMAN_CONTENT || {};
275
+
202
276
  const exist = fs.existsSync(path.join(KEYMAN_DIR_PATH, name));
203
277
  if (active === name) {
204
278
  return logger(
@@ -220,8 +294,8 @@ const deleteEnv = function (name) {
220
294
  };
221
295
 
222
296
  const version = function() {
223
- const package = require('../package.json');
224
- return logger(null, 'ssh-keyman version : ' + package.version);
297
+ const pkg = require('../package.json');
298
+ console.log(chalk.cyan('ssh-keyman') + chalk.gray(' version ') + chalk.bold(pkg.version));
225
299
  }
226
300
 
227
301
  module.exports = {
package/src/constants.js CHANGED
@@ -1,45 +1,16 @@
1
1
  const cliOptions = [
2
- ["i", "init", " -i \t\t initialize keyman directory and default environment"],
3
- ["c", "create", " -c [name] \t create new ssh environment"],
4
- ["s", "switch", " -s [name] \t switch to another ssh environment"],
5
- ["d", "delete", " -d [name] \t delete ssh environment"],
6
- ["ls", "list", " -ls\t\t list environments"],
7
- ["h", "help", " -h\t\t help"],
8
- ["v", "version", " -v\t\t version"],
2
+ ["i", "init", " -i Initialize keyman directory and default environment"],
3
+ ["c", "create", " -c [name] Create new ssh environment (interactive if no name)"],
4
+ ["s", "switch", " -s [name] Switch to another ssh environment (interactive if no name)"],
5
+ ["d", "delete", " -d [name] Delete ssh environment (interactive if no name)"],
6
+ ["ls", "list", " -ls List all environments"],
7
+ ["h", "help", " -h Show help"],
8
+ ["v", "version", " -v Show version"],
9
9
  ];
10
10
 
11
11
  const options = [];
12
12
 
13
- const consoleColors = {
14
- Reset: "\x1b[0m",
15
- Bright: "\x1b[1m",
16
- Dim: "\x1b[2m",
17
- Underscore: "\x1b[4m",
18
- Blink: "\x1b[5m",
19
- Reverse: "\x1b[7m",
20
- Hidden: "\x1b[8m",
21
-
22
- FgBlack: "\x1b[30m",
23
- FgRed: "\x1b[31m",
24
- FgGreen: "\x1b[32m",
25
- FgYellow: "\x1b[33m",
26
- FgBlue: "\x1b[34m",
27
- FgMagenta: "\x1b[35m",
28
- FgCyan: "\x1b[36m",
29
- FgWhite: "\x1b[37m",
30
-
31
- BgBlack: "\x1b[40m",
32
- BgRed: "\x1b[41m",
33
- BgGreen: "\x1b[42m",
34
- BgYellow: "\x1b[43m",
35
- BgBlue: "\x1b[44m",
36
- BgMagenta: "\x1b[45m",
37
- BgCyan: "\x1b[46m",
38
- BgWhite: "\x1b[47m",
39
- };
40
-
41
13
  module.exports = {
42
14
  cliOptions,
43
15
  options,
44
- consoleColors
45
16
  };