sshya 0.2.4 → 0.3.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/AGENTS.md +9 -7
- package/README.md +28 -31
- package/index.ts +18 -7
- package/package.json +3 -3
- package/s.ts +24 -0
- package/src/connection/connect.ts +10 -2
- package/src/connection/copy.ts +119 -0
- package/src/connection/index.ts +1 -0
- package/src/helpers/fzf.ts +62 -22
- package/src/helpers/selectAlias.ts +27 -12
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SSHya CLI - SSH Connection Manager
|
|
2
2
|
|
|
3
|
-
**Purpose**: CLI tool for managing SSH connections with aliases, interactive prompts, and fzf
|
|
3
|
+
**Purpose**: CLI tool for managing SSH connections with aliases, interactive prompts, and optional fzf selection.
|
|
4
4
|
|
|
5
5
|
## Core Features
|
|
6
6
|
- **Connection Management**: Add, remove, update, list SSH connections with custom aliases
|
|
@@ -15,19 +15,21 @@
|
|
|
15
15
|
### Database (`src/database.ts`)
|
|
16
16
|
- `Connection` interface with id, alias, user, host, key_path, port, remote_path
|
|
17
17
|
|
|
18
|
-
### Commands (`index.ts`)
|
|
18
|
+
### Commands (`index.ts` / `s.ts`)
|
|
19
19
|
- `add`/`remove`/`update`/`list`: Manage connections
|
|
20
|
-
- `print [alias]`: Output SSH command for scripting
|
|
20
|
+
- `print [alias]`: Output SSH command for scripting
|
|
21
21
|
- `import`/`export`: JSON file operations
|
|
22
|
-
- `fzf
|
|
22
|
+
- `fzf`: In-process fzf picker that connects to the selected alias
|
|
23
|
+
- `s`: Shortcut entrypoint that always runs `connect` (interactive picker or alias)
|
|
23
24
|
|
|
24
25
|
## Usage Patterns
|
|
25
26
|
1. **Direct**: `sshya add` → Interactive prompt flow
|
|
26
|
-
2. **
|
|
27
|
-
3. **
|
|
27
|
+
2. **Fast connect**: `s` or `sshya connect` → built-in fuzzy picker (no shell rc changes)
|
|
28
|
+
3. **fzf**: `sshya fzf` → fzf-based selection, then the same connect flow
|
|
29
|
+
4. **Scripting**: `sshya print my-server` → Outputs `ssh -p 2222 -t -i ~/.ssh/key user@host`
|
|
28
30
|
|
|
29
31
|
## Notable Implementation Details
|
|
30
|
-
-
|
|
32
|
+
- `package.json` bins: `sshya` → `index.ts`, `s` → `s.ts`
|
|
31
33
|
- Usage tracking via `lastUsed` timestamps
|
|
32
34
|
- Always includes `-t` flag for proper pseudo-terminal allocation
|
|
33
35
|
- **High-performance optimizations**: ~0.2ms processing time for 8 connections with caching and pre-computation
|
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@ This project is a command-line interface (CLI) tool named `sshya` for managing S
|
|
|
4
4
|
|
|
5
5
|
## Core Functionality
|
|
6
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`).
|
|
7
|
+
- Connect to saved connections with `s` or `sshya connect` (aliases: `ssh`, `go`).
|
|
8
|
+
- Copy files through saved connections with `sshya copy` (aliases: `sc`, `scp`).
|
|
8
9
|
- Generate SSH command strings for scripting with `sshya print`.
|
|
9
10
|
- Import and export connections from/to a JSON file.
|
|
10
|
-
- fzf
|
|
11
|
+
- Optional fzf picker with `sshya fzf`.
|
|
11
12
|
|
|
12
13
|
## Technology Stack
|
|
13
14
|
- **Runtime:** Bun
|
|
@@ -22,48 +23,44 @@ This project is a command-line interface (CLI) tool named `sshya` for managing S
|
|
|
22
23
|
## Implementation Details
|
|
23
24
|
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.
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Connecting
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Install exposes two commands: `sshya` for management, and `s` as a shortcut to connect. No shell rc changes are required.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
sshya fzf
|
|
36
|
-
# or the aliases
|
|
37
|
-
sshya instructions
|
|
38
|
-
sshya install
|
|
39
|
-
```
|
|
30
|
+
```bash
|
|
31
|
+
s # interactive picker (type to filter)
|
|
32
|
+
s my-server # connect by alias
|
|
33
|
+
sshya connect # same picker
|
|
34
|
+
sshya connect my-server
|
|
35
|
+
```
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
`s` and `sshya connect` use the built-in fuzzy picker. If you prefer fzf:
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
```bash
|
|
40
|
+
sshya fzf
|
|
41
|
+
```
|
|
44
42
|
|
|
45
|
-
|
|
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.
|
|
43
|
+
That requires `fzf` on your PATH. After you pick an entry, it runs the same in-process `connect` flow and spawns your system `ssh` with inherited stdio.
|
|
48
44
|
|
|
49
|
-
###
|
|
45
|
+
### Copy files
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
Use stored connections for `scp` uploads and downloads:
|
|
52
48
|
|
|
53
49
|
```bash
|
|
54
|
-
|
|
50
|
+
sshya copy my-server ./local.txt /tmp/remote.txt
|
|
51
|
+
sshya copy ./local.txt /tmp/remote.txt # interactive connection picker
|
|
52
|
+
sshya copy --download my-server /tmp/remote.txt ./
|
|
53
|
+
sshya copy --recursive my-server ./dist /var/www
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
You can also connect directly without fzf:
|
|
56
|
+
The `copy` command also works as `sshya sc` or `sshya scp`.
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
sshya connect # interactive picker
|
|
64
|
-
```
|
|
58
|
+
Relative remote paths are resolved from the connection's saved remote working directory when one is configured.
|
|
59
|
+
If an exact alias is not found, `connect` and `copy` use the first fuzzy match from the same search used by the interactive picker.
|
|
65
60
|
|
|
66
61
|
### Troubleshooting
|
|
67
62
|
|
|
68
|
-
- If
|
|
63
|
+
- If `s` is not found, reinstall or relink the package so both bins (`sshya` and `s`) are on your PATH.
|
|
64
|
+
- If `s` still opens an old fzf shell function, remove the `s()` snippet from `~/.zshrc` or `~/.bashrc` so the installed binary is used.
|
|
65
|
+
- If `sshya fzf` fails, install `fzf` or use `s` / `sshya connect` instead.
|
|
69
66
|
- If the port or key path is ignored, verify the connection with `sshya test <alias>`.
|
package/index.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
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, connectConnectionPrompt, exportConnectionsPrompt, importConnectionsPrompt, listConnectionsPrompt, removeConnectionPrompt, testConnectionPrompt, updateConnectionPrompt } from './src/connection';
|
|
6
|
+
import { addConnectionPrompt, connectConnectionPrompt, copyConnectionPrompt, 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';
|
|
10
|
-
import {
|
|
10
|
+
import { runFzfLauncher } from './src/helpers/fzf';
|
|
11
11
|
// built-in SSH handler removed
|
|
12
12
|
|
|
13
13
|
const version = pkg?.version ?? '1.0.0';
|
|
@@ -30,6 +30,20 @@ program
|
|
|
30
30
|
.description('Connect to an SSH connection by alias')
|
|
31
31
|
.action(connectConnectionPrompt);
|
|
32
32
|
|
|
33
|
+
program
|
|
34
|
+
.command('copy [alias] [source] [destination]')
|
|
35
|
+
.aliases(['sc', 'scp'])
|
|
36
|
+
.description('Copy files using a stored SSH connection')
|
|
37
|
+
.option('-d, --download', 'Copy from the remote host to a local destination')
|
|
38
|
+
.option('-r, --recursive', 'Copy directories recursively')
|
|
39
|
+
.option('-p, --preserve', 'Preserve modification times, access times, and modes')
|
|
40
|
+
.action((alias?: string, source?: string, destination?: string, options?: { download?: boolean; recursive?: boolean; preserve?: boolean }) => {
|
|
41
|
+
return copyConnectionPrompt(alias, source, destination, {
|
|
42
|
+
download: !!options?.download,
|
|
43
|
+
recursive: !!options?.recursive,
|
|
44
|
+
preserve: !!options?.preserve,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
33
47
|
|
|
34
48
|
program
|
|
35
49
|
.command('print [alias]')
|
|
@@ -79,11 +93,8 @@ program
|
|
|
79
93
|
|
|
80
94
|
program
|
|
81
95
|
.command('fzf')
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.action(() => {
|
|
85
|
-
printFzfInstructions();
|
|
86
|
-
});
|
|
96
|
+
.description('Select a connection with fzf and connect')
|
|
97
|
+
.action(runFzfLauncher);
|
|
87
98
|
|
|
88
99
|
program
|
|
89
100
|
.command('path')
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "sshya",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"description": "CLI for managing SSH connections with aliases, interactive prompts, and fzf integration",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@types/bun": "latest",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "bun run index.ts",
|
|
16
|
-
"build": "bun build index.ts --compile --outfile bin/sshya"
|
|
16
|
+
"build": "bun build index.ts --compile --outfile bin/sshya && bun build s.ts --compile --outfile bin/s"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@inquirer/search": "^3.0.17",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
30
|
"sshya": "index.ts",
|
|
31
|
-
"s": "
|
|
31
|
+
"s": "s.ts"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/s.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { connectConnectionPrompt } from './src/connection';
|
|
4
|
+
import { initDB } from './src/database';
|
|
5
|
+
import { enableEscapeExit } from './src/helpers/escExit';
|
|
6
|
+
|
|
7
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
8
|
+
enableEscapeExit();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
initDB();
|
|
12
|
+
|
|
13
|
+
const rawArgs = process.argv.slice(2);
|
|
14
|
+
const looksLikeBunShim = rawArgs[0] === 'bun' && rawArgs[1] === 'run';
|
|
15
|
+
const userArgs = looksLikeBunShim ? rawArgs.slice(3) : rawArgs;
|
|
16
|
+
const arg = userArgs[0];
|
|
17
|
+
|
|
18
|
+
if (arg === '-h' || arg === '--help') {
|
|
19
|
+
console.log('Usage: s [alias]');
|
|
20
|
+
console.log('Connect to a saved SSH connection. Omit alias to pick interactively.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await connectConnectionPrompt(arg);
|
|
@@ -2,14 +2,22 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { expandHomePath, getConnectionByAlias, recordConnectionUsage } from '../database';
|
|
4
4
|
import { buildRemoteCommand } from '../helpers/shell';
|
|
5
|
-
import { selectAlias } from '../helpers/selectAlias';
|
|
5
|
+
import { findAliasBySearchTerm, selectAlias } from '../helpers/selectAlias';
|
|
6
6
|
|
|
7
7
|
export async function connectConnectionPrompt(alias?: string): Promise<void> {
|
|
8
8
|
if (!alias) {
|
|
9
9
|
alias = await selectAlias('Select a connection to connect');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
let connection = getConnectionByAlias(alias);
|
|
13
|
+
if (!connection) {
|
|
14
|
+
const matchedAlias = findAliasBySearchTerm(alias);
|
|
15
|
+
if (matchedAlias) {
|
|
16
|
+
alias = matchedAlias;
|
|
17
|
+
connection = getConnectionByAlias(alias);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
if (!connection) {
|
|
14
22
|
console.error(chalk.red('Alias not found'));
|
|
15
23
|
process.exit(1);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { expandHomePath, getConnectionByAlias, recordConnectionUsage, type Connection } from '../database';
|
|
5
|
+
import { findAliasBySearchTerm, selectAlias } from '../helpers/selectAlias';
|
|
6
|
+
|
|
7
|
+
interface CopyOptions {
|
|
8
|
+
download?: boolean;
|
|
9
|
+
recursive?: boolean;
|
|
10
|
+
preserve?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NormalizedCopyArgs {
|
|
14
|
+
alias?: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
destination?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isAbsoluteRemotePath = (value: string): boolean => {
|
|
20
|
+
return value.startsWith('/') || value.startsWith('~/') || value === '~';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const resolveRemotePath = (connection: Connection, value: string): string => {
|
|
24
|
+
const remotePath = value.trim();
|
|
25
|
+
const remoteBase = connection.remote_path?.trim();
|
|
26
|
+
|
|
27
|
+
if (!remoteBase || isAbsoluteRemotePath(remotePath)) {
|
|
28
|
+
return remotePath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return path.posix.join(remoteBase, remotePath);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const buildRemoteSpec = (connection: Connection, remotePath: string): string => {
|
|
35
|
+
return `${connection.user}@${connection.host}:${resolveRemotePath(connection, remotePath)}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeCopyArgs = (first?: string, second?: string, third?: string): NormalizedCopyArgs => {
|
|
39
|
+
if (third) {
|
|
40
|
+
return { alias: first, source: second, destination: third };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (first && second) {
|
|
44
|
+
const maybeConnection = getConnectionByAlias(first);
|
|
45
|
+
if (maybeConnection) {
|
|
46
|
+
return { alias: first, source: second };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { source: first, destination: second };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { alias: first, source: second, destination: third };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export async function copyConnectionPrompt(
|
|
56
|
+
first?: string,
|
|
57
|
+
second?: string,
|
|
58
|
+
third?: string,
|
|
59
|
+
options: CopyOptions = {},
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const { download = false, recursive = false, preserve = false } = options;
|
|
62
|
+
let { alias, source, destination } = normalizeCopyArgs(first, second, third);
|
|
63
|
+
|
|
64
|
+
if (!source || !destination) {
|
|
65
|
+
console.error(chalk.red('Source and destination are required'));
|
|
66
|
+
console.error(chalk.grey('Usage: sshya copy [alias] <source> <destination>'));
|
|
67
|
+
console.error(chalk.grey(' sshya copy --download [alias] <remote-source> <local-destination>'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!alias) {
|
|
72
|
+
alias = await selectAlias(download ? 'Select a connection to copy from' : 'Select a connection to copy to');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let connection = getConnectionByAlias(alias);
|
|
76
|
+
if (!connection) {
|
|
77
|
+
const matchedAlias = findAliasBySearchTerm(alias);
|
|
78
|
+
if (matchedAlias) {
|
|
79
|
+
alias = matchedAlias;
|
|
80
|
+
connection = getConnectionByAlias(alias);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!connection) {
|
|
85
|
+
console.error(chalk.red('Alias not found'));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const args: string[] = [];
|
|
90
|
+
if (connection.key_path) {
|
|
91
|
+
args.push('-i', expandHomePath(connection.key_path) ?? connection.key_path);
|
|
92
|
+
}
|
|
93
|
+
if (connection.port) {
|
|
94
|
+
args.push('-P', String(connection.port));
|
|
95
|
+
}
|
|
96
|
+
if (recursive) {
|
|
97
|
+
args.push('-r');
|
|
98
|
+
}
|
|
99
|
+
if (preserve) {
|
|
100
|
+
args.push('-p');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (download) {
|
|
104
|
+
args.push(buildRemoteSpec(connection, source), destination);
|
|
105
|
+
} else {
|
|
106
|
+
args.push(source, buildRemoteSpec(connection, destination));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
recordConnectionUsage(alias);
|
|
110
|
+
|
|
111
|
+
const child = spawn('scp', args, { stdio: 'inherit' });
|
|
112
|
+
child.on('error', (error) => {
|
|
113
|
+
console.error(chalk.red(`Failed to start scp: ${error.message}`));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
child.on('close', (code) => {
|
|
117
|
+
process.exit(code ?? 1);
|
|
118
|
+
});
|
|
119
|
+
}
|
package/src/connection/index.ts
CHANGED
package/src/helpers/fzf.ts
CHANGED
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { connectConnectionPrompt } from '../connection/connect';
|
|
4
|
+
import { getConnections } from '../database';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
6
|
+
const FZF_ARGS = [
|
|
7
|
+
'--with-nth=1,2',
|
|
8
|
+
'--nth=1,2',
|
|
9
|
+
'--delimiter=\t',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export async function runFzfLauncher(): Promise<void> {
|
|
13
|
+
const connections = getConnections()
|
|
14
|
+
.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0));
|
|
15
|
+
|
|
16
|
+
if (connections.length === 0) {
|
|
17
|
+
console.log(chalk.yellow('No connections found. Add one with "sshya add"'));
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const input = connections
|
|
22
|
+
.map((c) => `${c.alias}\t${c.user}@${c.host}`)
|
|
23
|
+
.join('\n');
|
|
24
|
+
|
|
25
|
+
const alias = await pickAliasWithFzf(input);
|
|
26
|
+
if (!alias) {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await connectConnectionPrompt(alias);
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
function pickAliasWithFzf(input: string): Promise<string | undefined> {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const child = spawn('fzf', FZF_ARGS, { stdio: ['pipe', 'pipe', 'inherit'] });
|
|
27
36
|
|
|
37
|
+
child.on('error', (error: NodeJS.ErrnoException) => {
|
|
38
|
+
if (error.code === 'ENOENT') {
|
|
39
|
+
console.error(chalk.red('fzf is not installed. Use "s" or "sshya connect" instead.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.error(chalk.red(`Failed to start fzf: ${error.message}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let stdout = '';
|
|
47
|
+
child.stdout.on('data', (chunk: Buffer | string) => {
|
|
48
|
+
stdout += chunk.toString();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.stdin.on('error', () => {
|
|
52
|
+
// fzf may close stdin early when the user cancels.
|
|
53
|
+
});
|
|
54
|
+
child.stdin.write(input);
|
|
55
|
+
child.stdin.end();
|
|
56
|
+
|
|
57
|
+
child.on('close', (code) => {
|
|
58
|
+
if (code !== 0) {
|
|
59
|
+
resolve(undefined);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const alias = stdout.trim().split('\t')[0];
|
|
64
|
+
resolve(alias || undefined);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -4,6 +4,30 @@ import inquirer from "inquirer";
|
|
|
4
4
|
import { getConnections } from "../database";
|
|
5
5
|
import { enableEscapeExit } from "./escExit";
|
|
6
6
|
|
|
7
|
+
const getAliasChoices = () => {
|
|
8
|
+
return getConnections()
|
|
9
|
+
.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))
|
|
10
|
+
.map(c => ({ name: c.alias, value: c.alias }));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const searchAliasChoices = (term: string) => {
|
|
14
|
+
const choices = getAliasChoices();
|
|
15
|
+
if (term.trim().length === 0) {
|
|
16
|
+
return choices;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fuse = new Fuse(choices, {
|
|
20
|
+
keys: ['name'],
|
|
21
|
+
threshold: 0.4,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return fuse.search(term).map(r => r.item);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const findAliasBySearchTerm = (term: string): string | undefined => {
|
|
28
|
+
return searchAliasChoices(term)[0]?.value;
|
|
29
|
+
};
|
|
30
|
+
|
|
7
31
|
/**
|
|
8
32
|
* Prompt user to choose a connection alias using fuzzy search.
|
|
9
33
|
* Aborts the process if user presses ESC or if no connections are available.
|
|
@@ -11,8 +35,7 @@ import { enableEscapeExit } from "./escExit";
|
|
|
11
35
|
* @returns selected alias as string
|
|
12
36
|
*/
|
|
13
37
|
export async function selectAlias(message: string = "Select a connection"): Promise<string> {
|
|
14
|
-
|
|
15
|
-
if (connections.length === 0) {
|
|
38
|
+
if (getConnections().length === 0) {
|
|
16
39
|
console.log(chalk.yellow('No connections found. Add one with "sshya add"'));
|
|
17
40
|
process.exit(0);
|
|
18
41
|
}
|
|
@@ -29,20 +52,12 @@ export async function selectAlias(message: string = "Select a connection"): Prom
|
|
|
29
52
|
// The signature matches the @inquirer/search documentation:
|
|
30
53
|
// (term: string | void, { signal }: { signal: AbortSignal }) => Promise<Choice[]>
|
|
31
54
|
source: async (term: unknown) => {
|
|
32
|
-
const allChoices = connections.map(c => ({ name: c.alias, value: c.alias }));
|
|
33
|
-
|
|
34
55
|
// When term is undefined or empty, return the full, recently-used-sorted list.
|
|
35
56
|
if (typeof term !== 'string' || term.trim().length === 0) {
|
|
36
|
-
return
|
|
57
|
+
return searchAliasChoices('');
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
const fuse = new Fuse(allChoices, {
|
|
41
|
-
keys: ['name'],
|
|
42
|
-
threshold: 0.4,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
return fuse.search(term).map(r => r.item);
|
|
60
|
+
return searchAliasChoices(term);
|
|
46
61
|
},
|
|
47
62
|
},
|
|
48
63
|
]);
|