sshya 0.1.2 → 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/index.ts CHANGED
@@ -5,10 +5,10 @@ import { Command } from 'commander';
5
5
  import pkg from './package.json' assert { type: "json" };
6
6
  import { addConnectionPrompt, exportConnectionsPrompt, importConnectionsPrompt, listConnectionsPrompt, removeConnectionPrompt, testConnectionPrompt, updateConnectionPrompt } from './src/connection';
7
7
  import { initDB } from './src/database';
8
- import { connectInteractive, printConnectionsPrompt } from './src/helpers/connection';
8
+ import { printConnectionsPrompt } from './src/helpers/connection';
9
9
  import { enableEscapeExit } from './src/helpers/escExit';
10
10
  import { printFzfInstructions } from './src/helpers/fzf';
11
- import { handleSshCommand } from './src/sshCommandHandler';
11
+ // built-in SSH handler removed
12
12
 
13
13
  const version = pkg?.version ?? '1.0.0';
14
14
 
@@ -24,10 +24,7 @@ program
24
24
  .version(version)
25
25
  .description('A simple CLI to manage your SSH connections');
26
26
 
27
- program
28
- .command('connect [alias]')
29
- .description('Connect to an SSH connection by alias')
30
- .action(connectInteractive);
27
+ // connect command removed – use system ssh via fzf snippet
31
28
 
32
29
  program
33
30
  .command('print [alias]')
@@ -117,27 +114,11 @@ export function normalizeUserArgs(rawArgv: string[]): string[] {
117
114
  return looksLikeBunShim ? rawArgv.slice(3) : rawArgv;
118
115
  }
119
116
 
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
- }
126
117
 
127
118
  async function runFromProcess() {
128
119
  // Clean Bun compile shim arguments (e.g., "bun run index.ts") when running the compiled binary
129
120
  const rawArgs = process.argv.slice(2);
130
121
  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
- }
141
122
  // Reconstruct argv array expected by commander: [node, script, ...userArgs]
142
123
  const argvForCommander: string[] = [process.argv[0] ?? '', process.argv[1] ?? '', ...userArgs];
143
124
  program.parse(argvForCommander);
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.2",
5
+ "version": "0.2.0",
6
6
  "devDependencies": {
7
7
  "@types/bun": "latest",
8
8
  "@types/chalk": "^2.2.4"
@@ -31,6 +31,11 @@ export async function addConnectionPrompt() {
31
31
  name: 'key_path',
32
32
  message: 'Path to key (optional):',
33
33
  },
34
+ {
35
+ type: 'input',
36
+ name: 'remote_path',
37
+ message: 'Remote working directory (optional):',
38
+ },
34
39
  ]);
35
40
 
36
41
  const parsed = connectionSchema.safeParse(answers);
@@ -38,10 +43,10 @@ export async function addConnectionPrompt() {
38
43
  console.error(chalk.red('Invalid input:'), parsed.error.errors.map(e => e.message).join(', '));
39
44
  return;
40
45
  }
41
- const { alias, user, host, key_path, port } = parsed.data;
46
+ const { alias, user, host, key_path, port, remote_path } = parsed.data;
42
47
 
