Skip to content

Commit 0187579

Browse files
authored
Feat: Adds auto-command execution to SSH connections (#18)
* Feat: Adds auto command input feature for hosts Allows users to configure auto-run commands after connecting to a host. Prompts the user to set up auto commands during host creation. Supports multi-line command input via an editor. Displays the list of auto commands in the host information output. Specifies that commands should be entered one per line. * feat(cli): Display auto command count in host list * feat(cli): Add autoCommands management to host edit command * feat(connect): Add support for automatic command execution on SSH * feat(i18n): Add Korean translation for no saved hosts message * feat(connect): Implement auto-command execution and enhance SSH connection flow * Refactors host display and connection logic Improves code reusability and readability by extracting host display logic into a separate module. Standardizes host selection across commands using a shared function. Enhances the connection command to handle auto-run commands more effectively, combining them with an interactive bash session. Updates translations and prompts to English
1 parent f17e00c commit 0187579

File tree

9 files changed

+144
-66
lines changed

9 files changed

+144
-66
lines changed

packages/cli/src/commands/add.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import inquirer from 'inquirer';
22
import chalk from 'chalk';
33
import { existsSync } from 'fs';
44
import { addHost, loadConfig } from '../utils/config.js';
5+
import { displayHostInfo } from '../utils/display.js';
56
import type { SSHHost } from '../types/ssh.js';
67

78
export async function addCommand() {
@@ -114,15 +115,43 @@ export async function addCommand() {
114115
.filter(tag => tag.length > 0);
115116
},
116117
},
118+
{
119+
type: 'confirm',
120+
name: 'hasAutoCommands',
121+
message: 'Do you want to configure auto-run commands after connection?',
122+
default: false,
123+
},
124+
{
125+
type: 'editor',
126+
name: 'autoCommandsInput',
127+
message: 'Enter commands to run automatically (one per line):',
128+
when: answers => answers.hasAutoCommands,
129+
validate: (input: string) => {
130+
if (!input || !input.trim()) {
131+
return 'Please enter at least one command.';
132+
}
133+
return true;
134+
},
135+
},
117136
]);
118137

138+
// Process auto commands
139+
let autoCommands: string[] | undefined = undefined;
140+
if (answers.hasAutoCommands && answers.autoCommandsInput) {
141+
autoCommands = answers.autoCommandsInput
142+
.split('\n')
143+
.map((cmd: string) => cmd.trim())
144+
.filter((cmd: string) => cmd.length > 0);
145+
}
146+
119147
const newHost: SSHHost = {
120148
name: answers.name,
121149
host: answers.host,
122150
user: answers.user,
123151
port: answers.port,
124152
keyPath: answers.authMethod === 'key' ? answers.keyPath : undefined,
125153
usePassword: answers.authMethod === 'password',
154+
autoCommands: autoCommands,
126155
description: answers.description,
127156
tags: answers.tags,
128157
};
@@ -132,21 +161,7 @@ export async function addCommand() {
132161

133162
console.log();
134163
console.log(chalk.green('✅ Host added successfully!'));
135-
console.log();
136-
console.log(chalk.blue('📋 Host information:'));
137-
console.log(` ${chalk.cyan('Name:')} ${newHost.name}`);
138-
console.log(` ${chalk.cyan('Address:')} ${newHost.user}@${newHost.host}:${newHost.port}`);
139-
if (newHost.keyPath) {
140-
console.log(` ${chalk.cyan('Key file:')} ${newHost.keyPath}`);
141-
}
142-
if (newHost.description) {
143-
console.log(` ${chalk.cyan('Description:')} ${newHost.description}`);
144-
}
145-
if (newHost.tags && newHost.tags.length > 0) {
146-
console.log(` ${chalk.cyan('Tags:')} ${newHost.tags.join(', ')}`);
147-
}
148-
console.log();
149-
console.log(chalk.blue('💡 To connect:'), chalk.gray(`simple-ssh connect ${newHost.name}`));
164+
displayHostInfo(newHost);
150165
} catch (error) {
151166
console.log(chalk.red('❌ Error adding host:'), error);
152167
}

packages/cli/src/commands/connect.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { spawn } from 'child_process';
22
import inquirer from 'inquirer';
33
import chalk from 'chalk';
4-
import ora from 'ora';
54
import { loadConfig, getHost } from '../utils/config.js';
5+
import { createHostChoices } from '../utils/display.js';
66
import type { ConnectionOptions } from '../types/ssh.js';
77

