@ebowwa/terminal 0.2.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.
- package/dist/client.d.ts +15 -0
- package/dist/client.js +45 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.js +12 -0
- package/dist/exec.d.ts +47 -0
- package/dist/exec.js +107 -0
- package/dist/files.d.ts +124 -0
- package/dist/files.js +436 -0
- package/dist/fingerprint.d.ts +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/pool.d.ts +143 -0
- package/dist/pool.js +554 -0
- package/dist/pty.d.ts +59 -0
- package/dist/scp.d.ts +30 -0
- package/dist/scp.js +74 -0
- package/dist/sessions.d.ts +98 -0
- package/dist/tmux-exec.d.ts +50 -0
- package/dist/tmux.d.ts +213 -0
- package/dist/tmux.js +528 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +5 -0
- package/ebowwa-terminal-0.2.0.tgz +0 -0
- package/mcp/README.md +181 -0
- package/mcp/package.json +34 -0
- package/mcp/test-fix.sh +273 -0
- package/package.json +118 -0
- package/src/api.ts +752 -0
- package/src/client.ts +55 -0
- package/src/config.ts +489 -0
- package/src/error.ts +13 -0
- package/src/exec.ts +128 -0
- package/src/files.ts +636 -0
- package/src/fingerprint.ts +263 -0
- package/src/index.ts +144 -0
- package/src/manager.ts +319 -0
- package/src/mcp/index.ts +467 -0
- package/src/mcp/stdio.ts +708 -0
- package/src/network-error-detector.ts +121 -0
- package/src/pool.ts +662 -0
- package/src/pty.ts +285 -0
- package/src/scp.ts +109 -0
- package/src/sessions.ts +861 -0
- package/src/tmux-exec.ts +96 -0
- package/src/tmux-local.ts +839 -0
- package/src/tmux-manager.ts +962 -0
- package/src/tmux.ts +711 -0
- package/src/types.ts +19 -0
package/src/manager.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Key Manager - Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* Consolidates SSH key creation, upload, and management for Hetzner.
|
|
5
|
+
* Replaces duplicate ensureSSHKey() functions in api.ts and crud.ts.
|
|
6
|
+
*
|
|
7
|
+
* FEATURES:
|
|
8
|
+
* - Create or reuse local SSH key pairs
|
|
9
|
+
* - Upload public key to Hetzner if not exists
|
|
10
|
+
* - Handle fingerprint format conversion (SHA256 <-> MD5)
|
|
11
|
+
* - Provide consistent API for all SSH key operations
|
|
12
|
+
* - OS-specific user data directory for portable key storage
|
|
13
|
+
*
|
|
14
|
+
* ENVIRONMENT VARIABLES:
|
|
15
|
+
* - HETZNER_SSH_KEYS_DIR: Override default SSH keys directory
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { exec } from "child_process";
|
|
19
|
+
import { promisify } from "util";
|
|
20
|
+
import { existsSync, mkdirSync } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
import type { HetznerClient } from "../lib/hetzner/client";
|
|
23
|
+
import type { HetznerSSHKey } from "../lib/hetzner/types";
|
|
24
|
+
import { getLocalKeyFingerprint } from "./fingerprint.js";
|
|
25
|
+
|
|
26
|
+
// TypeScript declaration for environment variable
|
|
27
|
+
declare module "bun" {
|
|
28
|
+
interface Env {
|
|
29
|
+
HETZNER_SSH_KEYS_DIR?: string;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const execAsync = promisify(exec);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get OS-specific user data directory for SSH keys
|
|
37
|
+
* - macOS: ~/Library/Application Support/com.hetzner.codespaces/ssh-keys/
|
|
38
|
+
* - Linux: ~/.config/com.hetzner.codespaces/ssh-keys/
|
|
39
|
+
* - Windows: %APPDATA%\com.hetzner.codespaces\ssh-keys\
|
|
40
|
+
*
|
|
41
|
+
* Can be overridden via .env file:
|
|
42
|
+
* HETZNER_SSH_KEYS_DIR=/custom/path
|
|
43
|
+
*/
|
|
44
|
+
export function getDefaultKeysDir(): string {
|
|
45
|
+
// Allow override via environment variable (Bun reads .env automatically)
|
|
46
|
+
const envDir = Bun.env.HETZNER_SSH_KEYS_DIR || process.env.HETZNER_SSH_KEYS_DIR;
|
|
47
|
+
if (envDir) {
|
|
48
|
+
return envDir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
52
|
+
const appName = "com.hetzner.codespaces";
|
|
53
|
+
|
|
54
|
+
if (process.platform === "darwin") {
|
|
55
|
+
return join(home, "Library", "Application Support", appName, "ssh-keys");
|
|
56
|
+
} else if (process.platform === "linux") {
|
|
57
|
+
return join(home, ".config", appName, "ssh-keys");
|
|
58
|
+
} else if (process.platform === "win32") {
|
|
59
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
60
|
+
return join(appData, appName, "ssh-keys");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Universal fallback
|
|
64
|
+
return join(home, `.${appName}`, "ssh-keys");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SSH Key information returned by the manager
|
|
69
|
+
*/
|
|
70
|
+
export interface SSHKeyInfo {
|
|
71
|
+
keyId: number;
|
|
72
|
+
keyPath: string;
|
|
73
|
+
fingerprint: string;
|
|
74
|
+
name: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Configuration for SSH key manager
|
|
79
|
+
*/
|
|
80
|
+
export interface SSHKeyManagerConfig {
|
|
81
|
+
keyName?: string;
|
|
82
|
+
keysDir?: string; // Optional: defaults to OS-specific user data directory
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default SSH key configuration
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_KEY_NAME = "hetzner-codespaces-default";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fingerprint format types
|
|
92
|
+
*/
|
|
93
|
+
export type FingerprintFormat = "md5" | "sha256";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert SSH key fingerprint between formats
|
|
97
|
+
* Hetzner uses MD5 with colons, modern tools use SHA256
|
|
98
|
+
*/
|
|
99
|
+
export async function convertFingerprintFormat(
|
|
100
|
+
publicKeyPath: string,
|
|
101
|
+
format: FingerprintFormat
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
const flag = format === "md5" ? "-E md5" : "";
|
|
104
|
+
const { stdout } = await execAsync(`ssh-keygen -l -f "${publicKeyPath}" ${flag}`);
|
|
105
|
+
|
|
106
|
+
// Extract fingerprint from output
|
|
107
|
+
// MD5 format: "256 MD5:13:ac:a1:ea:4e:ce:9e:8c:32:1a:9e:23:d8:1c:e4:93 comment (ED25519)"
|
|
108
|
+
// SHA256 format: "256 SHA256:+JgBxEtEo1NSfUtA+Lcy7hZrh7nb6j/oo6hmP7x8Hjw comment (ED25519)"
|
|
109
|
+
const parts = stdout.trim().split(/\s+/);
|
|
110
|
+
let fingerprint = parts[1];
|
|
111
|
+
|
|
112
|
+
// Remove "MD5:" prefix if present (Hetzner API returns it without prefix)
|
|
113
|
+
if (fingerprint.startsWith("MD5:")) {
|
|
114
|
+
fingerprint = fingerprint.replace("MD5:", "");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return fingerprint;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get SSH key fingerprint in the format expected by Hetzner API (MD5 with colons)
|
|
122
|
+
*/
|
|
123
|
+
async function getHetznerFingerprint(publicKeyPath: string): Promise<string> {
|
|
124
|
+
return convertFingerprintFormat(publicKeyPath, "md5");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get SSH key fingerprint in modern SHA256 format (for local use)
|
|
129
|
+
*/
|
|
130
|
+
async function getLocalFingerprint(publicKeyPath: string): Promise<string> {
|
|
131
|
+
return convertFingerprintFormat(publicKeyPath, "sha256");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a new SSH key pair
|
|
136
|
+
*/
|
|
137
|
+
async function createKeyPair(keyPath: string, keyName: string): Promise<void> {
|
|
138
|
+
console.log(`[SSH] Generating new SSH key pair: ${keyPath}`);
|
|
139
|
+
// Quote the path to handle spaces in directory names (e.g., "Application Support")
|
|
140
|
+
await execAsync(
|
|
141
|
+
`ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "${keyName}"`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find SSH key in Hetzner by fingerprint
|
|
147
|
+
*/
|
|
148
|
+
async function findKeyByFingerprint(
|
|
149
|
+
hetznerClient: HetznerClient,
|
|
150
|
+
fingerprint: string
|
|
151
|
+
): Promise<HetznerSSHKey | null> {
|
|
152
|
+
const existingKeys = await hetznerClient.ssh_keys.list();
|
|
153
|
+
return existingKeys.find((k) => k.fingerprint === fingerprint) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Upload new SSH key to Hetzner
|
|
158
|
+
*/
|
|
159
|
+
async function uploadKeyToHetzner(
|
|
160
|
+
hetznerClient: HetznerClient,
|
|
161
|
+
keyName: string,
|
|
162
|
+
publicKey: string
|
|
163
|
+
): Promise<HetznerSSHKey> {
|
|
164
|
+
console.log(`[SSH] Uploading SSH key to Hetzner: ${keyName}`);
|
|
165
|
+
return await hetznerClient.ssh_keys.create({
|
|
166
|
+
name: keyName,
|
|
167
|
+
public_key: publicKey.trim(),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* SSH Key Manager - Main class for managing SSH keys
|
|
173
|
+
*/
|
|
174
|
+
export class SSHKeyManager {
|
|
175
|
+
private keyName: string;
|
|
176
|
+
private keysDir: string;
|
|
177
|
+
|
|
178
|
+
constructor(config: SSHKeyManagerConfig = {}) {
|
|
179
|
+
this.keyName = config.keyName || DEFAULT_KEY_NAME;
|
|
180
|
+
// Use provided keysDir or fall back to OS-specific default
|
|
181
|
+
this.keysDir = config.keysDir || getDefaultKeysDir();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the absolute path to the SSH keys directory
|
|
186
|
+
*/
|
|
187
|
+
private getKeysDirPath(): string {
|
|
188
|
+
return this.keysDir;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the full path to the SSH key files (absolute path)
|
|
193
|
+
*/
|
|
194
|
+
private getKeyPath(): string {
|
|
195
|
+
return join(this.keysDir, this.keyName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Ensure SSH key exists locally and on Hetzner
|
|
200
|
+
* This is the main entry point - replaces duplicate ensureSSHKey() functions
|
|
201
|
+
*
|
|
202
|
+
* @param hetznerClient - Hetzner API client
|
|
203
|
+
* @returns SSH key information for server creation
|
|
204
|
+
*/
|
|
205
|
+
async ensureSSHKey(hetznerClient: HetznerClient): Promise<SSHKeyInfo> {
|
|
206
|
+
// Ensure .ssh-keys directory exists (use absolute path)
|
|
207
|
+
const keysDirPath = this.getKeysDirPath();
|
|
208
|
+
mkdirSync(keysDirPath, { recursive: true });
|
|
209
|
+
|
|
210
|
+
const keyPath = this.getKeyPath();
|
|
211
|
+
const publicKeyPath = `${keyPath}.pub`;
|
|
212
|
+
|
|
213
|
+
// Check if key file exists locally
|
|
214
|
+
let publicKey: string;
|
|
215
|
+
let fingerprint: string;
|
|
216
|
+
|
|
217
|
+
if (existsSync(publicKeyPath)) {
|
|
218
|
+
// Read existing public key
|
|
219
|
+
publicKey = await Bun.file(publicKeyPath).text();
|
|
220
|
+
// Get fingerprint in Hetzner format (MD5 with colons)
|
|
221
|
+
fingerprint = await getHetznerFingerprint(publicKeyPath);
|
|
222
|
+
} else {
|
|
223
|
+
// Generate new key pair
|
|
224
|
+
await createKeyPair(keyPath, this.keyName);
|
|
225
|
+
publicKey = await Bun.file(publicKeyPath).text();
|
|
226
|
+
fingerprint = await getHetznerFingerprint(publicKeyPath);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if key exists on Hetzner by fingerprint
|
|
230
|
+
const existingKey = await findKeyByFingerprint(hetznerClient, fingerprint);
|
|
231
|
+
|
|
232
|
+
if (existingKey) {
|
|
233
|
+
console.log(
|
|
234
|
+
`[SSH] Using existing Hetzner SSH key: ${existingKey.name} (ID: ${existingKey.id})`,
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
keyId: existingKey.id,
|
|
238
|
+
keyPath,
|
|
239
|
+
fingerprint,
|
|
240
|
+
name: existingKey.name,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Upload new key to Hetzner
|
|
245
|
+
try {
|
|
246
|
+
const newKey = await uploadKeyToHetzner(hetznerClient, this.keyName, publicKey);
|
|
247
|
+
console.log(`[SSH] SSH key uploaded: ${newKey.name} (ID: ${newKey.id})`);
|
|
248
|
+
return {
|
|
249
|
+
keyId: newKey.id,
|
|
250
|
+
keyPath,
|
|
251
|
+
fingerprint,
|
|
252
|
+
name: newKey.name,
|
|
253
|
+
};
|
|
254
|
+
} catch (error: any) {
|
|
255
|
+
// Handle "already exists" error - try to find by name
|
|
256
|
+
if (
|
|
257
|
+
error?.message?.includes("not unique") ||
|
|
258
|
+
error?.message?.includes("already exists")
|
|
259
|
+
) {
|
|
260
|
+
console.log(
|
|
261
|
+
`[SSH] Key already exists in Hetzner, finding by name...`,
|
|
262
|
+
);
|
|
263
|
+
const keyByName = await hetznerClient.ssh_keys.findByName(this.keyName);
|
|
264
|
+
|
|
265
|
+
if (keyByName) {
|
|
266
|
+
console.log(
|
|
267
|
+
`[SSH] Found existing key by name: ${keyByName.name} (ID: ${keyByName.id})`,
|
|
268
|
+
);
|
|
269
|
+
return {
|
|
270
|
+
keyId: keyByName.id,
|
|
271
|
+
keyPath,
|
|
272
|
+
fingerprint: keyByName.fingerprint,
|
|
273
|
+
name: keyByName.name,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get local key fingerprint in SHA256 format
|
|
283
|
+
*/
|
|
284
|
+
async getLocalFingerprint(): Promise<string | null> {
|
|
285
|
+
const keyPath = this.getKeyPath();
|
|
286
|
+
return getLocalKeyFingerprint(keyPath);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get local key fingerprint in MD5 format (for Hetzner comparison)
|
|
291
|
+
*/
|
|
292
|
+
async getHetznerFingerprint(): Promise<string> {
|
|
293
|
+
const publicKeyPath = `${this.getKeyPath()}.pub`;
|
|
294
|
+
return getHetznerFingerprint(publicKeyPath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get the key path
|
|
299
|
+
*/
|
|
300
|
+
getKeyPathValue(): string {
|
|
301
|
+
return this.getKeyPath();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Convenience function - creates default manager and ensures SSH key
|
|
307
|
+
* This is a drop-in replacement for the old ensureSSHKey() functions
|
|
308
|
+
*
|
|
309
|
+
* @param hetznerClient - Hetzner API client
|
|
310
|
+
* @param config - Optional configuration
|
|
311
|
+
* @returns SSH key information
|
|
312
|
+
*/
|
|
313
|
+
export async function ensureSSHKey(
|
|
314
|
+
hetznerClient: HetznerClient,
|
|
315
|
+
config?: SSHKeyManagerConfig
|
|
316
|
+
): Promise<SSHKeyInfo> {
|
|
317
|
+
const manager = new SSHKeyManager(config);
|
|
318
|
+
return manager.ensureSSHKey(hetznerClient);
|
|
319
|
+
}
|