@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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/commands/add.js +112 -0
- package/dist/commands/connect.js +97 -0
- package/dist/commands/list.js +68 -0
- package/dist/commands/remove.js +60 -0
- package/dist/commands/sync.js +145 -0
- package/dist/index.js +152 -0
- package/dist/lib/config.js +167 -0
- package/dist/lib/crypto.js +71 -0
- package/dist/lib/types.js +2 -0
- package/package.json +57 -0
- package/src/commands/add.ts +79 -0
- package/src/commands/connect.ts +85 -0
- package/src/commands/list.ts +37 -0
- package/src/commands/remove.ts +29 -0
- package/src/commands/sync.ts +138 -0
- package/src/index.ts +130 -0
- package/src/lib/config.ts +160 -0
- package/src/lib/crypto.ts +59 -0
- package/src/lib/types.ts +17 -0
- package/tsconfig.json +15 -0
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
|
+
}
|
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
|
+
}
|