@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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Core SSH client for executing commands on remote servers
3
+ * Uses persistent connection pool for efficient reuse
4
+ * Uses base64 encoding to avoid shell escaping issues
5
+ */
6
+ import type { SSHOptions } from "./types.js";
7
+ /**
8
+ * Execute a command on a remote server via SSH
9
+ * Uses persistent connection pool for better performance
10
+ * @param command - Shell command to execute
11
+ * @param options - SSH connection options
12
+ * @returns Command output as string
13
+ */
14
+ export declare function execSSH(command: string, options: SSHOptions): Promise<string>;
15
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Core SSH client for executing commands on remote servers
3
+ * Uses persistent connection pool for efficient reuse
4
+ * Uses base64 encoding to avoid shell escaping issues
5
+ */
6
+ import { SSHError } from "./error.js";
7
+ import { SSHOptionsSchema, SSHCommandSchema } from '@ebowwa/codespaces-types/runtime/ssh';
8
+ import { getSSHPool } from './pool.js';
9
+ /**
10
+ * Execute a command on a remote server via SSH
11
+ * Uses persistent connection pool for better performance
12
+ * @param command - Shell command to execute
13
+ * @param options - SSH connection options
14
+ * @returns Command output as string
15
+ */
16
+ export async function execSSH(command, options) {
17
+ // Validate inputs with Zod
18
+ const validatedCommand = SSHCommandSchema.safeParse(command);
19
+ if (!validatedCommand.success) {
20
+ throw new Error(`Invalid SSH command: ${validatedCommand.error.issues.map(i => i.message).join(', ')}`);
21
+ }
22
+ const validatedOptions = SSHOptionsSchema.safeParse(options);
23
+ if (!validatedOptions.success) {
24
+ throw new Error(`Invalid SSH options: ${validatedOptions.error.issues.map(i => i.message).join(', ')}`);
25
+ }
26
+ const { host, user = "root", timeout = 5, port = 22, keyPath, password } = validatedOptions.data;
27
+ try {
28
+ // Get connection pool
29
+ const pool = getSSHPool();
30
+ // Execute command directly via SSH - the pool handles proper escaping
31
+ const output = await pool.exec(validatedCommand.data, {
32
+ host,
33
+ user,
34
+ timeout,
35
+ port,
36
+ keyPath,
37
+ password,
38
+ });
39
+ return output || "0";
40
+ }
41
+ catch (error) {
42
+ throw new SSHError(`SSH command failed: ${validatedCommand.data}`, error);
43
+ }
44
+ }
45
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SSH error class
3
+ */
4
+ export declare class SSHError extends Error {
5
+ readonly cause?: unknown | undefined;
6
+ constructor(message: string, cause?: unknown | undefined);
7
+ }
8
+ //# sourceMappingURL=error.d.ts.map
package/dist/error.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SSH error class
3
+ */
4
+ export class SSHError extends Error {
5
+ cause;
6
+ constructor(message, cause) {
7
+ super(message);
8
+ this.cause = cause;
9
+ this.name = "SSHError";
10
+ }
11
+ }
12
+ //# sourceMappingURL=error.js.map
package/dist/exec.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SSH command execution functions
3
+ */
4
+ import type { SSHOptions } from "./types.js";
5
+ /**
6
+ * Execute multiple SSH commands in parallel using multiple connections
7
+ *
8
+ * DESIGN DECISION: Multiple Connections vs Single Connection
9
+ * ===========================================================
10
+ *
11
+ * We use MULTIPLE SSH connections to avoid channel saturation issues.
12
+ * SSH servers typically limit concurrent channels per connection (~10).
13
+ * When executing 9+ commands in parallel, we can exceed this limit.
14
+ *
15
+ * Solution: Distribute commands across multiple pooled connections.
16
+ * Each connection handles a subset of commands, staying within channel limits.
17
+ *
18
+ * DESIGN DECISION: Promise.allSettled() vs Promise.all()
19
+ * ======================================================
20
+ *
21
+ * We use Promise.allSettled() instead of Promise.all() for a critical reason:
22
+ * Resource monitoring should be RESILIENT. If one command fails (e.g., GPU
23
+ * query on a CPU-only server), we still want results from all other commands.
24
+ *
25
+ * Example scenario:
26
+ * - CPU, memory, disk commands: succeed
27
+ * - GPU command: fails (no NVIDIA GPU)
28
+ * - Network command: succeeds
29
+ *
30
+ * With Promise.all(): entire batch fails, no metrics collected
31
+ * With Promise.allSettled(): we get 6/7 metrics, GPU returns "0" fallback
32
+ *
33
+ * ERROR HANDLING:
34
+ * ==============
35
+ * 1. Individual command failures are logged to console
36
+ * 2. Failed commands return "0" as fallback (matches execSSH default)
37
+ * 3. The function always completes successfully (never throws)
38
+ * 4. Calling code can check for "0" values to detect failures
39
+ */
40
+ export declare function execSSHParallel(commands: Record<string, string>, options: SSHOptions): Promise<Record<string, string>>;
41
+ /**
42
+ * Test SSH connection to a remote server
43
+ * @param options - SSH connection options
44
+ * @returns True if connection successful
45
+ */
46
+ export declare function testSSHConnection(options: SSHOptions): Promise<boolean>;
47
+ //# sourceMappingURL=exec.d.ts.map
package/dist/exec.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * SSH command execution functions
3
+ */
4
+ import { execSSH } from "./client.js";
5
+ import { getSSHPool } from "./pool.js";
6
+ /**
7
+ * Execute multiple SSH commands in parallel using multiple connections
8
+ *
9
+ * DESIGN DECISION: Multiple Connections vs Single Connection
10
+ * ===========================================================
11
+ *
12
+ * We use MULTIPLE SSH connections to avoid channel saturation issues.
13
+ * SSH servers typically limit concurrent channels per connection (~10).
14
+ * When executing 9+ commands in parallel, we can exceed this limit.
15
+ *
16
+ * Solution: Distribute commands across multiple pooled connections.
17
+ * Each connection handles a subset of commands, staying within channel limits.
18
+ *
19
+ * DESIGN DECISION: Promise.allSettled() vs Promise.all()
20
+ * ======================================================
21
+ *
22
+ * We use Promise.allSettled() instead of Promise.all() for a critical reason:
23
+ * Resource monitoring should be RESILIENT. If one command fails (e.g., GPU
24
+ * query on a CPU-only server), we still want results from all other commands.
25
+ *
26
+ * Example scenario:
27
+ * - CPU, memory, disk commands: succeed
28
+ * - GPU command: fails (no NVIDIA GPU)
29
+ * - Network command: succeeds
30
+ *
31
+ * With Promise.all(): entire batch fails, no metrics collected
32
+ * With Promise.allSettled(): we get 6/7 metrics, GPU returns "0" fallback
33
+ *
34
+ * ERROR HANDLING:
35
+ * ==============
36
+ * 1. Individual command failures are logged to console
37
+ * 2. Failed commands return "0" as fallback (matches execSSH default)
38
+ * 3. The function always completes successfully (never throws)
39
+ * 4. Calling code can check for "0" values to detect failures
40
+ */
41
+ export async function execSSHParallel(commands, options) {
42
+ const entries = Object.entries(commands);
43
+ const pool = getSSHPool();
44
+ // Determine optimal number of connections (3-4 connections is a good balance)
45
+ // This avoids SSH channel limits while maintaining parallelism
46
+ const numCommands = entries.length;
47
+ const numConnections = Math.min(numCommands, 4); // Max 4 connections
48
+ // Get multiple connections from the pool
49
+ const connections = await pool.getConnections(options, numConnections);
50
+ // Distribute commands across connections (round-robin)
51
+ const connectionPromises = connections.map((ssh, connIndex) => {
52
+ // Assign commands to this connection
53
+ const assignedCommands = entries.filter((_, i) => i % numConnections === connIndex);
54
+ // Execute all assigned commands on this connection
55
+ return Promise.allSettled(assignedCommands.map(async ([key, cmd]) => {
56
+ try {
57
+ const result = await ssh.execCommand(cmd, {
58
+ execOptions: {
59
+ timeout: (options.timeout || 5) * 1000,
60
+ },
61
+ });
62
+ // If we have stderr but no stdout, the command failed
63
+ if (result.stderr && !result.stdout) {
64
+ throw new Error(result.stderr);
65
+ }
66
+ return [key, result.stdout.trim()];
67
+ }
68
+ catch (error) {
69
+ // Log the error with full details including cause
70
+ console.error(`[execSSHParallel] Command "${key}" failed:`, error instanceof Error ? error.message : error);
71
+ // Log the underlying cause if available
72
+ if (error instanceof Error && error.cause) {
73
+ console.error(`[execSSHParallel] Command "${key}" cause:`, error.cause);
74
+ }
75
+ return [key, "0"]; // Fallback value
76
+ }
77
+ }));
78
+ });
79
+ // Wait for all connections to complete their assigned commands
80
+ const allSettledResults = await Promise.all(connectionPromises);
81
+ // Flatten results from all connections
82
+ const results = [];
83
+ for (const connResults of allSettledResults) {
84
+ for (const result of connResults) {
85
+ if (result.status === "fulfilled") {
86
+ results.push([...result.value]);
87
+ }
88
+ // Rejected promises are already logged and return "0" above
89
+ }
90
+ }
91
+ return Object.fromEntries(results);
92
+ }
93
+ /**
94
+ * Test SSH connection to a remote server
95
+ * @param options - SSH connection options
96
+ * @returns True if connection successful
97
+ */
98
+ export async function testSSHConnection(options) {
99
+ try {
100
+ await execSSH('echo "connection_test"', options);
101
+ return true;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Remote file operations via SSH
3
+ */
4
+ import type { SSHOptions } from "./types.js";
5
+ import { SSHError } from "./error.js";
6
+ /**
7
+ * Path sanitization options
8
+ */
9
+ export interface SanitizePathOptions {
10
+ /**
11
+ * Allowed base directories (absolute paths)
12
+ * Default: ["/root"] for root user
13
+ */
14
+ allowedBaseDirs?: string[];
15
+ /**
16
+ * User context for determining default base directory
17
+ */
18
+ user?: string;
19
+ /**
20
+ * Whether to allow absolute paths
21
+ * Default: false (security best practice)
22
+ */
23
+ allowAbsolutePaths?: boolean;
24
+ /**
25
+ * Maximum path depth to prevent deep traversal attempts
26
+ * Default: 20
27
+ */
28
+ maxDepth?: number;
29
+ /**
30
+ * Log suspicious path attempts
31
+ * Default: true
32
+ */
33
+ logSuspicious?: boolean;
34
+ }
35
+ /**
36
+ * Path traversal security error
37
+ */
38
+ export declare class PathTraversalError extends SSHError {
39
+ readonly attemptedPath: string;
40
+ readonly reason: string;
41
+ constructor(message: string, attemptedPath: string, reason: string);
42
+ }
43
+ /**
44
+ * Security event log for path traversal attempts
45
+ */
46
+ interface SecurityEvent {
47
+ timestamp: string;
48
+ attemptedPath: string;
49
+ reason: string;
50
+ severity: "blocked" | "suspicious" | "warning";
51
+ }
52
+ /**
53
+ * Get recent security events for monitoring
54
+ */
55
+ export declare function getSecurityEvents(limit?: number): SecurityEvent[];
56
+ /**
57
+ * Clear old security events (for maintenance)
58
+ */
59
+ export declare function clearSecurityEvents(olderThanMs?: number): number;
60
+ /**
61
+ * Sanitize and validate a file path for security
62
+ *
63
+ * This function prevents path traversal attacks by:
64
+ * 1. Rejecting paths with .. components
65
+ * 2. Validating against allowed base directories
66
+ * 3. Normalizing paths to remove . and redundant /
67
+ * 4. Checking for null bytes and other escape sequences
68
+ * 5. Limiting path depth to prevent deep traversal
69
+ *
70
+ * @param inputPath - The user-provided path to sanitize
71
+ * @param options - Sanitization options
72
+ * @returns Sanitized absolute path
73
+ * @throws PathTraversalError if path is suspicious or invalid
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Safe: within /root
78
+ * sanitizePath("project/file.txt", { user: "root" })
79
+ * // Returns: "/root/project/file.txt"
80
+ *
81
+ * // BLOCKED: attempts to escape
82
+ * sanitizePath("../../../etc/passwd", { user: "root" })
83
+ * // Throws: PathTraversalError
84
+ *
85
+ * // BLOCKED: null byte injection
86
+ * sanitizePath("file.txt\0../../../etc/passwd", { user: "root" })
87
+ * // Throws: PathTraversalError
88
+ * ```
89
+ */
90
+ export declare function sanitizePath(inputPath: string, options?: SanitizePathOptions): string;
91
+ export type FileType = "file" | "directory";
92
+ export interface RemoteFile {
93
+ name: string;
94
+ path: string;
95
+ size: string;
96
+ modified: string;
97
+ type: FileType;
98
+ }
99
+ export type PreviewType = "text" | "image" | "binary" | "error";
100
+ export interface FilePreview {
101
+ type: PreviewType;
102
+ content?: string;
103
+ error?: string;
104
+ }
105
+ /**
106
+ * List files in a directory on remote server
107
+ * @param path - Directory path to list (default: .)
108
+ * @param options - SSH connection options
109
+ * @returns List of files with metadata
110
+ * @throws PathTraversalError if path attempts to escape allowed directories
111
+ * @throws SSHError if SSH command fails
112
+ */
113
+ export declare function listFiles(path: string | undefined, options: SSHOptions): Promise<RemoteFile[]>;
114
+ /**
115
+ * Preview a file's content from remote server
116
+ * @param filePath - Path to the file to preview
117
+ * @param options - SSH connection options
118
+ * @returns File content for preview
119
+ * @throws PathTraversalError if path attempts to escape allowed directories
120
+ * @throws SSHError if SSH command fails
121
+ */
122
+ export declare function previewFile(filePath: string, options: SSHOptions): Promise<FilePreview>;
123
+ export {};
124
+ //# sourceMappingURL=files.d.ts.map