sshya 0.2.0 → 0.2.3
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/AGENTS.md +35 -0
- package/README.md +19 -39
- package/index.ts +7 -2
- package/package.json +2 -1
- package/src/connection/connect.ts +48 -0
- package/src/connection/index.ts +1 -0
- package/src/connection/list.ts +116 -26
- package/src/database.ts +35 -4
- package/src/helpers/connection.ts +6 -2
- package/src/helpers/fzf.ts +7 -37
- package/src/ssh.ts +0 -23
package/AGENTS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SSHya CLI - SSH Connection Manager
|
|
2
|
+
|
|
3
|
+
**Purpose**: CLI tool for managing SSH connections with aliases, interactive prompts, and fzf integration.
|
|
4
|
+
|
|
5
|
+
## Core Features
|
|
6
|
+
- **Connection Management**: Add, remove, update, list SSH connections with custom aliases
|
|
7
|
+
- **Import/Export**: JSON-based connection backup and restore
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
- **Runtime**: Bun (JavaScript runtime)
|
|
11
|
+
- **Language**: TypeScript
|
|
12
|
+
|
|
13
|
+
## Key Components
|
|
14
|
+
|
|
15
|
+
### Database (`src/database.ts`)
|
|
16
|
+
- `Connection` interface with id, alias, user, host, key_path, port, remote_path
|
|
17
|
+
|
|
18
|
+
### Commands (`index.ts`)
|
|
19
|
+
- `add`/`remove`/`update`/`list`: Manage connections
|
|
20
|
+
- `print [alias]`: Output SSH command for scripting/fzf
|
|
21
|
+
- `import`/`export`: JSON file operations
|
|
22
|
+
- `fzf`/`instructions`: Shell integration setup
|
|
23
|
+
|
|
24
|
+
## Usage Patterns
|
|
25
|
+
1. **Direct**: `sshya add` → Interactive prompt flow
|
|
26
|
+
2. **fzf Integration**: Shell function with `sshya print` for fast selection
|
|
27
|
+
3. **Scripting**: `sshya print my-server` → Outputs `ssh -p 2222 -t -i ~/.ssh/key user@host`
|
|
28
|
+
|
|
29
|
+
## Notable Implementation Details
|
|
30
|
+
- Shell-specific argument parsing (zsh vs bash) for fzf integration
|
|
31
|
+
- Usage tracking via `lastUsed` timestamps
|
|
32
|
+
- Always includes `-t` flag for proper pseudo-terminal allocation
|
|
33
|
+
- **High-performance optimizations**: ~0.2ms processing time for 8 connections with caching and pre-computation
|
|
34
|
+
|
|
35
|
+
This tool prioritizes developer experience with fast, keyboard-driven workflows while maintaining robust error handling and cross-platform compatibility.
|
package/README.md
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
This project is a command-line interface (CLI) tool named `sshya` for managing SSH connections.
|
|
4
4
|
|
|
5
5
|
## Core Functionality
|
|
6
|
-
- Add, remove, update, and list SSH connection configurations (alias, user, host, port, key path).
|
|
7
|
-
- Connect to saved
|
|
6
|
+
- Add, remove, update, and list SSH connection configurations (alias, user, host, port, key path, remote path).
|
|
7
|
+
- Connect to saved connections with `sshya connect` (aliases: `ssh`, `go`).
|
|
8
|
+
- Generate SSH command strings for scripting with `sshya print`.
|
|
8
9
|
- Import and export connections from/to a JSON file.
|
|
9
|
-
-
|
|
10
|
+
- fzf-based launcher for fast, keyboard-driven connection selection.
|
|
10
11
|
|
|
11
12
|
## Technology Stack
|
|
12
13
|
- **Runtime:** Bun
|
|
@@ -19,16 +20,7 @@ This project is a command-line interface (CLI) tool named `sshya` for managing S
|
|
|
19
20
|
- `zod`: for validating user input and imported data.
|
|
20
21
|
|
|
21
22
|
## Implementation Details
|
|
22
|
-
The main application logic is in `index.ts`.
|
|
23
|
-
- Laggy typing and missing keystrokes.
|
|
24
|
-
- Incorrect terminal behavior on window resizing.
|
|
25
|
-
|
|
26
|
-
The final solution involves:
|
|
27
|
-
1. Spawning the `ssh` process with `stdio: 'pipe'`.
|
|
28
|
-
2. Setting the local terminal to `rawMode` (`process.stdin.setRawMode(true)`).
|
|
29
|
-
3. Piping `stdin`, `stdout`, and `stderr` between the main process and the child `ssh` process.
|
|
30
|
-
4. Manually listening for `resize` events on `process.stdout` and forwarding them to the `ssh` child process by sending a `SIGWINCH` signal.
|
|
31
|
-
5. Using specific `ssh` command-line options (`-tt`, `ServerAliveInterval`, etc.) to ensure a stable and responsive interactive session.
|
|
23
|
+
The main application logic is in `index.ts`. SSH arguments are built in-process and passed directly to your system `ssh` binary, avoiding fragile shell string parsing.
|
|
32
24
|
|
|
33
25
|
## fzf-based launcher
|
|
34
26
|
|
|
@@ -50,40 +42,28 @@ You can enable a fast, keyboard-driven launcher for your saved connections using
|
|
|
50
42
|
|
|
51
43
|
### How it works
|
|
52
44
|
|
|
53
|
-
- The function displays your connections via `sshya list --oneline --names`, showing
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
- Argument splitting is handled per-shell to avoid issues with flags and quoting:
|
|
57
|
-
- zsh: `ssh ${(z)command}`
|
|
58
|
-
- bash: `read -r -a __args <<< "$command"; ssh "${__args[@]}"`
|
|
59
|
-
|
|
60
|
-
This ensures flags like `-p 2222` and identities like `-i '/path/with spaces/key'` are passed correctly to `ssh`.
|
|
61
|
-
|
|
62
|
-
### Why system ssh?
|
|
63
|
-
|
|
64
|
-
When launching from `fzf`, we intentionally use your host `ssh` binary instead of the built-in connect helper to avoid TTY and ZLE interaction problems.
|
|
45
|
+
- The function displays your connections via `sshya list --oneline --names`, showing alias and user@host.
|
|
46
|
+
- After you pick an entry with `fzf`, it runs `sshya connect <alias>`.
|
|
47
|
+
- `sshya connect` builds SSH arguments in-process and spawns your system `ssh` with inherited stdio, so TTY behavior matches typing the command yourself.
|
|
65
48
|
|
|
66
|
-
###
|
|
49
|
+
### Usage
|
|
67
50
|
|
|
68
|
-
|
|
51
|
+
After adding the snippet to your shell rc and reloading, simply run:
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
It also temporarily disables XON/XOFF flow control during selection (enables Ctrl-S) and restores your previous `stty` settings afterward.
|
|
53
|
+
```bash
|
|
54
|
+
s
|
|
55
|
+
```
|
|
74
56
|
|
|
75
|
-
|
|
57
|
+
This will launch the fzf interface for selecting and connecting to your saved SSH connections.
|
|
76
58
|
|
|
77
|
-
|
|
59
|
+
You can also connect directly without fzf:
|
|
78
60
|
|
|
79
61
|
```bash
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
sshya connect my-server
|
|
63
|
+
sshya connect # interactive picker
|
|
82
64
|
```
|
|
83
65
|
|
|
84
|
-
For bash, see the printed notes (example: `bind -x` with Ctrl-S), and ensure flow control is disabled (`stty -ixon`).
|
|
85
|
-
|
|
86
66
|
### Troubleshooting
|
|
87
67
|
|
|
88
|
-
- If
|
|
89
|
-
- If the port or key path is ignored,
|
|
68
|
+
- If fzf filtering looks wrong, reload your shell after updating the snippet. The launcher should call `sshya connect`, not parse `sshya print` output.
|
|
69
|
+
- If the port or key path is ignored, verify the connection with `sshya test <alias>`.
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import pkg from './package.json' assert { type: "json" };
|
|
6
|
-
import { addConnectionPrompt, exportConnectionsPrompt, importConnectionsPrompt, listConnectionsPrompt, removeConnectionPrompt, testConnectionPrompt, updateConnectionPrompt } from './src/connection';
|
|
6
|
+
import { addConnectionPrompt, connectConnectionPrompt, exportConnectionsPrompt, importConnectionsPrompt, listConnectionsPrompt, removeConnectionPrompt, testConnectionPrompt, updateConnectionPrompt } from './src/connection';
|
|
7
7
|
import { initDB } from './src/database';
|
|
8
8
|
import { printConnectionsPrompt } from './src/helpers/connection';
|
|
9
9
|
import { enableEscapeExit } from './src/helpers/escExit';
|
|
@@ -24,7 +24,12 @@ program
|
|
|
24
24
|
.version(version)
|
|
25
25
|
.description('A simple CLI to manage your SSH connections');
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
program
|
|
28
|
+
.command('connect [alias]')
|
|
29
|
+
.aliases(['ssh', 'go'])
|
|
30
|
+
.description('Connect to an SSH connection by alias')
|
|
31
|
+
.action(connectConnectionPrompt);
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
program
|
|
30
35
|
.command('print [alias]')
|
package/package.json
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"name": "sshya",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.3",
|
|
6
|
+
"description": "CLI for managing SSH connections with aliases, interactive prompts, and fzf integration",
|
|
6
7
|
"devDependencies": {
|
|
7
8
|
"@types/bun": "latest",
|
|
8
9
|
"@types/chalk": "^2.2.4"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { expandHomePath, getConnectionByAlias, recordConnectionUsage } from '../database';
|
|
4
|
+
import { buildRemoteCommand } from '../helpers/shell';
|
|
5
|
+
import { selectAlias } from '../helpers/selectAlias';
|
|
6
|
+
|
|
7
|
+
export async function connectConnectionPrompt(alias?: string): Promise<void> {
|
|
8
|
+
if (!alias) {
|
|
9
|
+
alias = await selectAlias('Select a connection to connect');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const connection = getConnectionByAlias(alias);
|
|
13
|
+
if (!connection) {
|
|
14
|
+
console.error(chalk.red('Alias not found'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const args: string[] = [];
|
|
19
|
+
if (connection.key_path) {
|
|
20
|
+
args.push('-i', expandHomePath(connection.key_path) ?? connection.key_path);
|
|
21
|
+
}
|
|
22
|
+
if (connection.port) {
|
|
23
|
+
args.push('-p', String(connection.port));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
args.push('-t', `${connection.user}@${connection.host}`);
|
|
27
|
+
|
|
28
|
+
if (connection.remote_path) {
|
|
29
|
+
const remoteCommand = buildRemoteCommand({
|
|
30
|
+
remotePath: connection.remote_path,
|
|
31
|
+
postCommand: 'exec "$SHELL" -l',
|
|
32
|
+
});
|
|
33
|
+
if (remoteCommand) {
|
|
34
|
+
args.push(remoteCommand);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
recordConnectionUsage(alias);
|
|
39
|
+
|
|
40
|
+
const child = spawn('ssh', args, { stdio: 'inherit' });
|
|
41
|
+
child.on('error', (error) => {
|
|
42
|
+
console.error(chalk.red(`Failed to start ssh: ${error.message}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
child.on('close', (code) => {
|
|
46
|
+
process.exit(code ?? 1);
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/connection/index.ts
CHANGED
package/src/connection/list.ts
CHANGED
|
@@ -1,40 +1,130 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import os from "os";
|
|
2
3
|
import { expandHomePath, getConnections } from "../database";
|
|
3
4
|
import { buildRemoteCommand } from "../helpers/shell";
|
|
4
5
|
|
|
5
6
|
export async function listConnectionsPrompt(options?: { oneline?: boolean; names?: boolean }) {
|
|
6
|
-
|
|
7
|
+
let connections = getConnections();
|
|
7
8
|
if (options?.oneline) {
|
|
8
|
-
//
|
|
9
|
+
// Sort by lastUsed (most recent first) for fzf - this helps with selection
|
|
10
|
+
if (options?.names) {
|
|
11
|
+
connections = connections.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Pre-compute constants outside the loop
|
|
15
|
+
const TAB = '\t';
|
|
16
|
+
const SPACE = ' ';
|
|
17
|
+
const DASH_I = '-i';
|
|
18
|
+
const DASH_P = '-p';
|
|
19
|
+
const DASH_T = '-t';
|
|
20
|
+
const SINGLE_QUOTE = "'";
|
|
21
|
+
const ESCAPED_QUOTE = "'\\''";
|
|
22
|
+
|
|
23
|
+
// Cache home directory for path expansion
|
|
24
|
+
const homeDir = os.homedir();
|
|
25
|
+
|
|
26
|
+
// Pre-compute user@host combinations to avoid string concatenation in loop
|
|
27
|
+
const userHostCache = new Map<string, string>();
|
|
9
28
|
for (const c of connections) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (c.port) {
|
|
18
|
-
parts.push('-p', String(c.port));
|
|
19
|
-
}
|
|
20
|
-
parts.push(`${c.user}@${c.host}`);
|
|
21
|
-
if (c.remote_path) {
|
|
22
|
-
const remoteCommand = buildRemoteCommand({
|
|
23
|
-
remotePath: String(c.remote_path),
|
|
24
|
-
postCommand: 'exec "$SHELL" -l',
|
|
25
|
-
});
|
|
26
|
-
if (remoteCommand) {
|
|
27
|
-
parts.push('-t', remoteCommand);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const args = parts.join(' ');
|
|
29
|
+
userHostCache.set(c.alias, `${c.user}@${c.host}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const c of connections) {
|
|
33
|
+
// Use cached user@host
|
|
34
|
+
const userHost = userHostCache.get(c.alias)!;
|
|
35
|
+
|
|
31
36
|
if (options?.names) {
|
|
32
|
-
//
|
|
33
|
-
|
|
37
|
+
// Optimized tab-separated format for fzf
|
|
38
|
+
let output = c.alias + TAB + userHost + TAB;
|
|
39
|
+
|
|
40
|
+
// Build SSH args array more efficiently
|
|
41
|
+
const args: string[] = [];
|
|
42
|
+
|
|
43
|
+
if (c.key_path) {
|
|
44
|
+
// Fast path expansion - only expand if it starts with ~
|
|
45
|
+
let expandedKeyPath = c.key_path;
|
|
46
|
+
if (expandedKeyPath.startsWith('~')) {
|
|
47
|
+
if (expandedKeyPath === '~') {
|
|
48
|
+
expandedKeyPath = homeDir;
|
|
49
|
+
} else if (expandedKeyPath.startsWith('~/')) {
|
|
50
|
+
expandedKeyPath = homeDir + expandedKeyPath.slice(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fast quote escaping - only escape single quotes
|
|
55
|
+
const needsEscaping = expandedKeyPath.includes(SINGLE_QUOTE);
|
|
56
|
+
const quotedKey = needsEscaping
|
|
57
|
+
? SINGLE_QUOTE + expandedKeyPath.replace(/'/g, ESCAPED_QUOTE) + SINGLE_QUOTE
|
|
58
|
+
: SINGLE_QUOTE + expandedKeyPath + SINGLE_QUOTE;
|
|
59
|
+
|
|
60
|
+
args.push(DASH_I, quotedKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (c.port) {
|
|
64
|
+
args.push(DASH_P, String(c.port));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Always add -t for interactive terminal allocation
|
|
68
|
+
args.push(DASH_T);
|
|
69
|
+
args.push(userHost);
|
|
70
|
+
|
|
71
|
+
if (c.remote_path) {
|
|
72
|
+
// Only build remote command if needed
|
|
73
|
+
const remoteCommand = buildRemoteCommand({
|
|
74
|
+
remotePath: String(c.remote_path),
|
|
75
|
+
postCommand: 'exec "$SHELL" -l',
|
|
76
|
+
});
|
|
77
|
+
if (remoteCommand) {
|
|
78
|
+
args.push(remoteCommand);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Join args with spaces (faster than array join for small arrays)
|
|
83
|
+
output += args.join(SPACE);
|
|
84
|
+
console.log(output);
|
|
34
85
|
} else {
|
|
35
|
-
|
|
86
|
+
// Simple args-only format (even faster)
|
|
87
|
+
const args: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (c.key_path) {
|
|
90
|
+
let expandedKeyPath = c.key_path;
|
|
91
|
+
if (expandedKeyPath.startsWith('~')) {
|
|
92
|
+
if (expandedKeyPath === '~') {
|
|
93
|
+
expandedKeyPath = homeDir;
|
|
94
|
+
} else if (expandedKeyPath.startsWith('~/')) {
|
|
95
|
+
expandedKeyPath = homeDir + expandedKeyPath.slice(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const needsEscaping = expandedKeyPath.includes(SINGLE_QUOTE);
|
|
100
|
+
const quotedKey = needsEscaping
|
|
101
|
+
? SINGLE_QUOTE + expandedKeyPath.replace(/'/g, ESCAPED_QUOTE) + SINGLE_QUOTE
|
|
102
|
+
: SINGLE_QUOTE + expandedKeyPath + SINGLE_QUOTE;
|
|
103
|
+
|
|
104
|
+
args.push(DASH_I, quotedKey);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (c.port) {
|
|
108
|
+
args.push(DASH_P, String(c.port));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
args.push(DASH_T);
|
|
112
|
+
args.push(userHost);
|
|
113
|
+
|
|
114
|
+
if (c.remote_path) {
|
|
115
|
+
const remoteCommand = buildRemoteCommand({
|
|
116
|
+
remotePath: String(c.remote_path),
|
|
117
|
+
postCommand: 'exec "$SHELL" -l',
|
|
118
|
+
});
|
|
119
|
+
if (remoteCommand) {
|
|
120
|
+
args.push(remoteCommand);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(args.join(SPACE));
|
|
36
125
|
}
|
|
37
126
|
}
|
|
127
|
+
|
|
38
128
|
process.exit(0);
|
|
39
129
|
}
|
|
40
130
|
|
package/src/database.ts
CHANGED
|
@@ -34,15 +34,24 @@ const normalizeOptionalString = (value?: string | number | null): string | undef
|
|
|
34
34
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// Cache home directory for performance
|
|
38
|
+
let homeDirCache: string | null = null;
|
|
39
|
+
|
|
37
40
|
export const expandHomePath = (input?: string): string | undefined => {
|
|
38
41
|
if (!input) {
|
|
39
42
|
return undefined;
|
|
40
43
|
}
|
|
44
|
+
|
|
45
|
+
// Cache home directory
|
|
46
|
+
if (!homeDirCache) {
|
|
47
|
+
homeDirCache = os.homedir();
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
if (input === '~') {
|
|
42
|
-
return
|
|
51
|
+
return homeDirCache;
|
|
43
52
|
}
|
|
44
53
|
if (input.startsWith('~/')) {
|
|
45
|
-
return path.join(
|
|
54
|
+
return path.join(homeDirCache, input.slice(2));
|
|
46
55
|
}
|
|
47
56
|
return input;
|
|
48
57
|
};
|
|
@@ -63,13 +72,31 @@ const normalizeConnection = (connection: Connection): Connection => {
|
|
|
63
72
|
};
|
|
64
73
|
};
|
|
65
74
|
|
|
75
|
+
// Cache for parsed connections to avoid re-reading/parsing
|
|
76
|
+
let connectionCache: Connection[] | null = null;
|
|
77
|
+
let cacheTimestamp: number = 0;
|
|
78
|
+
const CACHE_DURATION = 1000; // 1 second cache
|
|
79
|
+
|
|
66
80
|
const readDB = (): Connection[] => {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
|
|
83
|
+
// Return cached data if it's still fresh
|
|
84
|
+
if (connectionCache && (now - cacheTimestamp) < CACHE_DURATION) {
|
|
85
|
+
return connectionCache;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
if (!fs.existsSync(dbFilePath)) {
|
|
68
|
-
|
|
89
|
+
connectionCache = [];
|
|
90
|
+
cacheTimestamp = now;
|
|
91
|
+
return connectionCache;
|
|
69
92
|
}
|
|
93
|
+
|
|
70
94
|
const data = fs.readFileSync(dbFilePath, 'utf-8');
|
|
71
95
|
const parsed: Connection[] = JSON.parse(data);
|
|
72
|
-
|
|
96
|
+
connectionCache = parsed.map(normalizeConnection);
|
|
97
|
+
cacheTimestamp = now;
|
|
98
|
+
|
|
99
|
+
return connectionCache;
|
|
73
100
|
};
|
|
74
101
|
|
|
75
102
|
const writeDB = (data: Connection[]) => {
|
|
@@ -78,6 +105,10 @@ const writeDB = (data: Connection[]) => {
|
|
|
78
105
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
79
106
|
}
|
|
80
107
|
fs.writeFileSync(dbFilePath, JSON.stringify(data, null, 2));
|
|
108
|
+
|
|
109
|
+
// Invalidate cache when data is written
|
|
110
|
+
connectionCache = null;
|
|
111
|
+
cacheTimestamp = 0;
|
|
81
112
|
};
|
|
82
113
|
|
|
83
114
|
export const initDB = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { expandHomePath, getConnectionByAlias } from "../database";
|
|
2
|
+
import { expandHomePath, getConnectionByAlias, recordConnectionUsage } from "../database";
|
|
3
3
|
import { buildRemoteCommand } from "./shell";
|
|
4
4
|
|
|
5
5
|
export async function printConnectionsPrompt(alias?: string) {
|
|
@@ -12,6 +12,8 @@ export async function printConnectionsPrompt(alias?: string) {
|
|
|
12
12
|
console.error(chalk.red('Alias not found'));
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
+
// Record usage when connection is accessed via print (used by fzf)
|
|
16
|
+
recordConnectionUsage(alias);
|
|
15
17
|
const parts: string[] = [];
|
|
16
18
|
if (connection.key_path) {
|
|
17
19
|
const expandedKeyPath = expandHomePath(String(connection.key_path));
|
|
@@ -21,6 +23,8 @@ export async function printConnectionsPrompt(alias?: string) {
|
|
|
21
23
|
if (connection.port) {
|
|
22
24
|
parts.push('-p', String(connection.port));
|
|
23
25
|
}
|
|
26
|
+
// Always add -t for interactive terminal allocation (required for fzf integration)
|
|
27
|
+
parts.push('-t');
|
|
24
28
|
parts.push(`${connection.user}@${connection.host}`);
|
|
25
29
|
if (connection.remote_path) {
|
|
26
30
|
const remoteCommand = buildRemoteCommand({
|
|
@@ -28,7 +32,7 @@ export async function printConnectionsPrompt(alias?: string) {
|
|
|
28
32
|
postCommand: 'exec "$SHELL" -l',
|
|
29
33
|
});
|
|
30
34
|
if (remoteCommand) {
|
|
31
|
-
parts.push(
|
|
35
|
+
parts.push(remoteCommand);
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
console.log(parts.join(' '));
|
package/src/helpers/fzf.ts
CHANGED
|
@@ -4,53 +4,23 @@ export function printFzfInstructions() {
|
|
|
4
4
|
console.log(chalk.yellow('To enable a quick fzf-powered SSH launcher, add this to your shell rc:'));
|
|
5
5
|
console.log('\n' + chalk.bold('~/.bashrc or ~/.zshrc') + '\n');
|
|
6
6
|
const snippet = [
|
|
7
|
-
'
|
|
8
|
-
' local line alias userhost
|
|
9
|
-
' # Temporarily disable XON/XOFF so Ctrl-S works, then restore',
|
|
10
|
-
' oldstty=$(stty -g 2>/dev/null)',
|
|
11
|
-
' stty -ixon 2>/dev/null || true',
|
|
7
|
+
's() {',
|
|
8
|
+
' local line alias userhost',
|
|
12
9
|
' run_sshya() {',
|
|
13
|
-
'
|
|
14
|
-
' stdbuf -oL sshya "$@"',
|
|
15
|
-
' else',
|
|
16
|
-
' stdbuf -oL bun --silent run start "$@"',
|
|
17
|
-
' fi',
|
|
10
|
+
' stdbuf -oL sshya "$@"',
|
|
18
11
|
' }',
|
|
19
|
-
" line=$(run_sshya list --oneline --names | fzf --
|
|
12
|
+
" line=$(run_sshya list --oneline --names | fzf --with-nth=1,2 --nth=1,2 --delimiter=$'\\t') || return",
|
|
20
13
|
" alias=${line%%$'\\t'*}",
|
|
21
14
|
" userhost=${line#*$'\\t'}; userhost=${userhost%%$'\\t'*}",
|
|
22
|
-
" command=$(run_sshya print \"$alias\")",
|
|
23
15
|
" echo \"Connecting: $alias ($userhost)\"",
|
|
24
|
-
"
|
|
25
|
-
" if [ -n \"$ZSH_VERSION\" ]; then",
|
|
26
|
-
" local -a _args",
|
|
27
|
-
" local remote_cmd=\"\"",
|
|
28
|
-
" # Check if command contains -t for remote command",
|
|
29
|
-
" if [[ \"$command\" == *' -t '* ]]; then",
|
|
30
|
-
" # Split at -t, parse only SSH args, keep remote command as-is",
|
|
31
|
-
" _args=(${(zQ)command%% -t *})",
|
|
32
|
-
" remote_cmd=\"${command#* -t }\"",
|
|
33
|
-
" ssh \"${_args[@]}\" -t \"$remote_cmd\"",
|
|
34
|
-
" else",
|
|
35
|
-
" _args=(${(zQ)command})",
|
|
36
|
-
" ssh \"${_args[@]}\"",
|
|
37
|
-
" fi",
|
|
38
|
-
" zle -R -c",
|
|
39
|
-
" else",
|
|
40
|
-
" read -r -a __args <<< \"$command\"",
|
|
41
|
-
" ssh \"${__args[@]}\"",
|
|
42
|
-
" fi",
|
|
43
|
-
" stty \"$oldstty\" 2>/dev/null || true",
|
|
16
|
+
" run_sshya connect \"$alias\"",
|
|
44
17
|
'}',
|
|
45
|
-
'# zsh widget binding (recommended):',
|
|
46
|
-
'zle -N _fzf_sshya',
|
|
47
|
-
"bindkey '^S' _fzf_sshya",
|
|
48
18
|
].join('\n');
|
|
49
19
|
console.log(chalk.cyan(snippet));
|
|
50
20
|
console.log('\n' + chalk.green('Notes:'));
|
|
51
21
|
console.log('- This expects ' + chalk.bold('fzf') + ' to be installed.');
|
|
52
|
-
console.log('-
|
|
53
|
-
console.log('-
|
|
22
|
+
console.log('- After adding this to your shell rc, reload with: ' + chalk.cyan('source ~/.zshrc') + ' or ' + chalk.cyan('source ~/.bashrc'));
|
|
23
|
+
console.log('- Then simply run ' + chalk.bold('s') + ' to launch the fzf interface');
|
|
54
24
|
process.exit(0);
|
|
55
25
|
}
|
|
56
26
|
|
package/src/ssh.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Connection } from './database';
|
|
2
|
-
import { expandHomePath } from './database';
|
|
3
|
-
import { buildRemoteCommand } from './helpers/shell';
|
|
4
|
-
|
|
5
|
-
export function buildSSHArgs(connection: Connection) {
|
|
6
|
-
const args: (string | number)[] = [];
|
|
7
|
-
if (connection.key_path) {
|
|
8
|
-
const expandedKeyPath = expandHomePath(connection.key_path);
|
|
9
|
-
args.push('-i', expandedKeyPath ?? connection.key_path);
|
|
10
|
-
}
|
|
11
|
-
if (connection.port) args.push('-p', connection.port);
|
|
12
|
-
args.push(`${connection.user}@${connection.host}`);
|
|
13
|
-
if (connection.remote_path) {
|
|
14
|
-
const remoteCommand = buildRemoteCommand({
|
|
15
|
-
remotePath: connection.remote_path,
|
|
16
|
-
postCommand: 'exec "$SHELL" -l',
|
|
17
|
-
});
|
|
18
|
-
if (remoteCommand) {
|
|
19
|
-
args.push('-t', remoteCommand);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return args.map(String);
|
|
23
|
-
}
|