@ebowwa/terminal 0.3.0 → 0.3.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/api.d.ts +7 -0
- package/dist/client.d.ts +14 -0
- package/dist/config.d.ts +85 -0
- package/dist/error.d.ts +7 -0
- package/dist/exec.d.ts +46 -0
- package/dist/files.d.ts +123 -0
- package/dist/fingerprint.d.ts +66 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +5722 -6207
- package/dist/manager.d.ts +102 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.js +6160 -6717
- package/dist/mcp/stdio.d.ts +8 -0
- package/dist/network-error-detector.d.ts +18 -0
- package/dist/pool.d.ts +142 -0
- package/dist/pty.d.ts +58 -0
- package/dist/resources.d.ts +62 -0
- package/dist/scp.d.ts +29 -0
- package/dist/sessions.d.ts +100 -0
- package/dist/tmux-exec.d.ts +49 -0
- package/dist/tmux-local.d.ts +272 -0
- package/dist/tmux-manager.d.ts +327 -0
- package/dist/tmux.d.ts +212 -0
- package/dist/types.d.ts +17 -0
- package/mcp/package.json +1 -1
- package/package.json +7 -7
- package/src/api.js +861 -0
- package/src/client.js +92 -0
- package/src/config.js +490 -0
- package/src/error.js +32 -0
- package/src/exec.js +183 -0
- package/src/files.js +521 -0
- package/src/fingerprint.js +336 -0
- package/src/index.js +127 -0
- package/src/manager.js +358 -0
- package/src/mcp/index.js +555 -0
- package/src/mcp/stdio.js +840 -0
- package/src/network-error-detector.js +101 -0
- package/src/pool.js +840 -0
- package/src/pty.js +344 -0
- package/src/resources.js +64 -0
- package/src/scp.js +166 -0
- package/src/sessions.js +895 -0
- package/src/tmux-exec.js +169 -0
- package/src/tmux-local.js +937 -0
- package/src/tmux-manager.js +1026 -0
- package/src/tmux.js +826 -0
- package/src/tmux.ts +1 -1
- package/src/types.js +5 -0
- package/tsconfig.json +28 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core SSH client for executing commands on remote servers
|
|
4
|
+
* Uses persistent connection pool for efficient reuse
|
|
5
|
+
* Uses base64 encoding to avoid shell escaping issues
|
|
6
|
+
*/
|
|
7
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
8
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
9
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
11
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
12
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
13
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
17
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
18
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
19
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
20
|
+
function step(op) {
|
|
21
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
22
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
23
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
24
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
25
|
+
switch (op[0]) {
|
|
26
|
+
case 0: case 1: t = op; break;
|
|
27
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
28
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
29
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
30
|
+
default:
|
|
31
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
32
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
33
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
34
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
35
|
+
if (t[2]) _.ops.pop();
|
|
36
|
+
_.trys.pop(); continue;
|
|
37
|
+
}
|
|
38
|
+
op = body.call(thisArg, _);
|
|
39
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
40
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.execSSH = execSSH;
|
|
45
|
+
var error_js_1 = require("./error.js");
|
|
46
|
+
var ssh_1 = require("@ebowwa/codespaces-types/runtime/ssh");
|
|
47
|
+
var pool_js_1 = require("./pool.js");
|
|
48
|
+
/**
|
|
49
|
+
* Execute a command on a remote server via SSH
|
|
50
|
+
* Uses persistent connection pool for better performance
|
|
51
|
+
* @param command - Shell command to execute
|
|
52
|
+
* @param options - SSH connection options
|
|
53
|
+
* @returns Command output as string
|
|
54
|
+
*/
|
|
55
|
+
function execSSH(command, options) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
57
|
+
var validatedCommand, validatedOptions, _a, host, _b, user, _c, timeout, _d, port, keyPath, password, pool, output, error_1;
|
|
58
|
+
return __generator(this, function (_e) {
|
|
59
|
+
switch (_e.label) {
|
|
60
|
+
case 0:
|
|
61
|
+
validatedCommand = ssh_1.SSHCommandSchema.safeParse(command);
|
|
62
|
+
if (!validatedCommand.success) {
|
|
63
|
+
throw new Error("Invalid SSH command: ".concat(validatedCommand.error.issues.map(function (i) { return i.message; }).join(', ')));
|
|
64
|
+
}
|
|
65
|
+
validatedOptions = ssh_1.SSHOptionsSchema.safeParse(options);
|
|
66
|
+
if (!validatedOptions.success) {
|
|
67
|
+
throw new Error("Invalid SSH options: ".concat(validatedOptions.error.issues.map(function (i) { return i.message; }).join(', ')));
|
|
68
|
+
}
|
|
69
|
+
_a = validatedOptions.data, host = _a.host, _b = _a.user, user = _b === void 0 ? "root" : _b, _c = _a.timeout, timeout = _c === void 0 ? 5 : _c, _d = _a.port, port = _d === void 0 ? 22 : _d, keyPath = _a.keyPath, password = _a.password;
|
|
70
|
+
_e.label = 1;
|
|
71
|
+
case 1:
|
|
72
|
+
_e.trys.push([1, 3, , 4]);
|
|
73
|
+
pool = (0, pool_js_1.getSSHPool)();
|
|
74
|
+
return [4 /*yield*/, pool.exec(validatedCommand.data, {
|
|
75
|
+
host: host,
|
|
76
|
+
user: user,
|
|
77
|
+
timeout: timeout,
|
|
78
|
+
port: port,
|
|
79
|
+
keyPath: keyPath,
|
|
80
|
+
password: password,
|
|
81
|
+
})];
|
|
82
|
+
case 2:
|
|
83
|
+
output = _e.sent();
|
|
84
|
+
return [2 /*return*/, output || "0"];
|
|
85
|
+
case 3:
|
|
86
|
+
error_1 = _e.sent();
|
|
87
|
+
throw new error_js_1.SSHError("SSH command failed: ".concat(validatedCommand.data), error_1);
|
|
88
|
+
case 4: return [2 /*return*/];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSH Config Manager
|
|
4
|
+
*
|
|
5
|
+
* Manages ~/.ssh/config entries for easy node access.
|
|
6
|
+
* When a node is created, adds an alias so you can:
|
|
7
|
+
* ssh node-<id> or ssh <name>
|
|
8
|
+
* Instead of:
|
|
9
|
+
* ssh -i ~/.../key -o StrictHostKeyChecking=no root@167.235.236.8
|
|
10
|
+
*/
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
21
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
22
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
23
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
24
|
+
function step(op) {
|
|
25
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
26
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
27
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
28
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
29
|
+
switch (op[0]) {
|
|
30
|
+
case 0: case 1: t = op; break;
|
|
31
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
32
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
33
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
34
|
+
default:
|
|
35
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
36
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
37
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
38
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
39
|
+
if (t[2]) _.ops.pop();
|
|
40
|
+
_.trys.pop(); continue;
|
|
41
|
+
}
|
|
42
|
+
op = body.call(thisArg, _);
|
|
43
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
44
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.addSSHConfigEntry = addSSHConfigEntry;
|
|
49
|
+
exports.removeSSHConfigEntry = removeSSHConfigEntry;
|
|
50
|
+
exports.updateSSHConfigHost = updateSSHConfigHost;
|
|
51
|
+
exports.listSSHConfigEntries = listSSHConfigEntries;
|
|
52
|
+
exports.validateSSHConnection = validateSSHConnection;
|
|
53
|
+
exports.ensureCorrectSSHKey = ensureCorrectSSHKey;
|
|
54
|
+
exports.waitForSSHReady = waitForSSHReady;
|
|
55
|
+
exports.syncNodesToSSHConfig = syncNodesToSSHConfig;
|
|
56
|
+
var fs_1 = require("fs");
|
|
57
|
+
var path_1 = require("path");
|
|
58
|
+
var child_process_1 = require("child_process");
|
|
59
|
+
var util_1 = require("util");
|
|
60
|
+
var execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
61
|
+
var SSH_CONFIG_PATH = (0, path_1.join)(process.env.HOME || "~", ".ssh", "config");
|
|
62
|
+
// Marker comments to identify our managed entries
|
|
63
|
+
var BLOCK_START = "# >>> hetzner-codespaces managed";
|
|
64
|
+
var BLOCK_END = "# <<< hetzner-codespaces managed";
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a path to absolute, handling relative paths
|
|
67
|
+
*/
|
|
68
|
+
function resolveKeyPath(keyPath) {
|
|
69
|
+
if ((0, path_1.isAbsolute)(keyPath)) {
|
|
70
|
+
return keyPath;
|
|
71
|
+
}
|
|
72
|
+
// Try to resolve relative to current working directory
|
|
73
|
+
var resolved = (0, path_1.resolve)(process.cwd(), keyPath);
|
|
74
|
+
// If file exists at resolved path, use it
|
|
75
|
+
if ((0, fs_1.existsSync)(resolved)) {
|
|
76
|
+
try {
|
|
77
|
+
return (0, fs_1.realpathSync)(resolved);
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Return as-is if we can't resolve it
|
|
84
|
+
return keyPath;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the current SSH config file
|
|
88
|
+
*/
|
|
89
|
+
function readSSHConfig() {
|
|
90
|
+
if (!(0, fs_1.existsSync)(SSH_CONFIG_PATH)) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
return (0, fs_1.readFileSync)(SSH_CONFIG_PATH, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Write to SSH config file, ensuring proper permissions
|
|
97
|
+
*/
|
|
98
|
+
function writeSSHConfig(content) {
|
|
99
|
+
var sshDir = (0, path_1.dirname)(SSH_CONFIG_PATH);
|
|
100
|
+
// Ensure ~/.ssh exists with correct permissions
|
|
101
|
+
if (!(0, fs_1.existsSync)(sshDir)) {
|
|
102
|
+
(0, fs_1.mkdirSync)(sshDir, { mode: 448, recursive: true });
|
|
103
|
+
}
|
|
104
|
+
(0, fs_1.writeFileSync)(SSH_CONFIG_PATH, content, { mode: 384 });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Extract our managed block from SSH config
|
|
108
|
+
*/
|
|
109
|
+
function extractManagedBlock(config) {
|
|
110
|
+
var startIdx = config.indexOf(BLOCK_START);
|
|
111
|
+
var endIdx = config.indexOf(BLOCK_END);
|
|
112
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
113
|
+
return { before: config, managed: "", after: "" };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
before: config.substring(0, startIdx),
|
|
117
|
+
managed: config.substring(startIdx, endIdx + BLOCK_END.length + 1),
|
|
118
|
+
after: config.substring(endIdx + BLOCK_END.length + 1),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse managed block into entries
|
|
123
|
+
*/
|
|
124
|
+
function parseManagedEntries(managed) {
|
|
125
|
+
var entries = new Map();
|
|
126
|
+
// Match Host blocks within managed section
|
|
127
|
+
var hostRegex = /# node-id: (\S+)\nHost ([^\n]+)\n([\s\S]*?)(?=# node-id:|$)/g;
|
|
128
|
+
var match;
|
|
129
|
+
while ((match = hostRegex.exec(managed)) !== null) {
|
|
130
|
+
var id = match[1];
|
|
131
|
+
var hosts = match[2].trim();
|
|
132
|
+
var body = match[3];
|
|
133
|
+
// Parse body for HostName, User, IdentityFile, Port
|
|
134
|
+
var hostMatch = body.match(/HostName\s+(\S+)/);
|
|
135
|
+
var userMatch = body.match(/User\s+(\S+)/);
|
|
136
|
+
var keyMatch = body.match(/IdentityFile\s+(\S+)/);
|
|
137
|
+
var portMatch = body.match(/Port\s+(\d+)/);
|
|
138
|
+
if (hostMatch && keyMatch) {
|
|
139
|
+
entries.set(id, {
|
|
140
|
+
id: id,
|
|
141
|
+
name: hosts.split(/\s+/)[1] || hosts, // Second alias is usually the name
|
|
142
|
+
host: hostMatch[1],
|
|
143
|
+
user: (userMatch === null || userMatch === void 0 ? void 0 : userMatch[1]) || "root",
|
|
144
|
+
keyPath: keyMatch[1],
|
|
145
|
+
port: portMatch ? parseInt(portMatch[1]) : 22,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return entries;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Generate SSH config block for an entry
|
|
153
|
+
*/
|
|
154
|
+
function generateEntryBlock(entry) {
|
|
155
|
+
// Create aliases: node-<id> and <name> (sanitized)
|
|
156
|
+
var sanitizedName = entry.name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
157
|
+
var aliases = "node-".concat(entry.id, " ").concat(sanitizedName);
|
|
158
|
+
// Resolve key path to absolute
|
|
159
|
+
var absoluteKeyPath = resolveKeyPath(entry.keyPath);
|
|
160
|
+
return "# node-id: ".concat(entry.id, "\nHost ").concat(aliases, "\n HostName ").concat(entry.host, "\n User ").concat(entry.user || "root", "\n IdentityFile \"").concat(absoluteKeyPath, "\"\n Port ").concat(entry.port || 22, "\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n LogLevel ERROR\n IdentitiesOnly yes\n\n");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Rebuild the managed block from entries
|
|
164
|
+
*/
|
|
165
|
+
function buildManagedBlock(entries) {
|
|
166
|
+
if (entries.size === 0) {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
var block = "".concat(BLOCK_START, "\n");
|
|
170
|
+
block += "# Auto-generated SSH aliases for Hetzner nodes\n";
|
|
171
|
+
block += "# Do not edit manually - changes will be overwritten\n\n";
|
|
172
|
+
for (var _i = 0, _a = entries.values(); _i < _a.length; _i++) {
|
|
173
|
+
var entry = _a[_i];
|
|
174
|
+
block += generateEntryBlock(entry);
|
|
175
|
+
}
|
|
176
|
+
block += "".concat(BLOCK_END, "\n");
|
|
177
|
+
return block;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Add or update an SSH config entry for a node
|
|
181
|
+
*/
|
|
182
|
+
function addSSHConfigEntry(entry) {
|
|
183
|
+
var config = readSSHConfig();
|
|
184
|
+
var _a = extractManagedBlock(config), before = _a.before, managed = _a.managed, after = _a.after;
|
|
185
|
+
// Parse existing entries
|
|
186
|
+
var entries = parseManagedEntries(managed);
|
|
187
|
+
// Add/update entry
|
|
188
|
+
entries.set(entry.id, entry);
|
|
189
|
+
// Rebuild config
|
|
190
|
+
var newManaged = buildManagedBlock(entries);
|
|
191
|
+
var newConfig = before.trimEnd() + "\n\n" + newManaged + after.trimStart();
|
|
192
|
+
writeSSHConfig(newConfig);
|
|
193
|
+
console.log("[SSH Config] Added alias: ssh node-".concat(entry.id, " / ssh ").concat(entry.name.replace(/[^a-zA-Z0-9_-]/g, "-")));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Remove an SSH config entry for a node
|
|
197
|
+
*/
|
|
198
|
+
function removeSSHConfigEntry(id) {
|
|
199
|
+
var config = readSSHConfig();
|
|
200
|
+
var _a = extractManagedBlock(config), before = _a.before, managed = _a.managed, after = _a.after;
|
|
201
|
+
// Parse existing entries
|
|
202
|
+
var entries = parseManagedEntries(managed);
|
|
203
|
+
// Remove entry
|
|
204
|
+
if (!entries.has(id)) {
|
|
205
|
+
return; // Nothing to remove
|
|
206
|
+
}
|
|
207
|
+
entries.delete(id);
|
|
208
|
+
// Rebuild config
|
|
209
|
+
var newManaged = buildManagedBlock(entries);
|
|
210
|
+
var newConfig = before.trimEnd() + (newManaged ? "\n\n" + newManaged : "") + after.trimStart();
|
|
211
|
+
writeSSHConfig(newConfig);
|
|
212
|
+
console.log("[SSH Config] Removed alias for node-".concat(id));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Update IP address for an existing node (e.g., after rebuild)
|
|
216
|
+
*/
|
|
217
|
+
function updateSSHConfigHost(id, newHost) {
|
|
218
|
+
var config = readSSHConfig();
|
|
219
|
+
var _a = extractManagedBlock(config), before = _a.before, managed = _a.managed, after = _a.after;
|
|
220
|
+
// Parse existing entries
|
|
221
|
+
var entries = parseManagedEntries(managed);
|
|
222
|
+
// Update entry
|
|
223
|
+
var entry = entries.get(id);
|
|
224
|
+
if (!entry) {
|
|
225
|
+
console.warn("[SSH Config] No entry found for node-".concat(id));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
entry.host = newHost;
|
|
229
|
+
entries.set(id, entry);
|
|
230
|
+
// Rebuild config
|
|
231
|
+
var newManaged = buildManagedBlock(entries);
|
|
232
|
+
var newConfig = before.trimEnd() + "\n\n" + newManaged + after.trimStart();
|
|
233
|
+
writeSSHConfig(newConfig);
|
|
234
|
+
console.log("[SSH Config] Updated node-".concat(id, " host to ").concat(newHost));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* List all managed SSH config entries
|
|
238
|
+
*/
|
|
239
|
+
function listSSHConfigEntries() {
|
|
240
|
+
var config = readSSHConfig();
|
|
241
|
+
var managed = extractManagedBlock(config).managed;
|
|
242
|
+
var entries = parseManagedEntries(managed);
|
|
243
|
+
return Array.from(entries.values());
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Validate SSH connection works with the configured key
|
|
247
|
+
* Returns true if connection succeeds, throws on failure with diagnostic info
|
|
248
|
+
*/
|
|
249
|
+
function validateSSHConnection(host_1, keyPath_1) {
|
|
250
|
+
return __awaiter(this, arguments, void 0, function (host, keyPath, user, timeoutSeconds) {
|
|
251
|
+
var cmd, stdout, error_1, diagnostics, agentKeys, _a;
|
|
252
|
+
if (user === void 0) { user = "root"; }
|
|
253
|
+
if (timeoutSeconds === void 0) { timeoutSeconds = 10; }
|
|
254
|
+
return __generator(this, function (_b) {
|
|
255
|
+
switch (_b.label) {
|
|
256
|
+
case 0:
|
|
257
|
+
_b.trys.push([0, 2, , 7]);
|
|
258
|
+
cmd = [
|
|
259
|
+
"ssh",
|
|
260
|
+
"-o", "StrictHostKeyChecking=no",
|
|
261
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
262
|
+
"-o",
|
|
263
|
+
"ConnectTimeout=".concat(timeoutSeconds),
|
|
264
|
+
"-o", "IdentitiesOnly=yes", // Only use specified key, not ssh-agent
|
|
265
|
+
"-o", "BatchMode=yes", // Fail instead of prompting for password
|
|
266
|
+
"-i", keyPath,
|
|
267
|
+
"".concat(user, "@").concat(host),
|
|
268
|
+
"echo CONNECTION_OK"
|
|
269
|
+
].join(" ");
|
|
270
|
+
return [4 /*yield*/, execAsync(cmd, { timeout: (timeoutSeconds + 5) * 1000 })];
|
|
271
|
+
case 1:
|
|
272
|
+
stdout = (_b.sent()).stdout;
|
|
273
|
+
if (stdout.includes("CONNECTION_OK")) {
|
|
274
|
+
return [2 /*return*/, { success: true }];
|
|
275
|
+
}
|
|
276
|
+
return [2 /*return*/, {
|
|
277
|
+
success: false,
|
|
278
|
+
error: "Connection established but test command failed",
|
|
279
|
+
diagnostics: stdout
|
|
280
|
+
}];
|
|
281
|
+
case 2:
|
|
282
|
+
error_1 = _b.sent();
|
|
283
|
+
diagnostics = "";
|
|
284
|
+
// Check if key file exists
|
|
285
|
+
if (!(0, fs_1.existsSync)(keyPath)) {
|
|
286
|
+
diagnostics += "Key file missing: ".concat(keyPath, "\n");
|
|
287
|
+
}
|
|
288
|
+
_b.label = 3;
|
|
289
|
+
case 3:
|
|
290
|
+
_b.trys.push([3, 5, , 6]);
|
|
291
|
+
return [4 /*yield*/, execAsync("ssh-add -l 2>&1")];
|
|
292
|
+
case 4:
|
|
293
|
+
agentKeys = (_b.sent()).stdout;
|
|
294
|
+
diagnostics += "ssh-agent keys:\n".concat(agentKeys, "\n");
|
|
295
|
+
return [3 /*break*/, 6];
|
|
296
|
+
case 5:
|
|
297
|
+
_a = _b.sent();
|
|
298
|
+
diagnostics += "ssh-agent: no keys loaded\n";
|
|
299
|
+
return [3 /*break*/, 6];
|
|
300
|
+
case 6: return [2 /*return*/, {
|
|
301
|
+
success: false,
|
|
302
|
+
error: error_1.message || String(error_1),
|
|
303
|
+
diagnostics: diagnostics,
|
|
304
|
+
}];
|
|
305
|
+
case 7: return [2 /*return*/];
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Ensure SSH key is loaded correctly and agent doesn't interfere
|
|
312
|
+
* Clears wrong keys from agent and adds the correct one
|
|
313
|
+
*/
|
|
314
|
+
function ensureCorrectSSHKey(keyPath) {
|
|
315
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
316
|
+
var ourFingerprint, ourFp, agentList, error_2;
|
|
317
|
+
return __generator(this, function (_a) {
|
|
318
|
+
switch (_a.label) {
|
|
319
|
+
case 0:
|
|
320
|
+
_a.trys.push([0, 6, , 7]);
|
|
321
|
+
return [4 /*yield*/, execAsync("ssh-keygen -lf \"".concat(keyPath, ".pub\""))];
|
|
322
|
+
case 1:
|
|
323
|
+
ourFingerprint = (_a.sent()).stdout;
|
|
324
|
+
ourFp = ourFingerprint.split(/\s+/)[1];
|
|
325
|
+
return [4 /*yield*/, execAsync("ssh-add -l 2>&1").catch(function () { return ({ stdout: "" }); })];
|
|
326
|
+
case 2:
|
|
327
|
+
agentList = (_a.sent()).stdout;
|
|
328
|
+
if (!!agentList.includes(ourFp)) return [3 /*break*/, 5];
|
|
329
|
+
// Clear agent and add our key
|
|
330
|
+
return [4 /*yield*/, execAsync("ssh-add -D 2>/dev/null").catch(function () { })];
|
|
331
|
+
case 3:
|
|
332
|
+
// Clear agent and add our key
|
|
333
|
+
_a.sent();
|
|
334
|
+
return [4 /*yield*/, execAsync("ssh-add \"".concat(keyPath, "\""))];
|
|
335
|
+
case 4:
|
|
336
|
+
_a.sent();
|
|
337
|
+
console.log("[SSH] Added key to ssh-agent: ".concat(keyPath));
|
|
338
|
+
_a.label = 5;
|
|
339
|
+
case 5: return [3 /*break*/, 7];
|
|
340
|
+
case 6:
|
|
341
|
+
error_2 = _a.sent();
|
|
342
|
+
// Non-fatal - we can still use the key file directly
|
|
343
|
+
console.warn("[SSH] Could not configure ssh-agent: ".concat(error_2));
|
|
344
|
+
return [3 /*break*/, 7];
|
|
345
|
+
case 7: return [2 /*return*/];
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Wait for SSH to become ready on a new server
|
|
352
|
+
* Polls until connection succeeds or timeout
|
|
353
|
+
*/
|
|
354
|
+
function waitForSSHReady(host_1, keyPath_1) {
|
|
355
|
+
return __awaiter(this, arguments, void 0, function (host, keyPath, options) {
|
|
356
|
+
var _a, user, _b, maxAttempts, _c, intervalMs, onAttempt, attempt, result;
|
|
357
|
+
var _d;
|
|
358
|
+
if (options === void 0) { options = {}; }
|
|
359
|
+
return __generator(this, function (_e) {
|
|
360
|
+
switch (_e.label) {
|
|
361
|
+
case 0:
|
|
362
|
+
_a = options.user, user = _a === void 0 ? "root" : _a, _b = options.maxAttempts, maxAttempts = _b === void 0 ? 30 : _b, _c = options.intervalMs, intervalMs = _c === void 0 ? 5000 : _c, onAttempt = options.onAttempt;
|
|
363
|
+
attempt = 1;
|
|
364
|
+
_e.label = 1;
|
|
365
|
+
case 1:
|
|
366
|
+
if (!(attempt <= maxAttempts)) return [3 /*break*/, 5];
|
|
367
|
+
onAttempt === null || onAttempt === void 0 ? void 0 : onAttempt(attempt, maxAttempts);
|
|
368
|
+
return [4 /*yield*/, validateSSHConnection(host, keyPath, user, 5)];
|
|
369
|
+
case 2:
|
|
370
|
+
result = _e.sent();
|
|
371
|
+
if (result.success) {
|
|
372
|
+
return [2 /*return*/, { success: true, attempts: attempt }];
|
|
373
|
+
}
|
|
374
|
+
// Check for fatal errors (not just "connection refused")
|
|
375
|
+
if ((_d = result.error) === null || _d === void 0 ? void 0 : _d.includes("Permission denied")) {
|
|
376
|
+
// Key mismatch - won't resolve by waiting
|
|
377
|
+
return [2 /*return*/, {
|
|
378
|
+
success: false,
|
|
379
|
+
attempts: attempt,
|
|
380
|
+
error: "SSH key rejected: ".concat(result.error, "\n").concat(result.diagnostics || ""),
|
|
381
|
+
}];
|
|
382
|
+
}
|
|
383
|
+
if (!(attempt < maxAttempts)) return [3 /*break*/, 4];
|
|
384
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, intervalMs); })];
|
|
385
|
+
case 3:
|
|
386
|
+
_e.sent();
|
|
387
|
+
_e.label = 4;
|
|
388
|
+
case 4:
|
|
389
|
+
attempt++;
|
|
390
|
+
return [3 /*break*/, 1];
|
|
391
|
+
case 5: return [2 /*return*/, {
|
|
392
|
+
success: false,
|
|
393
|
+
attempts: maxAttempts,
|
|
394
|
+
error: "SSH not ready after ".concat(maxAttempts, " attempts (").concat((maxAttempts * intervalMs) / 1000, "s)"),
|
|
395
|
+
}];
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Sync all existing Hetzner nodes to SSH config
|
|
402
|
+
* Call this to add aliases for nodes created before this feature
|
|
403
|
+
*/
|
|
404
|
+
function syncNodesToSSHConfig(nodes_1) {
|
|
405
|
+
return __awaiter(this, arguments, void 0, function (nodes, options) {
|
|
406
|
+
var _a, validateSSH, onProgress, results, existingEntries, existingIds, _loop_1, _i, nodes_2, node;
|
|
407
|
+
if (options === void 0) { options = {}; }
|
|
408
|
+
return __generator(this, function (_b) {
|
|
409
|
+
switch (_b.label) {
|
|
410
|
+
case 0:
|
|
411
|
+
_a = options.validateSSH, validateSSH = _a === void 0 ? false : _a, onProgress = options.onProgress;
|
|
412
|
+
results = [];
|
|
413
|
+
existingEntries = listSSHConfigEntries();
|
|
414
|
+
existingIds = new Set(existingEntries.map(function (e) { return e.id; }));
|
|
415
|
+
_loop_1 = function (node) {
|
|
416
|
+
var result, existing, sshResult, error_3;
|
|
417
|
+
return __generator(this, function (_c) {
|
|
418
|
+
switch (_c.label) {
|
|
419
|
+
case 0:
|
|
420
|
+
result = {
|
|
421
|
+
id: node.id,
|
|
422
|
+
name: node.name,
|
|
423
|
+
ip: node.ip,
|
|
424
|
+
status: "added",
|
|
425
|
+
};
|
|
426
|
+
_c.label = 1;
|
|
427
|
+
case 1:
|
|
428
|
+
_c.trys.push([1, 4, , 5]);
|
|
429
|
+
// Check if already exists
|
|
430
|
+
if (existingIds.has(node.id)) {
|
|
431
|
+
existing = existingEntries.find(function (e) { return e.id === node.id; });
|
|
432
|
+
if ((existing === null || existing === void 0 ? void 0 : existing.host) === node.ip) {
|
|
433
|
+
result.status = "skipped";
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// IP changed, update it
|
|
437
|
+
updateSSHConfigHost(node.id, node.ip);
|
|
438
|
+
result.status = "updated";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Add new entry
|
|
443
|
+
addSSHConfigEntry({
|
|
444
|
+
id: node.id,
|
|
445
|
+
name: node.name,
|
|
446
|
+
host: node.ip,
|
|
447
|
+
user: "root",
|
|
448
|
+
keyPath: node.keyPath,
|
|
449
|
+
});
|
|
450
|
+
result.status = "added";
|
|
451
|
+
}
|
|
452
|
+
if (!(validateSSH && result.status !== "skipped")) return [3 /*break*/, 3];
|
|
453
|
+
return [4 /*yield*/, validateSSHConnection(node.ip, node.keyPath)];
|
|
454
|
+
case 2:
|
|
455
|
+
sshResult = _c.sent();
|
|
456
|
+
result.sshReady = sshResult.success;
|
|
457
|
+
if (!sshResult.success) {
|
|
458
|
+
result.error = sshResult.error;
|
|
459
|
+
}
|
|
460
|
+
_c.label = 3;
|
|
461
|
+
case 3: return [3 /*break*/, 5];
|
|
462
|
+
case 4:
|
|
463
|
+
error_3 = _c.sent();
|
|
464
|
+
result.status = "error";
|
|
465
|
+
result.error = String(error_3);
|
|
466
|
+
return [3 /*break*/, 5];
|
|
467
|
+
case 5:
|
|
468
|
+
results.push(result);
|
|
469
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(result);
|
|
470
|
+
return [2 /*return*/];
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
_i = 0, nodes_2 = nodes;
|
|
475
|
+
_b.label = 1;
|
|
476
|
+
case 1:
|
|
477
|
+
if (!(_i < nodes_2.length)) return [3 /*break*/, 4];
|
|
478
|
+
node = nodes_2[_i];
|
|
479
|
+
return [5 /*yield**/, _loop_1(node)];
|
|
480
|
+
case 2:
|
|
481
|
+
_b.sent();
|
|
482
|
+
_b.label = 3;
|
|
483
|
+
case 3:
|
|
484
|
+
_i++;
|
|
485
|
+
return [3 /*break*/, 1];
|
|
486
|
+
case 4: return [2 /*return*/, results];
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
package/src/error.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSH error class
|
|
4
|
+
*/
|
|
5
|
+
var __extends = (this && this.__extends) || (function () {
|
|
6
|
+
var extendStatics = function (d, b) {
|
|
7
|
+
extendStatics = Object.setPrototypeOf ||
|
|
8
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
9
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
10
|
+
return extendStatics(d, b);
|
|
11
|
+
};
|
|
12
|
+
return function (d, b) {
|
|
13
|
+
if (typeof b !== "function" && b !== null)
|
|
14
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
15
|
+
extendStatics(d, b);
|
|
16
|
+
function __() { this.constructor = d; }
|
|
17
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SSHError = void 0;
|
|
22
|
+
var SSHError = /** @class */ (function (_super) {
|
|
23
|
+
__extends(SSHError, _super);
|
|
24
|
+
function SSHError(message, cause) {
|
|
25
|
+
var _this = _super.call(this, message) || this;
|
|
26
|
+
_this.cause = cause;
|
|
27
|
+
_this.name = "SSHError";
|
|
28
|
+
return _this;
|
|
29
|
+
}
|
|
30
|
+
return SSHError;
|
|
31
|
+
}(Error));
|
|
32
|
+
exports.SSHError = SSHError;
|