-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
187 lines (175 loc) · 5.05 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* Splits a command string into arguments, respecting quoted substrings.
* Supports double quotes ("") and single quotes ('').
* @param cmd - The command string to split.
* @returns An array of command and arguments.
*/
function splitArgs(cmd: string): string[] {
const args: string[] = []
let current = ''
let inDoubleQuotes = false
let inSingleQuotes = false
let escaped = false
for (let i = 0; i < cmd.length; i++) {
const c = cmd[i]
if (escaped) {
current += c
escaped = false
} else if (c === '\\') {
escaped = true
} else if (c === '"' && !inSingleQuotes) {
inDoubleQuotes = !inDoubleQuotes
} else if (c === "'" && !inDoubleQuotes) {
inSingleQuotes = !inSingleQuotes
} else if (c === ' ' && !inDoubleQuotes && !inSingleQuotes) {
if (current.length > 0) {
args.push(current)
current = ''
}
} else {
current += c
}
}
if (current.length > 0) {
args.push(current)
}
return args
}
/**
* Interface representing the result of an execution.
*/
export interface ExecResult {
/**
* Indicates whether the command executed successfully.
*/
success: boolean
/**
* The output message or error message from the command execution.
*/
message: string
}
/**
* Spawns a child process synchronously, inheriting the parent's stdio streams.
* @param cmd - The command to execute.
* @param cwd - The working directory for the command.
* @returns A Promise resolving to an ExecResult containing success status and message.
*/
export const spawnSync = async (
cmd: string,
cwd?: string,
): Promise<ExecResult> => {
try {
const argsArray = splitArgs(cmd)
const firstCmd = argsArray[0]
const args = argsArray.slice(1)
const command = new Deno.Command(firstCmd, {
args,
cwd,
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
})
const child = command.spawn()
// Wait for the process to exit
const status = await child.status
if (status.success) {
return {
success: true,
message: 'Process completed',
}
} else {
return {
success: false,
message: `Process failed with code ${status.code}`,
}
}
} catch (error) {
return handleError(error)
}
}
/**
* Executes a command and collects its output.
* @param cmd - The command to execute.
* @param cwd - The working directory for the command.
* @returns A Promise resolving to an ExecResult containing success status and output message.
*/
export const exec = async (cmd: string, cwd?: string): Promise<ExecResult> => {
try {
const argsArray = splitArgs(cmd)
const firstCmd = argsArray[0]
const args = argsArray.slice(1)
const command = new Deno.Command(firstCmd, {
args,
cwd,
})
// Create a subprocess and collect its output
const { code, stdout, stderr } = await command.output()
if (code !== 0) {
const errorOutput = new TextDecoder().decode(stderr).trim()
return {
success: false,
message: errorOutput,
}
}
const output = new TextDecoder().decode(stdout).trim()
return {
success: true,
message: output,
}
} catch (error) {
return handleError(error)
}
}
/**
* Handles errors by logging them and returning an ExecResult.
* @param error - The error to handle.
* @returns An ExecResult with success status and error message.
*/
const handleError = (error: unknown): ExecResult => {
if (error instanceof Error) {
console.error(error)
return {
success: false,
message: error.message,
}
} else {
return {
success: false,
message: 'An unknown error occurred',
}
}
}
/**
* Executes an SSH command on a remote server synchronously.
*
* This function constructs and executes an SSH command using a specified RSA key,
* user credentials, and host address. It navigates to the specified working directory (cwd),
* sources the user profile, and executes the provided command.
*
* @param {string} user - The SSH username to use for connection.
* @param {string} host - The hostname or IP address of the remote server.
* @param {string} rsaKeyPath - The path to the RSA private key for authentication.
* @param {string} execCmd - The command to execute on the remote server.
* @param {string} [cwd='~'] - The working directory on the remote server. Defaults to the home directory.
* @returns {Promise<ExecResult>} - A Promise that resolves to an object containing the success status and message.
*
* @example
* // Executes 'solana --version' command on a remote server
* const user = 'solv'
* const host = '145.40.126.169'
* const rsaKey = '~/.ssh/id_rsa'
* const execCmd = `solana --version`
*
* await sshCmd(user, host, rsaKey, execCmd)
*/
export const sshCmd = async (
user: string,
host: string,
rsaKeyPath: string,
execCmd: string,
cwd = '~',
): Promise<ExecResult> => {
const cmd =
`ssh -i ${rsaKeyPath} -o StrictHostKeyChecking=no ${user}@${host} -p 22 'cd ${cwd} && source ~/.profile && ${execCmd}'`
return await spawnSync(cmd)
}