88
export async function connectCommand(hostName?: string, options: ConnectionOptions = {}) {
@@ -30,10 +30,7 @@ export async function connectCommand(hostName?: string, options: ConnectionOptio
3030
type: 'list',
3131
name: 'selectedHost',
3232
message: 'Select a host to connect:',
33-
choices: config.hosts.map(host => ({
34-
name: `${chalk.cyan(host.name)} - ${host.user}@${host.host}:${host.port}${host.description ? ` (${host.description})` : ''}`,
35-
value: host.name,
36-
})),
33+
choices: createHostChoices(config.hosts),
3734
},
3835
]);
3936

@@ -52,23 +49,37 @@ export async function connectCommand(hostName?: string, options: ConnectionOptio
5249
console.log(chalk.blue(`🔗 Connecting to ${targetHost.name}...`));
5350
console.log(chalk.gray(` ${user}@${targetHost.host}:${port}`));
5451

55-
const spinner = ora('Attempting SSH connection...').start();
56-
5752
// Build SSH command
58-
const sshArgs = ['-p', port.toString(), `${user}@${targetHost.host}`];
53+
const sshArgs = ['-p', port.toString()];
5954

6055
// Add key file if specified
6156
if (targetHost.keyPath) {
6257
sshArgs.unshift('-i', targetHost.keyPath);
6358
}
6459

60+
// Check for auto commands
61+
const hasAutoCommands = targetHost.autoCommands && targetHost.autoCommands.length > 0;
62+
63+
if (hasAutoCommands) {
64+
console.log(chalk.blue(`🤖 Auto commands configured (${targetHost.autoCommands!.length} commands):`));
65+
targetHost.autoCommands!.forEach((cmd, index) => {
66+
console.log(` ${chalk.dim(`${index + 1}.`)} ${chalk.yellow(cmd)}`);
67+
});
68+
console.log();
69+
70+
// Combine auto commands and add bash for interactive session
71+
const commandString = targetHost.autoCommands!.join(' && ') + '; bash';
72+
sshArgs.push(`${user}@${targetHost.host}`, commandString);
73+
} else {
74+
// Regular interactive connection
75+
sshArgs.push(`${user}@${targetHost.host}`);
76+
}
77+
6578
// Show password prompt info
6679
if (targetHost.usePassword) {
6780
console.log(chalk.yellow('💡 This host uses password authentication. Please enter password when prompted.'));
6881
}
6982

70-
spinner.stop();
71-
7283
console.log(chalk.green(`✅ Starting SSH connection...`));
7384
console.log(chalk.gray(`Command: ssh ${sshArgs.join(' ')}`));
7485
console.log();

packages/cli/src/commands/edit.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import inquirer from 'inquirer';
22
import chalk from 'chalk';
33
import { existsSync } from 'fs';
44
import { loadConfig, addHost, getHost } from '../utils/config.js';
5+
import { displayHostInfo, createHostChoices } from '../utils/display.js';
56
import type { SSHHost } from '../types/ssh.js';
67

78
export async function editCommand(hostName?: string) {
@@ -22,10 +23,7 @@ export async function editCommand(hostName?: string) {
2223
name: 'selectedHost',
2324
message: 'Select host to edit:',
2425
choices: [
25-
...config.hosts.map(host => ({
26-
name: `${chalk.cyan(host.name)} - ${host.user}@${host.host}:${host.port}`,
27-
value: host.name,
28-
})),
26+
...createHostChoices(config.hosts),
2927
new inquirer.Separator(),
3028
{
3129
name: chalk.gray('Cancel'),
@@ -173,15 +171,38 @@ export async function editCommand(hostName?: string) {
173171
.filter(tag => tag.length > 0);
174172
},
175173
},
174+
{
175+
type: 'confirm',
176+
name: 'hasAutoCommands',
177+
message: 'Do you want to modify auto-run commands?',
178+
default: !!(targetHost.autoCommands && targetHost.autoCommands.length > 0),
179+
},
180+
{
181+
type: 'editor',
182+
name: 'autoCommandsInput',
183+
message: 'Enter commands to run automatically (one per line):',
184+
default: targetHost.autoCommands ? targetHost.autoCommands.join('\n') : '',
185+
when: answers => answers.hasAutoCommands,
186+
},
176187
]);
177188

189+
// Process auto commands
190+
let autoCommands: string[] | undefined = undefined;
191+
if (answers.hasAutoCommands && answers.autoCommandsInput) {
192+
autoCommands = answers.autoCommandsInput
193+
.split('\n')
194+
.map((cmd: string) => cmd.trim())
195+
.filter((cmd: string) => cmd.length > 0);
196+
}
197+
178198
const updatedHost: SSHHost = {
179199
name: answers.name,
180200
host: answers.host,
181201
user: answers.user,
182202
port: answers.port,
183203
keyPath: answers.authMethod === 'key' ? answers.keyPath : undefined,
184204
usePassword: answers.authMethod === 'password',
205+
autoCommands: autoCommands,
185206
description: answers.description,
186207
tags: answers.tags,
187208
};
@@ -190,21 +211,7 @@ export async function editCommand(hostName?: string) {
190211
await addHost(updatedHost);
191212

192213
console.log(chalk.green('✅ Host updated successfully!'));
193-
console.log();
194-
console.log(chalk.blue('📋 Updated host information:'));
195-
console.log(` ${chalk.cyan('Name:')} ${updatedHost.name}`);
196-
console.log(` ${chalk.cyan('Address:')} ${updatedHost.user}@${updatedHost.host}:${updatedHost.port}`);
197-
if (updatedHost.keyPath) {
198-
console.log(` ${chalk.cyan('Key file:')} ${updatedHost.keyPath}`);
199-
}
200-
if (updatedHost.description) {
201-
console.log(` ${chalk.cyan('Description:')} ${updatedHost.description}`);
202-
}
203-
if (updatedHost.tags && updatedHost.tags.length > 0) {
204-
console.log(` ${chalk.cyan('Tags:')} ${updatedHost.tags.join(', ')}`);
205-
}
206-
console.log();
207-
console.log(chalk.blue('💡 To connect:'), chalk.gray(`simple-ssh connect ${updatedHost.name}`));
214+
displayHostInfo(updatedHost, 'Updated host information');
208215
} catch (error) {
209216
console.log(chalk.red('❌ Error updating host:'), error);
210217
}

packages/cli/src/commands/list.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export async function listCommand() {
2828
} else {
2929
console.log(` ${chalk.dim('🔧 Auth:')} ${chalk.gray('Default SSH settings')}`);
3030
}
31+
if (host.autoCommands && host.autoCommands.length > 0) {
32+
console.log(` ${chalk.dim('🤖 Auto commands:')} ${chalk.magenta(`${host.autoCommands.length} command(s)`)}`);
33+
}
3134
});
3235

