@abhiseck/zssh 0.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/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const chalk = __importStar(require("chalk"));
41
+ const commander_1 = require("commander");
42
+ const inquirer_1 = __importDefault(require("inquirer"));
43
+ const config_1 = require("./lib/config");
44
+ const program = new commander_1.Command();
45
+ const configManager = new config_1.ConfigManager();
46
+ async function promptForMasterPassword(isNew = false) {
47
+ const { password } = await inquirer_1.default.prompt([
48
+ {
49
+ type: "password",
50
+ name: "password",
51
+ message: isNew
52
+ ? "Set a master password for your encrypted config:"
53
+ : "Enter master password:",
54
+ mask: "*",
55
+ },
56
+ ]);
57
+ return password;
58
+ }
59
+ async function initConfig() {
60
+ if (configManager.isConfigExists()) {
61
+ const password = await promptForMasterPassword();
62
+ configManager.setMasterKey(password);
63
+ try {
64
+ configManager.load();
65
+ }
66
+ catch (e) {
67
+ console.error(chalk.red("Invalid password or corrupted config."));
68
+ process.exit(1);
69
+ }
70
+ }
71
+ else {
72
+ console.log(chalk.yellow("No existing config found. Initializing..."));
73
+ const password = await promptForMasterPassword(true);
74
+ const { confirm } = await inquirer_1.default.prompt([
75
+ {
76
+ type: "password",
77
+ name: "confirm",
78
+ message: "Confirm master password:",
79
+ mask: "*",
80
+ },
81
+ ]);
82
+ if (password !== confirm) {
83
+ console.error(chalk.red("Passwords do not match."));
84
+ process.exit(1);
85
+ }
86
+ configManager.setMasterKey(password);
87
+ configManager.save(); // Create empty encrypted file
88
+ console.log(chalk.green("Config initialized!"));
89
+ }
90
+ }
91
+ program
92
+ .name("zssh")
93
+ .description("SSH Chain - Encrypted SSH Connection Manager")
94
+ .version("1.0.0")
95
+ .addHelpText("after", `
96
+ Example:
97
+ $ zssh add
98
+ $ zssh list
99
+ $ zssh connect my-server
100
+ $ zssh connect 5f3a1
101
+ $ zssh save
102
+ $ zssh sync
103
+ `);
104
+ program
105
+ .command("add")
106
+ .description("Add a new SSH connection")
107
+ .action(async () => {
108
+ await initConfig();
109
+ const { addCommand } = await Promise.resolve().then(() => __importStar(require("./commands/add")));
110
+ await addCommand(configManager);
111
+ });
112
+ program
113
+ .command("list")
114
+ .description("List all saved connections")
115
+ .action(async () => {
116
+ await initConfig();
117
+ const { listCommand } = await Promise.resolve().then(() => __importStar(require("./commands/list")));
118
+ await listCommand(configManager);
119
+ });
120
+ program
121
+ .command("connect <idOrAlias>")
122
+ .description("Connect to a server")
123
+ .action(async (idOrAlias) => {
124
+ await initConfig();
125
+ const { connectCommand } = await Promise.resolve().then(() => __importStar(require("./commands/connect")));
126
+ await connectCommand(configManager, idOrAlias);
127
+ });
128
+ program
129
+ .command("remove <idOrAlias>")
130
+ .description("Remove a connection")
131
+ .action(async (idOrAlias) => {
132
+ await initConfig();
133
+ const { removeCommand } = await Promise.resolve().then(() => __importStar(require("./commands/remove")));
134
+ await removeCommand(configManager, idOrAlias);
135
+ });
136
+ program
137
+ .command("save")
138
+ .description("Save config to GitHub Gist")
139
+ .action(async () => {
140
+ await initConfig();
141
+ const { saveCommand } = await Promise.resolve().then(() => __importStar(require("./commands/sync")));
142
+ await saveCommand(configManager);
143
+ });
144
+ program
145
+ .command("sync")
146
+ .description("Sync config from GitHub Gist")
147
+ .action(async () => {
148
+ await initConfig();
149
+ const { syncCommand } = await Promise.resolve().then(() => __importStar(require("./commands/sync")));
150
+ await syncCommand(configManager);
151
+ });
152
+ program.parse(process.argv);
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ConfigManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const crypto_1 = require("./crypto");
41
+ const CONFIG_PATH = path.join(os.homedir(), ".sshc.json");
42
+ class ConfigManager {
43
+ constructor() {
44
+ this.masterKey = null;
45
+ this.config = {
46
+ connections: [],
47
+ };
48
+ }
49
+ setMasterKey(key) {
50
+ this.masterKey = key;
51
+ }
52
+ load() {
53
+ if (!fs.existsSync(CONFIG_PATH)) {
54
+ return true; // New config
55
+ }
56
+ try {
57
+ const fileContent = fs.readFileSync(CONFIG_PATH, "utf-8");
58
+ // Try parsing as plain JSON first (migration or initial unencrypted)
59
+ try {
60
+ const plain = JSON.parse(fileContent);
61
+ if (plain.connections) {
62
+ this.config = plain;
63
+ return true;
64
+ }
65
+ }
66
+ catch (e) {
67
+ // Not plain JSON, likely encrypted string or object
68
+ }
69
+ // If we are here, it's likely encrypted.
70
+ // We expect the file to contain just the encrypted string or { data: '...' }
71
+ // For simplicity let's assume the file content IS the encrypted string if not JSON
72
+ // Or we wrap it. Let's assume we wrap it to be safe.
73
+ let encryptedData = fileContent;
74
+ try {
75
+ const wrapped = JSON.parse(fileContent);
76
+ if (wrapped.encrypted) {
77
+ encryptedData = wrapped.encrypted;
78
+ }
79
+ }
80
+ catch (e) {
81
+ // It's raw encrypted string
82
+ }
83
+ if (!this.masterKey) {
84
+ throw new Error("Master key required to decrypt config");
85
+ }
86
+ const decrypted = (0, crypto_1.decrypt)(encryptedData, this.masterKey);
87
+ this.config = JSON.parse(decrypted);
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ if (error instanceof Error &&
92
+ error.message.includes("Master key required")) {
93
+ throw error;
94
+ }
95
+ // wrong password or corrupted
96
+ throw new Error("Failed to load config: " +
97
+ (error instanceof Error ? error.message : String(error)));
98
+ }
99
+ }
100
+ getConfig() {
101
+ return this.config;
102
+ }
103
+ setGithubToken(token) {
104
+ this.config.githubToken = token;
105
+ this.save();
106
+ }
107
+ setGistId(id) {
108
+ this.config.gistId = id;
109
+ this.save();
110
+ }
111
+ getEncryptedContent() {
112
+ if (!this.masterKey)
113
+ return null;
114
+ const json = JSON.stringify(this.config);
115
+ return (0, crypto_1.encrypt)(json, this.masterKey);
116
+ }
117
+ mergeEncrypted(encryptedContent) {
118
+ if (!this.masterKey)
119
+ return false;
120
+ try {
121
+ const decrypted = (0, crypto_1.decrypt)(encryptedContent, this.masterKey);
122
+ const remoteConfig = JSON.parse(decrypted);
123
+ let changed = false;
124
+ remoteConfig.connections.forEach((remoteConn) => {
125
+ if (!this.config.connections.find((c) => c.id === remoteConn.id)) {
126
+ this.config.connections.push(remoteConn);
127
+ changed = true;
128
+ }
129
+ // Could add logic to update existing ones if newer
130
+ });
131
+ if (changed) {
132
+ this.save();
133
+ }
134
+ return true;
135
+ }
136
+ catch (e) {
137
+ console.error("Failed to decrypt and merge remote config:", e.message);
138
+ return false;
139
+ }
140
+ }
141
+ save() {
142
+ if (!this.masterKey) {
143
+ throw new Error("Master key required to save config");
144
+ }
145
+ const json = JSON.stringify(this.config);
146
+ const encrypted = (0, crypto_1.encrypt)(json, this.masterKey);
147
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify({ encrypted }), "utf-8");
148
+ }
149
+ getConnections() {
150
+ return this.config.connections;
151
+ }
152
+ addConnection(connection) {
153
+ this.config.connections.push(connection);
154
+ this.save();
155
+ }
156
+ removeConnection(idOrAlias) {
157
+ this.config.connections = this.config.connections.filter((c) => c.id !== idOrAlias && c.alias !== idOrAlias);
158
+ this.save();
159
+ }
160
+ getConnection(idOrAlias) {
161
+ return this.config.connections.find((c) => c.id === idOrAlias || c.alias === idOrAlias);
162
+ }
163
+ isConfigExists() {
164
+ return fs.existsSync(CONFIG_PATH);
165
+ }
166
+ }
167
+ exports.ConfigManager = ConfigManager;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.encrypt = encrypt;
37
+ exports.decrypt = decrypt;
38
+ const crypto = __importStar(require("crypto"));
39
+ const ALGORITHM = "aes-256-gcm";
40
+ const IV_LENGTH = 16;
41
+ const SALT_LENGTH = 64;
42
+ const TAG_LENGTH = 16;
43
+ const KEY_LENGTH = 32;
44
+ const ITERATIONS = 100000;
45
+ function encrypt(text, masterKey) {
46
+ const iv = crypto.randomBytes(IV_LENGTH);
47
+ const salt = crypto.randomBytes(SALT_LENGTH);
48
+ const key = crypto.pbkdf2Sync(masterKey, salt, ITERATIONS, KEY_LENGTH, "sha512");
49
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
50
+ let encrypted = cipher.update(text, "utf8", "hex");
51
+ encrypted += cipher.final("hex");
52
+ const tag = cipher.getAuthTag();
53
+ // Format: salt:iv:tag:encrypted
54
+ return `${salt.toString("hex")}:${iv.toString("hex")}:${tag.toString("hex")}:${encrypted}`;
55
+ }
56
+ function decrypt(text, masterKey) {
57
+ const parts = text.split(":");
58
+ if (parts.length !== 4) {
59
+ throw new Error("Invalid encrypted data format");
60
+ }
61
+ const salt = Buffer.from(parts[0], "hex");
62
+ const iv = Buffer.from(parts[1], "hex");
63
+ const tag = Buffer.from(parts[2], "hex");
64
+ const encryptedText = parts[3];
65
+ const key = crypto.pbkdf2Sync(masterKey, salt, ITERATIONS, KEY_LENGTH, "sha512");
66
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
67
+ decipher.setAuthTag(tag);
68
+ let decrypted = decipher.update(encryptedText, "hex", "utf8");
69
+ decrypted += decipher.final("utf8");
70
+ return decrypted;
71
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@abhiseck/zssh",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "build": "tsc",
9
+ "start": "ts-node src/index.ts",
10
+ "dev": "ts-node src/index.ts"
11
+ },
12
+ "bin": {
13
+ "zssh": "dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "git",
17
+ "cli",
18
+ "account-management",
19
+ "git-user",
20
+ "git-config",
21
+ "ssh-key",
22
+ "multiple-accounts"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/iamalipe/sshc.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/iamalipe/sshc/issues"
30
+ },
31
+ "homepage": "https://github.com/iamalipe/sshc#readme",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "author": "Abhiseck Bhattacharya <abhiseck@outlook.com>",
36
+ "license": "MIT",
37
+ "type": "commonjs",
38
+ "dependencies": {
39
+ "axios": "^1.13.5",
40
+ "boxen": "^5.1.2",
41
+ "chalk": "^4.1.2",
42
+ "cli-table3": "^0.6.5",
43
+ "commander": "^14.0.3",
44
+ "inquirer": "^8.2.7",
45
+ "keytar": "^7.9.0",
46
+ "ssh2-promise": "^1.0.3",
47
+ "uuid": "^13.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/inquirer": "^9.0.9",
51
+ "@types/node": "^25.2.3",
52
+ "@types/ssh2": "^1.15.5",
53
+ "@types/uuid": "^10.0.0",
54
+ "ts-node": "^10.9.2",
55
+ "typescript": "^5.9.3"
56
+ }
57
+ }
@@ -0,0 +1,79 @@
1
+ import * as chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { ConfigManager } from "../lib/config";
5
+ import { SSHConnection } from "../lib/types";
6
+
7
+ export async function addCommand(configManager: ConfigManager) {
8
+ console.log(chalk.blue("Adding new SSH connection..."));
9
+
10
+ const answers = await inquirer.prompt([
11
+ {
12
+ type: "input",
13
+ name: "host",
14
+ message: "Host (IP or Domain):",
15
+ validate: (input) => (input ? true : "Host is required"),
16
+ },
17
+ {
18
+ type: "input",
19
+ name: "port",
20
+ message: "Port:",
21
+ default: "22",
22
+ validate: (input) =>
23
+ !isNaN(parseInt(input)) ? true : "Port must be a number",
24
+ },
25
+ {
26
+ type: "input",
27
+ name: "username",
28
+ message: "Username:",
29
+ validate: (input) => (input ? true : "Username is required"),
30
+ },
31
+ {
32
+ type: "list",
33
+ name: "authType",
34
+ message: "Authentication Method:",
35
+ choices: ["Password", "Private Key", "Agent (No Password/Key stored)"],
36
+ },
37
+ {
38
+ type: "password",
39
+ name: "password",
40
+ message: "Password:",
41
+ when: (answers) => answers.authType === "Password",
42
+ mask: "*",
43
+ },
44
+ {
45
+ type: "input",
46
+ name: "privateKeyPath",
47
+ message: "Private Key Path:",
48
+ when: (answers) => answers.authType === "Private Key",
49
+ default: "~/.ssh/id_rsa",
50
+ },
51
+ {
52
+ type: "input",
53
+ name: "alias",
54
+ message: "Alias (short name):",
55
+ },
56
+ {
57
+ type: "input",
58
+ name: "description",
59
+ message: "Description (optional):",
60
+ },
61
+ ]);
62
+
63
+ const connection: SSHConnection = {
64
+ id: uuidv4().split("-")[0], // Short ID
65
+ alias: answers.alias,
66
+ host: answers.host,
67
+ port: parseInt(answers.port),
68
+ username: answers.username,
69
+ password: answers.password,
70
+ privateKeyPath: answers.privateKeyPath,
71
+ description: answers.description,
72
+ createdAt: new Date().toISOString(),
73
+ };
74
+
75
+ configManager.addConnection(connection);
76
+ console.log(chalk.green(`\nConnection added successfully!`));
77
+ console.log(`ID: ${chalk.cyan(connection.id)}`);
78
+ if (connection.alias) console.log(`Alias: ${chalk.cyan(connection.alias)}`);
79
+ }
@@ -0,0 +1,85 @@
1
+ import * as chalk from "chalk";
2
+ import { spawn } from "child_process";
3
+ import { ConfigManager } from "../lib/config";
4
+
5
+ async function isSshpassInstalled(): Promise<boolean> {
6
+ return new Promise((resolve) => {
7
+ const check = spawn("command", ["-v", "sshpass"]);
8
+ check.on("close", (code) => {
9
+ resolve(code === 0);
10
+ });
11
+ });
12
+ }
13
+
14
+ export async function connectCommand(
15
+ configManager: ConfigManager,
16
+ idOrAlias: string,
17
+ ) {
18
+ const conn = configManager.getConnection(idOrAlias);
19
+
20
+ if (!conn) {
21
+ console.log(chalk.red(`Connection not found: ${idOrAlias}`));
22
+ return;
23
+ }
24
+
25
+ console.log(
26
+ chalk.blue(
27
+ `Connecting to ${conn.alias || conn.id} (${conn.username}@${conn.host})...`,
28
+ ),
29
+ );
30
+
31
+ const args: string[] = [];
32
+
33
+ // Port
34
+ if (conn.port) {
35
+ args.push("-p", conn.port.toString());
36
+ }
37
+
38
+ // Identity file
39
+ if (conn.privateKeyPath) {
40
+ args.push("-i", conn.privateKeyPath);
41
+ }
42
+
43
+ // Destination
44
+ args.push(`${conn.username}@${conn.host}`);
45
+
46
+ let command = "ssh";
47
+ let finalArgs = args;
48
+ const env = { ...process.env };
49
+
50
+ // Handle Password
51
+ if (conn.password) {
52
+ const hasSshpass = await isSshpassInstalled();
53
+ if (hasSshpass) {
54
+ command = "sshpass";
55
+ // options for sshpass
56
+ // -p password
57
+ // We can also use SSHPASS env var to avoid showing in process list (slightly safer)
58
+ env["SSHPASS"] = conn.password;
59
+ finalArgs = ["-e", "ssh", ...args]; // -e means take password from env
60
+ } else {
61
+ console.log(chalk.yellow('Warning: "sshpass" is not installed.'));
62
+ console.log(
63
+ chalk.yellow("Cannot auto-fill password. You will be prompted by SSH."),
64
+ );
65
+ console.log(
66
+ chalk.dim(
67
+ 'Install sshpass (e.g. "brew install sshpass" on Mac) to enable auto-login.',
68
+ ),
69
+ );
70
+ }
71
+ }
72
+
73
+ const child = spawn(command, finalArgs, {
74
+ stdio: "inherit",
75
+ env: env,
76
+ });
77
+
78
+ child.on("close", (code) => {
79
+ if (code !== 0) {
80
+ console.log(chalk.red(`\nSSH session ended with code ${code}`));
81
+ } else {
82
+ console.log(chalk.green("\nDisconnected."));
83
+ }
84
+ });
85
+ }
@@ -0,0 +1,37 @@
1
+ import * as chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { ConfigManager } from "../lib/config";
4
+
5
+ export async function listCommand(configManager: ConfigManager) {
6
+ const connections = configManager.getConnections();
7
+
8
+ if (connections.length === 0) {
9
+ console.log(
10
+ chalk.yellow('No connections found. Use "sshc add" to add one.'),
11
+ );
12
+ return;
13
+ }
14
+
15
+ const table = new Table({
16
+ head: [
17
+ chalk.cyan("ID"),
18
+ chalk.cyan("Alias"),
19
+ chalk.cyan("Host"),
20
+ chalk.cyan("User"),
21
+ chalk.cyan("Port"),
22
+ ],
23
+ style: { head: [], border: [] },
24
+ });
25
+
26
+ connections.forEach((conn) => {
27
+ table.push([
28
+ conn.id,
29
+ conn.alias || "",
30
+ conn.host,
31
+ conn.username,
32
+ conn.port,
33
+ ]);
34
+ });
35
+
36
+ console.log(table.toString());
37
+ }
@@ -0,0 +1,29 @@
1
+ import * as chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { ConfigManager } from "../lib/config";
4
+
5
+ export async function removeCommand(
6
+ configManager: ConfigManager,
7
+ idOrAlias: string,
8
+ ) {
9
+ const conn = configManager.getConnection(idOrAlias);
10
+
11
+ if (!conn) {
12
+ console.log(chalk.red(`Connection not found: ${idOrAlias}`));
13
+ return;
14
+ }
15
+
16
+ const { confirm } = await inquirer.prompt([
17
+ {
18
+ type: "confirm",
19
+ name: "confirm",
20
+ message: `Are you sure you want to delete ${conn.alias || conn.id} (${conn.username}@${conn.host})?`,
21
+ default: false,
22
+ },
23
+ ]);
24
+
25
+ if (confirm) {
26
+ configManager.removeConnection(idOrAlias);
27
+ console.log(chalk.green("Connection removed."));
28
+ }
29
+ }