sshya 0.1.1 → 0.1.2
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/README.md +58 -0
- package/index.ts +53 -15
- package/package.json +2 -2
- package/src/connection/list.ts +25 -1
- package/src/database.ts +1 -1
- package/src/helpers/{connectInteractive.ts → connection.ts} +15 -3
- package/src/helpers/fzf.ts +47 -0
- package/src/ssh.ts +8 -13
package/README.md
CHANGED
|
@@ -29,3 +29,61 @@ The final solution involves:
|
|
|
29
29
|
3. Piping `stdin`, `stdout`, and `stderr` between the main process and the child `ssh` process.
|
|
30
30
|
4. Manually listening for `resize` events on `process.stdout` and forwarding them to the `ssh` child process by sending a `SIGWINCH` signal.
|
|
31
31
|
5. Using specific `ssh` command-line options (`-tt`, `ServerAliveInterval`, etc.) to ensure a stable and responsive interactive session.
|
|
32
|
+
|
|
33
|
+
## fzf-based launcher
|
|
34
|
+
|
|
35
|
+
You can enable a fast, keyboard-driven launcher for your saved connections using `fzf`.
|
|
36
|
+
|
|
37
|
+
### Quick setup
|
|
38
|
+
|
|
39
|
+
1. Ensure `fzf` is installed.
|
|
40
|
+
2. Print the shell snippet:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sshya fzf
|
|
44
|
+
# or the aliases
|
|
45
|
+
sshya instructions
|
|
46
|
+
sshya install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. Copy the printed snippet into your `~/.zshrc` or `~/.bashrc` and reload your shell.
|
|
50
|
+
|
|
51
|
+
### How it works
|
|
52
|
+
|
|
53
|
+
- The function displays your connections via `sshya list --oneline --names`, showing:
|
|
54
|
+
- alias, user@host (for display), and a third tab-separated column containing ssh-ready arguments.
|
|
55
|
+
- After you pick an entry with `fzf`, it runs `sshya print <alias>` to obtain a single-line command string and then invokes your system `ssh` client.
|
|
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.
|
|
65
|
+
|
|
66
|
+
### ZLE/TTY handling (zsh)
|
|
67
|
+
|
|
68
|
+
The snippet detaches the zsh line editor before starting `ssh` and restores it after:
|
|
69
|
+
|
|
70
|
+
- Before `ssh`: `zle -I`
|
|
71
|
+
- After `ssh`: `zle -R -c`
|
|
72
|
+
|
|
73
|
+
It also temporarily disables XON/XOFF flow control during selection (enables Ctrl-S) and restores your previous `stty` settings afterward.
|
|
74
|
+
|
|
75
|
+
### Keybinding
|
|
76
|
+
|
|
77
|
+
The snippet includes an example zsh binding:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
zle -N fzf_sshya
|
|
81
|
+
bindkey '^S' fzf_sshya
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For bash, see the printed notes (example: `bind -x` with Ctrl-S), and ensure flow control is disabled (`stty -ixon`).
|
|
85
|
+
|
|
86
|
+
### Troubleshooting
|
|
87
|
+
|
|
88
|
+
- If typing doesn't work in the remote session, make sure you've reloaded your shell after updating the snippet (it uses `zle -I`, `zle -R -c`, and restores `stty`).
|
|
89
|
+
- If the port or key path is ignored, ensure you're on the updated snippet which splits args correctly for zsh/bash and make sure `sshya print <alias>` outputs the expected `-p`/`-i` flags.
|
package/index.ts
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import pkg from './package.json' assert { type: "json" };
|
|
5
6
|
import { addConnectionPrompt, exportConnectionsPrompt, importConnectionsPrompt, listConnectionsPrompt, removeConnectionPrompt, testConnectionPrompt, updateConnectionPrompt } from './src/connection';
|
|
6
7
|
import { initDB } from './src/database';
|
|
7
|
-
import { connectInteractive } from './src/helpers/
|
|
8
|
+
import { connectInteractive, printConnectionsPrompt } from './src/helpers/connection';
|
|
8
9
|
import { enableEscapeExit } from './src/helpers/escExit';
|
|
10
|
+
import { printFzfInstructions } from './src/helpers/fzf';
|
|
9
11
|
import { handleSshCommand } from './src/sshCommandHandler';
|
|
10
12
|
|
|
11
|
-
import pkg from './package.json' assert { type: "json" };
|
|
12
13
|
const version = pkg?.version ?? '1.0.0';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
16
|
+
enableEscapeExit();
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
initDB();
|
|
16
20
|
|
|
17
21
|
const program = new Command();
|
|
@@ -25,6 +29,11 @@ program
|
|
|
25
29
|
.description('Connect to an SSH connection by alias')
|
|
26
30
|
.action(connectInteractive);
|
|
27
31
|
|
|
32
|
+
program
|
|
33
|
+
.command('print [alias]')
|
|
34
|
+
.description('Print an SSH connection by alias')
|
|
35
|
+
.action(printConnectionsPrompt);
|
|
36
|
+
|
|
28
37
|
program
|
|
29
38
|
.command('add')
|
|
30
39
|
.aliases(['create', 'new'])
|
|
@@ -35,7 +44,9 @@ program
|
|
|
35
44
|
.command('list')
|
|
36
45
|
.aliases(['ps', 'ls'])
|
|
37
46
|
.description('List all SSH connections')
|
|
38
|
-
.
|
|
47
|
+
.option('--oneline', 'Output each connection as ssh args on one line')
|
|
48
|
+
.option('--names', 'Include alias as first column (tab-separated) with --oneline')
|
|
49
|
+
.action((options: { oneline?: boolean; names?: boolean }) => listConnectionsPrompt({ oneline: !!options?.oneline, names: !!options?.names }));
|
|
39
50
|
|
|
40
51
|
program
|
|
41
52
|
.command('remove [alias]')
|
|
@@ -64,6 +75,14 @@ program
|
|
|
64
75
|
.description('Export connections to a JSON file (default: sshm-export.json)')
|
|
65
76
|
.action(exportConnectionsPrompt);
|
|
66
77
|
|
|
78
|
+
program
|
|
79
|
+
.command('fzf')
|
|
80
|
+
.aliases(['instructions', 'install'])
|
|
81
|
+
.description('Show instructions to enable fzf-based connection launcher')
|
|
82
|
+
.action(() => {
|
|
83
|
+
printFzfInstructions();
|
|
84
|
+
});
|
|
85
|
+
|
|
67
86
|
program
|
|
68
87
|
.command('path')
|
|
69
88
|
.description('Provides instructions to make the CLI globally accessible')
|
|
@@ -92,20 +111,39 @@ program
|
|
|
92
111
|
process.exit(0);
|
|
93
112
|
});
|
|
94
113
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const looksLikeBunShim =
|
|
98
|
-
|
|
114
|
+
// Helpers exported for testing primary dispatch logic
|
|
115
|
+
export function normalizeUserArgs(rawArgv: string[]): string[] {
|
|
116
|
+
const looksLikeBunShim = rawArgv[0] === 'bun' && rawArgv[1] === 'run';
|
|
117
|
+
return looksLikeBunShim ? rawArgv.slice(3) : rawArgv;
|
|
118
|
+
}
|
|
99
119
|
|
|
100
|
-
|
|
120
|
+
export function determineEntryAction(userArgs: string[]): 'ssh' | 'interactive' | 'cli' {
|
|
121
|
+
const isSsh = userArgs.length > 0 && (userArgs[0] === 'ssh' || userArgs.some(arg => arg.includes('@')));
|
|
122
|
+
if (isSsh) return 'ssh';
|
|
123
|
+
if (userArgs.length === 0) return 'interactive';
|
|
124
|
+
return 'cli';
|
|
125
|
+
}
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
async function runFromProcess() {
|
|
128
|
+
// Clean Bun compile shim arguments (e.g., "bun run index.ts") when running the compiled binary
|
|
129
|
+
const rawArgs = process.argv.slice(2);
|
|
130
|
+
const userArgs = normalizeUserArgs(rawArgs);
|
|
131
|
+
const action = determineEntryAction(userArgs);
|
|
132
|
+
|
|
133
|
+
if (action === 'ssh') {
|
|
134
|
+
await handleSshCommand(userArgs);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (action === 'interactive') {
|
|
138
|
+
await connectInteractive();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
108
141
|
// Reconstruct argv array expected by commander: [node, script, ...userArgs]
|
|
109
142
|
const argvForCommander: string[] = [process.argv[0] ?? '', process.argv[1] ?? '', ...userArgs];
|
|
110
143
|
program.parse(argvForCommander);
|
|
111
144
|
}
|
|
145
|
+
|
|
146
|
+
// Only execute when this module is the entrypoint (prevents side effects during tests)
|
|
147
|
+
if (import.meta.main) {
|
|
148
|
+
await runFromProcess();
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "sshya",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@types/bun": "latest",
|
|
8
8
|
"@types/chalk": "^2.2.4"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "bun run index.ts",
|
|
15
|
-
"build": "bun build index.ts --compile --outfile bin/
|
|
15
|
+
"build": "bun build index.ts --compile --outfile bin/sshya"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@inquirer/search": "^3.0.17",
|
package/src/connection/list.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { getConnections } from "../database";
|
|
3
3
|
|
|
4
|
-
export async function listConnectionsPrompt() {
|
|
4
|
+
export async function listConnectionsPrompt(options?: { oneline?: boolean; names?: boolean }) {
|
|
5
5
|
const connections = getConnections();
|
|
6
|
+
if (options?.oneline) {
|
|
7
|
+
// Print each connection as ssh-ready args on a single line
|
|
8
|
+
for (const c of connections) {
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
if (c.key_path) {
|
|
11
|
+
// Safely single-quote the key path for shells
|
|
12
|
+
const quotedKey = `'${String(c.key_path).replace(/'/g, "'\\''")}'`;
|
|
13
|
+
parts.push('-i', quotedKey);
|
|
14
|
+
}
|
|
15
|
+
if (c.port) {
|
|
16
|
+
parts.push('-p', String(c.port));
|
|
17
|
+
}
|
|
18
|
+
parts.push(`${c.user}@${c.host}`);
|
|
19
|
+
const args = parts.join(' ');
|
|
20
|
+
if (options?.names) {
|
|
21
|
+
// Tab-separated: alias<TAB>user@host<TAB>args for easy fzf display and parsing
|
|
22
|
+
console.log(`${c.alias}\t${c.user}@${c.host}\t${args}`);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
if (connections.length === 0) {
|
|
7
31
|
console.log(chalk.yellow('No connections found. Add one with "sshya add"'));
|
|
8
32
|
return;
|
package/src/database.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { getConnectionByAlias } from "../database";
|
|
3
|
-
import { runSSH } from "../ssh";
|
|
3
|
+
import { buildSSHArgs, runSSH } from "../ssh";
|
|
4
4
|
import { selectAlias } from "./selectAlias";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -12,8 +12,20 @@ export async function connectInteractive(alias?: string) {
|
|
|
12
12
|
}
|
|
13
13
|
const connection = getConnectionByAlias(alias);
|
|
14
14
|
if (connection) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
runSSH(alias, connection);
|
|
16
|
+
} else {
|
|
17
|
+
console.error(chalk.red('Alias not found'));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function printConnectionsPrompt(alias?: string) {
|
|
22
|
+
if (!alias) {
|
|
23
|
+
alias = await selectAlias('Select a connection');
|
|
24
|
+
}
|
|
25
|
+
const connection = getConnectionByAlias(alias);
|
|
26
|
+
if (connection) {
|
|
27
|
+
let args = buildSSHArgs(connection);
|
|
28
|
+
console.log(args.join(' '));
|
|
17
29
|
} else {
|
|
18
30
|
console.error(chalk.red('Alias not found'));
|
|
19
31
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function printFzfInstructions() {
|
|
4
|
+
console.log(chalk.yellow('To enable a quick fzf-powered SSH launcher, add this to your shell rc:'));
|
|
5
|
+
console.log('\n' + chalk.bold('~/.bashrc or ~/.zshrc') + '\n');
|
|
6
|
+
const snippet = [
|
|
7
|
+
'fzf_sshya() {',
|
|
8
|
+
' local line alias userhost command oldstty',
|
|
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',
|
|
12
|
+
' run_sshya() {',
|
|
13
|
+
' if command -v sshya >/dev/null 2>&1; then',
|
|
14
|
+
' stdbuf -oL sshya "$@"',
|
|
15
|
+
' else',
|
|
16
|
+
' stdbuf -oL bun --silent run start "$@"',
|
|
17
|
+
' fi',
|
|
18
|
+
' }',
|
|
19
|
+
" line=$(run_sshya list --oneline --names | fzf --tac --with-nth=1,2 --delimiter=$'\\t') || { stty \"$oldstty\" 2>/dev/null || true; return; }",
|
|
20
|
+
" alias=${line%%$'\\t'*}",
|
|
21
|
+
" userhost=${line#*$'\\t'}; userhost=${userhost%%$'\\t'*}",
|
|
22
|
+
" echo \"Connecting: $alias ($userhost)\"",
|
|
23
|
+
" # Ensure ZLE releases the terminal before starting interactive ssh",
|
|
24
|
+
" if [ -n \"$ZSH_VERSION\" ]; then zle -I; fi",
|
|
25
|
+
" command=$(run_sshya print \"$alias\")",
|
|
26
|
+
" if [ -n \"$ZSH_VERSION\" ]; then",
|
|
27
|
+
" ssh ${(z)command}",
|
|
28
|
+
" zle -R -c",
|
|
29
|
+
" else",
|
|
30
|
+
" read -r -a __args <<< \"$command\"",
|
|
31
|
+
" ssh \"${__args[@]}\"",
|
|
32
|
+
" fi",
|
|
33
|
+
" stty \"$oldstty\" 2>/dev/null || true",
|
|
34
|
+
'}',
|
|
35
|
+
'# zsh widget binding (recommended):',
|
|
36
|
+
'zle -N fzf_sshya',
|
|
37
|
+
"bindkey '^S' fzf_sshya",
|
|
38
|
+
].join('\n');
|
|
39
|
+
console.log(chalk.cyan(snippet));
|
|
40
|
+
console.log('\n' + chalk.green('Notes:'));
|
|
41
|
+
console.log('- This expects ' + chalk.bold('fzf') + ' to be installed.');
|
|
42
|
+
console.log('- On bash, you can bind with: ' + chalk.cyan("bind -x '" + '\\"\\u0013\\":fzf_sshya' + "'"));
|
|
43
|
+
console.log('- If Ctrl-S is flow control, disable with: ' + chalk.cyan('stty -ixon'));
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
package/src/ssh.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChildProcess, spawn } from 'child_process';
|
|
2
|
+
import type { Connection } from './database';
|
|
2
3
|
import { recordConnectionUsage } from './database';
|
|
3
4
|
|
|
4
5
|
// Track the currently running SSH child-process so other parts of the program
|
|
@@ -9,23 +10,17 @@ export function isSshSessionActive(): boolean {
|
|
|
9
10
|
return currentSshProcess !== null;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function buildSSHArgs(
|
|
13
|
-
const args = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'-o', 'ServerAliveInterval=15',
|
|
18
|
-
'-o', 'TCPKeepAlive=yes',
|
|
19
|
-
];
|
|
20
|
-
if (key_path) args.push('-i', key_path);
|
|
21
|
-
if (port) args.push('-p', port);
|
|
22
|
-
args.push(`${user}@${host}`);
|
|
13
|
+
export function buildSSHArgs(connection: Connection) {
|
|
14
|
+
const args = [];
|
|
15
|
+
if (connection.key_path) args.push('-i', connection.key_path);
|
|
16
|
+
if (connection.port) args.push('-p', connection.port);
|
|
17
|
+
args.push(`${connection.user}@${connection.host}`);
|
|
23
18
|
return args;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
export function runSSH(alias: string,
|
|
21
|
+
export function runSSH(alias: string, connection: Connection) {
|
|
27
22
|
recordConnectionUsage(alias);
|
|
28
|
-
const args = buildSSHArgs(
|
|
23
|
+
const args = buildSSHArgs(connection);
|
|
29
24
|
|
|
30
25
|
const sshProcess = spawn('ssh', args, {
|
|
31
26
|
stdio: 'pipe', // Full control over stdio
|