3336
console.log();

packages/cli/src/commands/remove.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inquirer from 'inquirer';
22
import chalk from 'chalk';
33
import { loadConfig, removeHost, getHost } from '../utils/config.js';
4+
import { createHostChoices } from '../utils/display.js';
45

56
export async function removeCommand(hostName?: string) {
67
const config = await loadConfig();
@@ -20,10 +21,7 @@ export async function removeCommand(hostName?: string) {
2021
name: 'selectedHost',
2122
message: 'Select host to remove:',
2223
choices: [
23-
...config.hosts.map(host => ({
24-
name: `${chalk.cyan(host.name)} - ${host.user}@${host.host}:${host.port}`,
25-
value: host.name,
26-
})),
24+
...createHostChoices(config.hosts),
2725
new inquirer.Separator(),
2826
{
2927
name: chalk.gray('Cancel'),
@@ -66,25 +64,25 @@ export async function removeCommand(hostName?: string) {
6664
{
6765
type: 'confirm',
6866
name: 'confirmed',
69-
message: '정말로 삭제하시겠습니까?',
67+
message: 'Are you sure you want to delete this host?',
7068
default: false,
7169
},
7270
]);
7371

7472
if (!confirmed) {
75-
console.log(chalk.blue('취소되었습니다.'));
73+
console.log(chalk.blue('Cancelled.'));
7674
return;
7775
}
7876

7977
try {
8078
const success = await removeHost(targetHostName);
8179

8280
if (success) {
83-
console.log(chalk.green(`✅ 호스트 '${targetHostName}'이 성공적으로 삭제되었습니다.`));
81+
console.log(chalk.green(`✅ Host '${targetHostName}' has been successfully removed.`));
8482
} else {
85-
console.log(chalk.red(`❌ 호스트 '${targetHostName}' 삭제에 실패했습니다.`));
83+
console.log(chalk.red(`❌ Failed to remove host '${targetHostName}'.`));
8684
}
8785
} catch (error) {
88-
console.log(chalk.red('❌ 호스트 삭제 중 오류가 발생했습니다:'), error);
86+
console.log(chalk.red('❌ Error occurred while removing host:'), error);
8987
}
9088
}

packages/cli/src/types/ssh.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export interface SSHHost {
44
user: string;
55
port: number;
66
keyPath?: string;
7-
usePassword?: boolean; // 비밀번호 사용 여부 (연결 시 입력받음)
8-
autoCommands?: string[]; // 접속 후 자동 실행할 명령어들
7+
usePassword?: boolean; // Whether to use password (prompted on connection)
8+
autoCommands?: string[]; // Commands to run automatically after connection
99
description?: string;
1010
tags?: string[];
1111
}

packages/cli/src/utils/config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from 'path';
44
import { homedir } from 'os';
55
import type { SSHConfig, SSHHost } from '../types/ssh.js';
66

7-
// 테스트 환경에서 HOME 환경변수를 우선 사용
7+
// Use HOME environment variable first for test environments
88
function getHomeDir(): string {
99
return process.env.HOME || process.env.USERPROFILE || homedir();
1010
}
@@ -43,14 +43,14 @@ export async function loadConfig(): Promise<SSHConfig> {
4343
const content = await readFile(configFile, 'utf-8');
4444
const config = JSON.parse(content) as SSHConfig;
4545

46-
// 기본값 병합
46+
// Merge default values
4747
return {
4848
...DEFAULT_CONFIG,
4949
...config,
5050
hosts: config.hosts || [],
5151
};
5252
} catch (error) {
53-
console.error('설정 파일을 읽는 중 오류가 발생했습니다:', error);
53+
console.error('Error reading configuration file:', error);
5454
return DEFAULT_CONFIG;
5555
}
5656
}
@@ -61,15 +61,15 @@ export async function saveConfig(config: SSHConfig): Promise<void> {
6161
try {
6262
await writeFile(getConfigFile(), JSON.stringify(config, null, 2), 'utf-8');
6363
} catch (error) {
64-
console.error('설정 파일을 저장하는 중 오류가 발생했습니다:', error);
64+
console.error('Error saving configuration file:', error);
6565
throw error;
6666
}
6767
}
6868

6969
export async function addHost(host: SSHHost): Promise<void> {
7070
const config = await loadConfig();
7171

72-
// 중복 이름 체크
72+
// Check for duplicate names
7373
const existingIndex = config.hosts.findIndex(h => h.name === host.name);
7474
if (existingIndex >= 0) {
7575
config.hosts[existingIndex] = host;

packages/cli/src/utils/display.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import chalk from 'chalk';
2+
import type { SSHHost } from '../types/ssh.js';
3+
4+
/**
5+
* Display host information in a formatted way
6+
*/
7+
export function displayHostInfo(host: SSHHost, title = 'Host information'): void {
8+
console.log();
9+
console.log(chalk.blue(`📋 ${title}:`));
10+
console.log(` ${chalk.cyan('Name:')} ${host.name}`);
11+
console.log(` ${chalk.cyan('Address:')} ${host.user}@${host.host}:${host.port}`);
12+
13+
if (host.keyPath) {
14+
console.log(` ${chalk.cyan('Key file:')} ${host.keyPath}`);
15+
}
16+
17+
if (host.description) {
18+
console.log(` ${chalk.cyan('Description:')} ${host.description}`);
19+
}
20+
21+
if (host.tags && host.tags.length > 0) {
22+
console.log(` ${chalk.cyan('Tags:')} ${host.tags.join(', ')}`);
23+
}
24+
25+
if (host.autoCommands && host.autoCommands.length > 0) {
26+
console.log(` ${chalk.cyan('Auto commands:')} ${host.autoCommands.length} command(s)`);
27+
host.autoCommands.forEach((cmd, index) => {
28+
console.log(` ${chalk.dim(`${index + 1}.`)} ${chalk.yellow(cmd)}`);
29+
});
30+
}
31+
32+
console.log();
33+
console.log(chalk.blue('💡 To connect:'), chalk.gray(`simple-ssh connect ${host.name}`));
34+
}
35+
36+
/**
37+
* Create host selection choices for inquirer
38+
*/
39+
export function createHostChoices(hosts: SSHHost[]) {
40+
return hosts.map(host => ({
41+
name: `${chalk.cyan(host.name)} - ${host.user}@${host.host}:${host.port}${host.description ? ` (${host.description})` : ''}`,
42+
value: host.name,
43+
}));
44+
}

0 commit comments

Comments
 (0)