zx
zx copied to clipboard
Feature request: support secure shell
I love to use this to unify scripts that otherwise would end up being complex bash scripts.
It would be great if the lib was not limited to child_processes but could run commands via SSH on remote machines. This way I could get rid of automation tools like Ansible in some scenarios. Let me know if this sounds reasonable and valuable.
This is what I was thinking of too. Maybe as a separate project.
A hack to get this is to create an executable bash script with this content:
#!/bin/sh
ssh -t -q $SSH_USER@$SSH_HOST "$2"
This requires a non interactive authentication method (I tried with public key auth). In the JavaScript one can set the script as shell and provide parameters to it:
process.env.SSH_USER = 'USER'
process.env.SSH_HOST = 'HOSTNAME'
$.shell = '/path/to/script.sh'
One limitation is the $.cwd
won't work, because it sets the cwd for the locally spawned child process and has no chance to manipulate the remotely invoked shell. A workaround to this is to extend $.prefix
$.prefix += 'cd /path/to/work/in;'
I love to use this to unify scripts that otherwise would end up being complex bash scripts.
It would be great if the lib was not limited to child_processes but could run commands via SSH on remote machines. This way I could get rid of automation tools like Ansible in some scenarios. Let me know if this sounds reasonable and valuable.
I had a somewhat similar idea, being able to run zx in a Docker container and execute the commands on the host machine (or some remote instance). Goal would be to not even have to have Node.js / zx installed [though Docker would need to be], and still run the scripts outside of the container.
That way, if anything funky needed to be set up to make it work, or give it some extra capability, that could be bottled up within the container itself, and the potentially interactive output be handled on the host / some other machine.
I was thinking about sending commands by ssh.
I imagine an API like this:
const exampleConfig = {
user: 'fred',
identityFile: '/path/to/id_rsa',
hostName: 'example.com',
passphrase: 'secret'
};
await ssh(exampleConfig, $ => {
await $`ls -la`;
});
This would preserve the feel of zx but using ssh rather than child_process as the execution mechanism
I hacked something together which basically works for my case: https://gist.github.com/joewalker/9cb2529325009b89d4935c42f49d3f74
I also was thinking about something like this. One thing I’m wondering it what about cd() function. Should it be working too?
@joewalker did you saw https://www.npmjs.com/package/ssh2-promise, it's looks like you looking for.
I also was thinking about something like this. One thing I’m wondering it what about cd() function. Should it be working too?
The gist I posted doesn't support cd()
, but ideally it should. Zx assumes that it's the only command processor, so I guess to support the case where ssh adds others, you'd need to do something like:
await ssh(exampleConfig, { $, cd } => {
cd('/')
await $`ls -la`
});
@joewalker did you saw https://www.npmjs.com/package/ssh2-promise, it's looks like you looking for.
I hadn't seen that thanks. The idea was to have something like zx + ssh which isn't exactly ssh2-promise, but ssh2-promise is the closest published thing that works now.
Here's my quick hack:
import { NodeSSH } from 'node-ssh';
const ssh = async (config, job) => {
const conn = new NodeSSH();
await conn.connect(config);
let cwd = undefined;
const cd = (newCwd) => cwd = newCwd;
const $ = async (pieces, ...args) => {
const { verbose = true } = $;
let cmd = '';
pieces.forEach((p, i) => {
cmd += `${p}${i === pieces.length - 1 ? '' : args[i]}`;
});
if (verbose) {
console.log('$', cmd);
}
const output = await conn.execCommand(cmd, {
cwd,
onStdout: (b) => process.stdout.write(b),
onStderr: (b) => process.stderr.write(b),
});
if (output.code) {
throw new Error(`exit code: ${output.code}`);
}
return output;
}
await job({ $, cd });
await conn.dispose();
}
await ssh({
host: '...',
username: '...',
privateKey: `${os.homedir()}/.ssh/id_rsa`
}, async ({ $, cd }) => {
await $`pwd`;
cd('/etc');
await $`pwd`;
await $`echo stderr > /dev/stderr`;
});
sounds like zx is reinventing the python tool fabric
Alternate version from the one by @r0binary which more or less does the same thing but more explicitly, and supports any case where zx splits arguments instead of passing them all as one big arg to bash -c
(I'm not sure if there are any cases like this):
#!/usr/bin/env bash
shift # Get rid of the -c that zx prefixes every command with
exec ssh -T ${SSH_ARGS} <<< "$@" # Note: intentional word splitting on SSH_ARGS
use it like:
import { $ } from "zx";
$.shell = "/path/to/ssh_shell.sh";
process.env.SSH_ARGS = "-v [email protected]";
await $`cd /some/directory/on/the/remote && ls -la | grep js`;
completed in 7.2.0
Great news, thanks. So can we close the ticket then?
For those curious for more info, here's the diff of 7.1.1..7.2.0
with the changes, in particular, a new dependency on webpod
as shown in the newly updated readme:
Executing commands on remote hosts
The
zx
uses webpod to execute commands on remote hosts.
import { ssh } from 'zx' await ssh('user@host')`echo Hello, world!`
It looks like webpod
is a new library from @antonmedv who is in this very thread, so thanks for that! 😸
For now it is not possible to enable inherit mode for ssh stdio, so migrating to this new feature would reduce scripts' transparency.
Why do you need inherited stdio for remote shell?
@antonmedv I have a multi-stage build script which is part-local and part-remote.
Remote parts are essentially ssh -T $remote <<HEREDOC
. So they are like local subcommands in inherit mode; and I can see the output (progress and errors) immediately.
Ok, got it: for real-time output. Will add this.
No interactive inputs?
@antonmedv not sure, I'm not using this, but maybe someone is. I think people would try to blindly port trailing .stdio()
option from their local zx statements. (that was my first try). If so, it may be worth to port this syntax 1-to-1 for ssh, so interactive inputs will be supported as well.
Is it possible to use cd
, fs
and all other goodies in ssh when using webpod?
For cd() not sure, as it will be unclear what will be changed.
Maybe via this API:
const $ = ssh('host')
$.cd('./path')
Same for fs. $.fs.readFile()
?
@antonmedv I understand that the host machine would need to also have (node and) zx installed, right?
I first thought of an API like:
ssh("user@host").within((target)=>{
target.fs.readFile()
target.cd('./path')
})
But that there is no need to use within, as there is no interference here.
So yes, I much prefer your API.
No, no need to install zx and node on the host.
@antonmedv cool. There are tools like https://github.com/adaltas/node-ssh2-fs that enable a similar experience. Curious about your plan to implement it 👀 ?
P.S: I'm very much interested in contributing this feature, as it's very painful to actually work around it: have to use shell syntax [ -d "folder" ] ||
, and stdout isn't handled+ it's a pain to navigate folders without whithin
...
Hello. Is it possible to set stdio = inherit for ssh yet?
Hello!
An ability to create interactive console on request could be really useful. I wanted to create the script that would send signal to the nodejs process on the remote host, open ssh tunnel and connect to the interactive nodejs debugger.
It would be nice to have stdio for ssh. I think I will revert my script to ssh -T heredoc
for the moment.