43
48
  try {
44
- addConnection(alias, user, host, key_path, port ? String(port) : undefined);
49
+ addConnection(alias, user, host, key_path, port ? String(port) : undefined, remote_path);
45
50
  console.log(chalk.green('Connection added successfully'));
46
51
 
47
52
  const { test } = await inquirer.prompt([
@@ -50,11 +50,25 @@ export const importConnectionsPrompt = async (file: string) => {
50
50
  }
51
51
 
52
52
  if (action === 'Overwrite') {
53
- updateConnection(parsedConn.alias, parsedConn.user, parsedConn.host, parsedConn.key_path ?? '', parsedConn.port ? String(parsedConn.port) : undefined);
53
+ updateConnection(
54
+ parsedConn.alias,
55
+ parsedConn.user,
56
+ parsedConn.host,
57
+ parsedConn.key_path ?? '',
58
+ parsedConn.port ? String(parsedConn.port) : undefined,
59
+ parsedConn.remote_path,
60
+ );
54
61
  updatedCount++;
55
62
  }
56
63
  } else {
57
- addConnection(parsedConn.alias, parsedConn.user, parsedConn.host, parsedConn.key_path, parsedConn.port ? String(parsedConn.port) : undefined);
64
+ addConnection(
65
+ parsedConn.alias,
66
+ parsedConn.user,
67
+ parsedConn.host,
68
+ parsedConn.key_path,
69
+ parsedConn.port ? String(parsedConn.port) : undefined,
70
+ parsedConn.remote_path,
71
+ );
58
72
  importedCount++;
59
73
  }
60
74
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { getConnections } from "../database";
2
+ import { expandHomePath, getConnections } from "../database";
3
+ import { buildRemoteCommand } from "../helpers/shell";
3
4
 
4
5
  export async function listConnectionsPrompt(options?: { oneline?: boolean; names?: boolean }) {
5
6
  const connections = getConnections();
@@ -8,14 +9,24 @@ export async function listConnectionsPrompt(options?: { oneline?: boolean; names
8
9
  for (const c of connections) {
9
10
  const parts: string[] = [];
10
11
  if (c.key_path) {
12
+ const expandedKeyPath = expandHomePath(String(c.key_path));
11
13
  // Safely single-quote the key path for shells
12
- const quotedKey = `'${String(c.key_path).replace(/'/g, "'\\''")}'`;
14
+ const quotedKey = `'${String(expandedKeyPath).replace(/'/g, "'\\''")}'`;
13
15
  parts.push('-i', quotedKey);
14
16
  }
15
17
  if (c.port) {
16
18
  parts.push('-p', String(c.port));
17
19
  }
18
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
+ }
19
30
  const args = parts.join(' ');
20
31
  if (options?.names) {
21
32
  // Tab-separated: alias<TAB>user@host<TAB>args for easy fzf display and parsing
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { spawn } from 'child_process';
3
- import { getConnectionByAlias } from '../database';
3
+ import { expandHomePath, getConnectionByAlias } from '../database';
4
4
  import { selectAlias } from '../helpers/selectAlias';
5
+ import { buildRemoteCommand } from '../helpers/shell';
5
6
 
6
7
  export async function testConnectionPrompt(alias?: string): Promise<void> {
7
8
 
@@ -15,14 +16,25 @@ export async function testConnectionPrompt(alias?: string): Promise<void> {
15
16
  return;
16
17
  }
17
18
 
18
- const { user, host, key_path, port } = connection;
19
+ const { user, host, key_path, port, remote_path } = connection;
19
20
  console.log(`\nTesting connection to ${chalk.blue(alias)}...`);
20
21
 
21
22
  const testPromise = new Promise<{ code: number | null; stderr: string }>((resolve) => {
22
23
  const args = ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5'];
23
- if (key_path) args.push('-i', key_path);
24
+ if (key_path) args.push('-i', expandHomePath(key_path) ?? key_path);
24
25
  if (port) args.push('-p', String(port));
25
- args.push(`${user}@${host}`, 'exit');
26
+ args.push(`${user}@${host}`);
27
+ if (remote_path) {
28
+ const remoteCommand = buildRemoteCommand({
29
+ remotePath: remote_path,
30
+ postCommand: 'exit',
31
+ });
32
+ if (remoteCommand) {
33
+ args.push('-t', remoteCommand);
34
+ }
35
+ } else {
36
+ args.push('exit');
37
+ }
26
38
 
27
39
  const child = spawn('ssh', args, { stdio: ['ignore', 'ignore', 'pipe'] });
28
40
  let stderr = '';
@@ -37,8 +37,14 @@ export async function updateConnectionPrompt(alias?: string) {
37
37
  message: 'Path to key (optional):',
38
38
  default: connection.key_path,
39
39
  },
40
+ {
41
+ type: 'input',
42
+ name: 'remote_path',
43
+ message: 'Remote working directory (optional):',
44
+ default: connection.remote_path,
45
+ },
40
46
  ]);
41
- updateConnection(alias, answers.user, answers.host, answers.key_path, answers.port);
47
+ updateConnection(alias, answers.user, answers.host, answers.key_path, answers.port, answers.remote_path);
42
48
  console.log(chalk.green('Connection updated successfully'));
43
49
 
44
50
  const { test } = await inquirer.prompt([
package/src/database.ts CHANGED
@@ -13,6 +13,7 @@ export interface Connection {
13
13
  host: string;
14
14
  key_path?: string;
15
15
  port?: string;
16
+ remote_path?: string;
16
17
  lastUsed?: number;
17
18
  }
18
19
 
@@ -22,14 +23,53 @@ export const connectionSchema = z.object({
22
23
  host: z.string().min(1),
23
24
  key_path: z.string().optional(),
24
25
  port: z.union([z.string(), z.number()]).optional(),
26
+ remote_path: z.string().optional(),
25
27
  });
26
28
 
29
+ const normalizeOptionalString = (value?: string | number | null): string | undefined => {
30
+ if (value === undefined || value === null) {
31
+ return undefined;
32
+ }
33
+ const trimmed = String(value).trim();
34
+ return trimmed.length > 0 ? trimmed : undefined;
35
+ };
36
+
37
+ export const expandHomePath = (input?: string): string | undefined => {
38
+ if (!input) {
39
+ return undefined;
40
+ }
41
+ if (input === '~') {
42
+ return os.homedir();
43
+ }
44
+ if (input.startsWith('~/')) {
45
+ return path.join(os.homedir(), input.slice(2));
46
+ }
47
+ return input;
48
+ };
49
+
50
+ const normalizeConnection = (connection: Connection): Connection => {
51
+ const normalizedKeyPath = expandHomePath(normalizeOptionalString(connection.key_path));
52
+ const normalizedPort = normalizeOptionalString(connection.port);
53
+ const normalizedRemotePath = normalizeOptionalString(connection.remote_path);
54
+
55
+ return {
56
+ ...connection,
57
+ alias: connection.alias.trim(),
58
+ user: connection.user.trim(),
59
+ host: connection.host.trim(),
60
+ key_path: normalizedKeyPath,
61
+ port: normalizedPort,
62
+ remote_path: normalizedRemotePath,
63
+ };
64
+ };
65
+
27
66
  const readDB = (): Connection[] => {
28
67
  if (!fs.existsSync(dbFilePath)) {
29
68
  return [];
30
69
  }
31
70
  const data = fs.readFileSync(dbFilePath, 'utf-8');
32
- return JSON.parse(data);
71
+ const parsed: Connection[] = JSON.parse(data);
72
+ return parsed.map(normalizeConnection);
33
73
  };
34
74
 
35
75
  const writeDB = (data: Connection[]) => {
@@ -49,21 +89,42 @@ export const initDB = () => {
49
89
  // Initialize empty database file if it does not exist
50
90
  if (!fs.existsSync(dbFilePath)) {
51
91
  writeDB([]);
92
+ return;
52
93
  }
94
+
95
+ // Normalize any existing data on disk once during init
96
+ const existingData = readDB();
97
+ writeDB(existingData);
53
98
  };
54
99
 
55
- export const addConnection = (alias: string, user: string, host: string, key_path?: string, port?: string) => {
100
+ export const addConnection = (
101
+ alias: string,
102
+ user: string,
103
+ host: string,
104
+ key_path?: string,
105
+ port?: string,
106
+ remote_path?: string,
107
+ ) => {
56
108
  const connections = readDB();
57
- if (connections.some(c => c.alias === alias)) {
58
- throw new Error(`Connection with alias "${alias}" already exists.`);
109
+ const normalizedAlias = alias.trim();
110
+ if (connections.some(c => c.alias === normalizedAlias)) {
111
+ throw new Error(`Connection with alias "${normalizedAlias}" already exists.`);
59
112
  }
113
+
114
+ const normalizedUser = user.trim();
115
+ const normalizedHost = host.trim();
116
+ const normalizedKeyPath = expandHomePath(normalizeOptionalString(key_path));
117
+ const normalizedPort = normalizeOptionalString(port);
118
+ const normalizedRemotePath = normalizeOptionalString(remote_path);
119
+
60
120
  const newConnection: Connection = {
61
121
  id: connections.length > 0 ? Math.max(...connections.map(c => c.id)) + 1 : 1,
62
- alias,
63
- user,
64
- host,
65
- key_path,
66
- port,
122
+ alias: normalizedAlias,
123
+ user: normalizedUser,
124
+ host: normalizedHost,
125
+ key_path: normalizedKeyPath,
126
+ port: normalizedPort,
127
+ remote_path: normalizedRemotePath,
67
128
  };
68
129
  connections.push(newConnection);
69
130
  writeDB(connections);
@@ -84,14 +145,22 @@ export const removeConnection = (alias: string) => {
84
145
  writeDB(newConnections);
85
146
  };
86
147
 
87
- export const updateConnection = (alias: string, newUser: string, newHost: string, newKeyPath: string, newPort?: string) => {
148
+ export const updateConnection = (
149
+ alias: string,
150
+ newUser: string,
151
+ newHost: string,
152
+ newKeyPath?: string,
153
+ newPort?: string,
154
+ newRemotePath?: string,
155
+ ) => {
88
156
  const connections = readDB();
89
157
  const connection = connections.find(c => c.alias === alias);
90
158
  if (connection) {
91
- connection.user = newUser;
92
- connection.host = newHost;
93
- connection.key_path = newKeyPath;
94
- connection.port = newPort;
159
+ connection.user = newUser.trim();
160
+ connection.host = newHost.trim();
161
+ connection.key_path = expandHomePath(normalizeOptionalString(newKeyPath));
162
+ connection.port = normalizeOptionalString(newPort);
163
+ connection.remote_path = normalizeOptionalString(newRemotePath);
95
164
  writeDB(connections);
96
165
  }
97
166
  };
@@ -1,32 +1,35 @@
1
1
  import chalk from "chalk";
2
- import { getConnectionByAlias } from "../database";
3
- import { buildSSHArgs, runSSH } from "../ssh";
4
- import { selectAlias } from "./selectAlias";
2
+ import { expandHomePath, getConnectionByAlias } from "../database";
3
+ import { buildRemoteCommand } from "./shell";
5
4
 
6
- /**
7
- * Helper to prompt user to select a connection and then connect
8
- */
9
- export async function connectInteractive(alias?: string) {
5
+ export async function printConnectionsPrompt(alias?: string) {
10
6
  if (!alias) {
11
- alias = await selectAlias('Select a connection');
7
+ console.error(chalk.red('Alias is required'));
8
+ process.exit(1);
12
9
  }
13
10
  const connection = getConnectionByAlias(alias);
14
- if (connection) {
15
- runSSH(alias, connection);
16
- } else {
11
+ if (!connection) {
17
12
  console.error(chalk.red('Alias not found'));
13
+ process.exit(1);
18
14
  }
19
- }
20
-
21
- export async function printConnectionsPrompt(alias?: string) {
22
- if (!alias) {
23
- alias = await selectAlias('Select a connection');
15
+ const parts: string[] = [];
16
+ if (connection.key_path) {
17
+ const expandedKeyPath = expandHomePath(String(connection.key_path));
18
+ const quotedKey = `'${String(expandedKeyPath).replace(/'/g, "'\\''")}'`;
19
+ parts.push('-i', quotedKey);
24
20
  }
25
- const connection = getConnectionByAlias(alias);
26
- if (connection) {
27
- let args = buildSSHArgs(connection);
28
- console.log(args.join(' '));
29
- } else {
30
- console.error(chalk.red('Alias not found'));
21
+ if (connection.port) {
22
+ parts.push('-p', String(connection.port));
23
+ }
24
+ parts.push(`${connection.user}@${connection.host}`);
25
+ if (connection.remote_path) {
26
+ const remoteCommand = buildRemoteCommand({
27
+ remotePath: String(connection.remote_path),
28
+ postCommand: 'exec "$SHELL" -l',
29
+ });
30
+ if (remoteCommand) {
31
+ parts.push('-t', remoteCommand);
32
+ }
31
33
  }
34
+ console.log(parts.join(' '));
32
35
  }
@@ -1,5 +1,4 @@
1
1
  import readline from 'readline';
2
- import { isSshSessionActive } from '../ssh';
3
2
 
4
3
  /**
5
4
  * Enable global Escape key handler that terminates the CLI gracefully.
@@ -16,12 +15,7 @@ export function enableEscapeExit(): () => void {
16
15
  }
17
16
 
18
17
  const onKeypress = (_: string, key: readline.Key) => {
19
- // Handle ESC key – ignored while an SSH session is active so that
20
- // pressing Escape inside the remote shell works as expected.
21
18
  if (key.name === 'escape') {
22
- if (isSshSessionActive()) {
23
- return; // let the remote shell receive the key
24
- }
25
19
  console.log('\nCancelled by user.');
26
20
  process.exit(0);
27
21
  }
@@ -32,9 +26,6 @@ export function enableEscapeExit(): () => void {
32
26
  // their usual shortcut inside the SSH client, so we don’t force-exit
33
27
  // the parent app in that case.
34
28
  if (key.ctrl && key.name === 'c') {
35
- if (isSshSessionActive()) {
36
- return; // let ssh handle the interrupt
37
- }
38
29
  console.log('\nCancelled by user.');
39
30
  process.exit(0);
40
31
  }
@@ -4,7 +4,7 @@ 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
- 'fzf_sshya() {',
7
+ '_fzf_sshya() {',
8
8
  ' local line alias userhost command oldstty',
9
9
  ' # Temporarily disable XON/XOFF so Ctrl-S works, then restore',
10
10
  ' oldstty=$(stty -g 2>/dev/null)',
@@ -19,12 +19,22 @@ export function printFzfInstructions() {
19
19
  " line=$(run_sshya list --oneline --names | fzf --tac --with-nth=1,2 --delimiter=$'\\t') || { stty \"$oldstty\" 2>/dev/null || true; return; }",
20
20
  " alias=${line%%$'\\t'*}",
21
21
  " userhost=${line#*$'\\t'}; userhost=${userhost%%$'\\t'*}",
22
+ " command=$(run_sshya print \"$alias\")",
22
23
  " echo \"Connecting: $alias ($userhost)\"",
23
24
  " # 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
25
  " if [ -n \"$ZSH_VERSION\" ]; then",
27
- " ssh ${(z)command}",
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",
28
38
  " zle -R -c",
29
39
  " else",
30
40
  " read -r -a __args <<< \"$command\"",
@@ -33,13 +43,13 @@ export function printFzfInstructions() {
33
43
  " stty \"$oldstty\" 2>/dev/null || true",
34
44
  '}',
35
45
  '# zsh widget binding (recommended):',
36
- 'zle -N fzf_sshya',
37
- "bindkey '^S' fzf_sshya",
46
+ 'zle -N _fzf_sshya',
47
+ "bindkey '^S' _fzf_sshya",
38
48
  ].join('\n');
39
49
  console.log(chalk.cyan(snippet));
40
50
  console.log('\n' + chalk.green('Notes:'));
41
51
  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' + "'"));
52
+ console.log('- On bash, you can bind with: ' + chalk.cyan("bind -x '" + '\\"\\u0013\\":_fzf_sshya' + "'"));
43
53
  console.log('- If Ctrl-S is flow control, disable with: ' + chalk.cyan('stty -ixon'));
44
54
  process.exit(0);
45
55
  }
@@ -0,0 +1,26 @@
1
+ export const escapeSingleQuotes = (value: string): string => value.replace(/'/g, "'\\''");
2
+
3
+ export const wrapInSingleQuotes = (value: string): string => `'${escapeSingleQuotes(value)}'`;
4
+
5
+ interface RemoteCommandOptions {
6
+ remotePath?: string;
7
+ postCommand?: string;
8
+ }
9
+
10
+ export const buildRemoteCommand = ({
11
+ remotePath,
12
+ postCommand,
13
+ }: RemoteCommandOptions): string | undefined => {
14
+ const commands: string[] = [];
15
+ if (typeof remotePath === 'string' && remotePath.trim().length > 0) {
16
+ commands.push(`cd ${wrapInSingleQuotes(remotePath.trim())}`);
17
+ }
18
+ if (postCommand && postCommand.trim().length > 0) {
19
+ commands.push(postCommand.trim());
20
+ }
21
+ if (commands.length === 0) {
22
+ return undefined;
23
+ }
24
+ return commands.join(' && ');
25
+ };
26
+
package/src/ssh.ts CHANGED
@@ -1,71 +1,23 @@
1
- import { ChildProcess, spawn } from 'child_process';
2
1
  import type { Connection } from './database';
3
- import { recordConnectionUsage } from './database';
4
-
5
- // Track the currently running SSH child-process so other parts of the program
6
- // (e.g. the global key-handler) can know whether we are inside an SSH session.
7
- let currentSshProcess: ChildProcess | null = null;
8
-
9
- export function isSshSessionActive(): boolean {
10
- return currentSshProcess !== null;
11
- }
2
+ import { expandHomePath } from './database';
3
+ import { buildRemoteCommand } from './helpers/shell';
12
4
 
13
5
  export function buildSSHArgs(connection: Connection) {
14
- const args = [];
15
- if (connection.key_path) args.push('-i', connection.key_path);
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
+ }
16
11
  if (connection.port) args.push('-p', connection.port);
17
12
  args.push(`${connection.user}@${connection.host}`);
18
- return args;
19
- }
20
-
21
- export function runSSH(alias: string, connection: Connection) {
22
- recordConnectionUsage(alias);
23
- const args = buildSSHArgs(connection);
24
-
25
- const sshProcess = spawn('ssh', args, {
26
- stdio: 'pipe', // Full control over stdio
27
- });
28
-
29
- // Mark session active so that other modules can detect we are now inside an
30
- // SSH session.
31
- currentSshProcess = sshProcess;
32
-
33
- if (process.stdin.isTTY) {
34
- process.stdin.setRawMode(true);
35
- }
36
-
37
- process.stdin.pipe(sshProcess.stdin);
38
- sshProcess.stdout.pipe(process.stdout);
39
- sshProcess.stderr.pipe(process.stderr);
40
-
41
- const onResize = () => {
42
- if (sshProcess.pid) {
43
- try {
44
- process.kill(sshProcess.pid, 'SIGWINCH');
45
- } catch {
46
- /* Process already exited – ignore */
47
- }
48
- }
49
- };
50
-
51
- const cleanup = (code?: number | null) => {
52
- if (process.stdout.isTTY) {
53
- process.stdout.removeListener('resize', onResize);
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);
54
20
  }
55
- if (process.stdin.isTTY) {
56
- process.stdin.setRawMode(false);
57
- }
58
- process.stdin.unpipe(sshProcess.stdin);
59
- // Mark session ended
60
- currentSshProcess = null;
61
- process.exit(code ?? 0);
62
- };
63
-
64
- if (process.stdout.isTTY) {
65
- process.stdout.on('resize', onResize);
66
- onResize();
67
21
  }
68
-
69
- sshProcess.on('exit', cleanup);
70
- sshProcess.on('error', () => cleanup(1));
71
- }
22
+ return args.map(String);
23
+ }
@@ -1,93 +0,0 @@
1
- import chalk from 'chalk';
2
- import inquirer from 'inquirer';
3
- import { addConnection, getConnections } from './database';
4
- import { runSSH } from './ssh';
5
-
6
- export interface ParsedSSH {
7
- user: string;
8
- host: string;
9
- key_path?: string;
10
- port?: string;
11
- }
12
-
13
- export function parseSshCommand(args: string[]): ParsedSSH | null {
14
- if (args[0] === 'ssh') {
15
- args = args.slice(1);
16
- }
17
-
18
- let user = '';
19
- let host = '';
20
- let key_path: string | undefined;
21
- let port: string | undefined;
22
-
23
- const userHostArg = args.find(arg => arg.includes('@') && !arg.startsWith('-'));
24
- if (!userHostArg) return null;
25
-
26
- const parts = userHostArg.split('@');
27
- if (parts.length < 2) return null;
28
- user = parts[0] ?? '';
29
- host = parts[1] ?? '';
30
-
31
- for (let i = 0; i < args.length; i++) {
32
- const arg = args[i];
33
- if (!arg) continue;
34
-
35
- if (arg === '-i' && i + 1 < args.length) {
36
- key_path = args[i + 1]!;
37
- i++;
38
- // Support combined flag and value, e.g. -i/home/key.pem
39
- } else if (arg.startsWith('-i') && arg.length > 2) {
40
- key_path = arg.slice(2);
41
- } else if (arg === '-p' && i + 1 < args.length) {
42
- port = args[i + 1]!;
43
- i++;
44
- // Support combined flag and value, e.g. -p5572
45
- } else if (arg.startsWith('-p') && arg.length > 2) {
46
- port = arg.slice(2);
47
- }
48
- }
49
-
50
- return { user, host, key_path, port };
51
- }
52
-
53
- export async function handleSshCommand(args: string[]) {
54
- const connectionDetails = parseSshCommand(args);
55
- if (!connectionDetails) {
56
- console.error(chalk.red('Could not parse SSH command.'));
57
- console.log('Expected format: ssh -i <key> user@host -p <port>');
58
- return;
59
- }
60
-
61
- const { user, host, key_path, port } = connectionDetails;
62
-
63
- const connections = getConnections();
64
- const existingConnection = connections.find(c => {
65
- const dbPort = c.port ? String(c.port) : undefined;
66
- const dbKeyPath = c.key_path || undefined;
67
- return c.user === user && c.host === host && dbKeyPath === key_path && dbPort === port;
68
- });
69
-
70
- if (existingConnection) {
71
- console.log(chalk.green(`Found existing connection with alias "${existingConnection.alias}". Connecting...`));
72
- runSSH(existingConnection.alias, user, host, key_path, port);
73
- return;
74
- }
75
-
76
- console.log('This looks like a new connection.');
77
- const { alias } = await inquirer.prompt([
78
- {
79
- type: 'input',
80
- name: 'alias',
81
- message: 'Enter an alias to save this connection for future use:',
82
- validate: input => (input ? true : 'Alias cannot be empty.'),
83
- },
84
- ]);
85
-
86
- try {
87
- addConnection(alias, user, host, key_path, port ? String(port) : undefined);
88
- console.log(chalk.green(`Connection saved as "${alias}". Connecting...`));
89
- runSSH(alias, user, host, key_path, port);
90
- } catch (err: any) {
91
- console.error(chalk.red(err.message));
92
- }
93
- }