ssh-client-wrapper 2.11.0 → 2.13.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.
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - uses: actions/checkout@v3
16
- - uses: actions/download-artifact@v3
16
+ - uses: actions/download-artifact@v4
17
17
  with:
18
18
  name: code-coverage-report
19
19
  - uses: paambaati/codeclimate-action@v5.0.0
@@ -0,0 +1,29 @@
1
+ name: Release and Publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ test:
10
+ uses: ./.github/workflows/run_test.yml
11
+
12
+ release:
13
+ needs: test
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write
17
+ issues: write
18
+ pull-requests: write
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 20
24
+ - run: npm ci
25
+ - name: Semantic Release
26
+ uses: cycjimmy/semantic-release-action@v4
27
+ env:
28
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,11 +1,8 @@
1
1
  name: run UT
2
2
  on:
3
- push:
4
- tags-ignore:
5
- - 'v*'
3
+ pull_request:
6
4
  branches:
7
- - '*'
8
- - '!main'
5
+ - main
9
6
  workflow_call:
10
7
  concurrency:
11
8
  group: ${{ github.workflow }}-${{ github.ref }}
@@ -21,7 +18,6 @@ jobs:
21
18
  TEST_USER: testuser
22
19
  TEST_PW: passw0rd
23
20
  TEST_PORT: 10022
24
- DEBUG: 'ssh*'
25
21
  runs-on: ubuntu-latest
26
22
  strategy:
27
23
  matrix:
@@ -35,7 +31,9 @@ jobs:
35
31
  - run: npm ci
36
32
  - run: npm run test
37
33
  - run: npm run coverage
38
- - uses: actions/upload-artifact@v3
34
+ if: matrix.node-version == '20.x'
35
+ - uses: actions/upload-artifact@v4
36
+ if: matrix.node-version == '20.x'
39
37
  with:
40
38
  name: code-coverage-report
41
39
  path: coverage-report.lcov
@@ -0,0 +1 @@
1
+ npx lint-staged
package/.husky/pre-commit CHANGED
@@ -1,4 +1 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
1
  npx lint-staged
@@ -0,0 +1,16 @@
1
+ {
2
+ "branches": ["main"],
3
+ "plugins": [
4
+ "@semantic-release/commit-analyzer",
5
+ "@semantic-release/release-notes-generator",
6
+ "@semantic-release/changelog",
7
+ ["@semantic-release/npm", {
8
+ "npmPublish": true
9
+ }],
10
+ ["@semantic-release/git", {
11
+ "assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
12
+ "message": "chore(release): ${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}"
13
+ }],
14
+ "@semantic-release/github"
15
+ ]
16
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # [2.13.0](https://github.com/so5/ssh-client-wrapper/compare/v2.12.4...v2.13.0) (2025-08-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * upgrade debug from 4.4.0 to 4.4.1 ([35f966a](https://github.com/so5/ssh-client-wrapper/commit/35f966a7212d6bb9677964688bb94ae06f869ada))
7
+
8
+
9
+ ### Features
10
+
11
+ * Improve DX and automate releases ([805f2ea](https://github.com/so5/ssh-client-wrapper/commit/805f2ea7756f6ad61238a4a21d86575a17e748d9))
12
+ * Rewrite in ESM ([c5a95c1](https://github.com/so5/ssh-client-wrapper/commit/c5a95c18401e7b373249d3707609f2a9576f7400))
13
+ * Rewrite in ESM ([71ed6f5](https://github.com/so5/ssh-client-wrapper/commit/71ed6f56af43cac1471cbf4a4ed3016f7e55ab4d))
package/README.md CHANGED
@@ -4,3 +4,102 @@
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/8f3c0ea00e755ae31081/maintainability)](https://codeclimate.com/github/so5/ssh-client-wrapper/maintainability)
5
5
  # ssh-client-wrapper
6
6
  open ssh client wrapper library for nodejs
7
+
8
+ ## Installation
9
+ ```bash
10
+ npm install ssh-client-wrapper
11
+ ```
12
+
13
+ ## Usage
14
+ Here is a simple example of how to use `ssh-client-wrapper` to execute a command on a remote server.
15
+
16
+ ```javascript
17
+ import SshClientWrapper from 'ssh-client-wrapper';
18
+
19
+ const hostInfo = {
20
+ host: 'remote.server.com',
21
+ port: 22,
22
+ user: 'username',
23
+ password: 'password'
24
+ };
25
+
26
+ const ssh = new SshClientWrapper(hostInfo);
27
+
28
+ async function run() {
29
+ try {
30
+ await ssh.canConnect();
31
+ console.log('Connection successful!');
32
+
33
+ const { output, rt } = await ssh.execAndGetOutput('ls -l /home');
34
+ if (rt === 0) {
35
+ console.log('Directory listing:');
36
+ output.forEach(line => console.log(line));
37
+ } else {
38
+ console.error('Error executing command');
39
+ }
40
+ } catch (err) {
41
+ console.error('Connection or command failed:', err);
42
+ } finally {
43
+ ssh.disconnect();
44
+ }
45
+ }
46
+
47
+ run();
48
+ ```
49
+
50
+ ## API Guide
51
+
52
+ ### `new SshClientWrapper(hostInfo)`
53
+ Creates a new SSH client instance.
54
+
55
+ * `hostInfo` (Object): Connection details for the remote host.
56
+ * `host` (string): The hostname or IP address of the server.
57
+ * `user` (string): The username for authentication.
58
+ * `port` (number, optional): The port number. Defaults to 22.
59
+ * `password` (string | Function, optional): The password for authentication, or a function that returns the password.
60
+ * `keyFile` (string, optional): The path to the private key file for key-based authentication.
61
+ * `passphrase` (string | Function, optional): The passphrase for the private key, or a function that returns it.
62
+ * `noStrictHostKeyChecking` (boolean, optional): If `true`, bypasses strict host key checking.
63
+ * And more... see `lib/index.js` for all available options.
64
+
65
+ ### `.exec(cmd, [timeout], [outputCallback], [rcfile], [prependCmd])`
66
+ Executes a command on the remote host.
67
+
68
+ * Returns: `Promise<number>` - The return code of the command.
69
+
70
+ ### `.execAndGetOutput(cmd, [timeout], [rcfile], [prependCmd])`
71
+ Executes a command and returns its standard output.
72
+
73
+ * Returns: `Promise<{output: string[], rt: number}>` - An object containing the output as an array of strings and the return code.
74
+
75
+ ### `.ls(target, [lsOpt], [timeout])`
76
+ Executes the `ls` command on the remote host.
77
+
78
+ * Returns: `Promise<string[]>` - The output of the `ls` command as an array of strings.
79
+
80
+ ### `.expect(cmd, expects, [timeout])`
81
+ Executes a command and interacts with it, similar to the `expect` tool.
82
+
83
+ * Returns: `Promise<number>` - The return code of the command.
84
+
85
+ ### `.send(src, dst, [opt], [timeout])`
86
+ Uploads files or directories to the remote host using `rsync`.
87
+
88
+ * `src` (string[]): An array of local paths to send.
89
+ * `dst` (string): The remote destination path.
90
+ * Returns: `Promise<void>`
91
+
92
+ ### `.recv(src, dst, [opt], [timeout])`
93
+ Downloads files or directories from the remote host using `rsync`.
94
+
95
+ * `src` (string[]): An array of remote paths to receive.
96
+ * `dst` (string): The local destination path.
97
+ * Returns: `Promise<void>`
98
+
99
+ ### `.canConnect([timeout])`
100
+ Checks if a connection to the remote host can be established.
101
+
102
+ * Returns: `Promise<boolean>` - Resolves with `true` on success.
103
+
104
+ ### `.disconnect()`
105
+ Closes the master SSH connection to the remote host.
@@ -0,0 +1,181 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import stylistic from "@stylistic/eslint-plugin";
4
+ import jsdoc from "eslint-plugin-jsdoc";
5
+ import eslintPluginChaiFriendly from "eslint-plugin-chai-friendly";
6
+ import eslintPluginChaiExpect from "eslint-plugin-chai-expect";
7
+
8
+ const styleRules = {
9
+ "arrow-body-style": ["error", "always"],
10
+ "@stylistic/arrow-spacing": [
11
+ "error",
12
+ {
13
+ after: false,
14
+ before: false
15
+ }],
16
+ "@stylistic/comma-dangle": [
17
+ "error",
18
+ "never"
19
+ ],
20
+ "@stylistic/spaced-comment": [
21
+ "error",
22
+ "never"
23
+ ],
24
+ "@stylistic/lines-around-comment": [
25
+ "error",
26
+ {
27
+ beforeBlockComment: true,
28
+ afterBlockComment: false,
29
+ beforeLineComment: false,
30
+ afterLineComment: false,
31
+ allowBlockStart: true
32
+ }
33
+ ],
34
+ "@stylistic/lines-between-class-members": [
35
+ "error",
36
+ "always",
37
+ {
38
+ exceptAfterSingleLine: true
39
+ }
40
+ ],
41
+ "@stylistic/newline-per-chained-call": [
42
+ "error",
43
+ {
44
+ ignoreChainWithDepth: 2
45
+ }
46
+ ],
47
+ "@stylistic/padded-blocks": [
48
+ "error",
49
+ "never"
50
+ ],
51
+ "@stylistic/brace-style": [
52
+ "error",
53
+ "1tbs",
54
+ {
55
+ allowSingleLine: true
56
+ }
57
+ ],
58
+ "@stylistic/padding-line-between-statements": [
59
+ "error",
60
+ {
61
+ blankLine: "any",
62
+ prev: [
63
+ "const",
64
+ "let",
65
+ "var"
66
+ ],
67
+ next: "*"
68
+ },
69
+ {
70
+ blankLine: "always",
71
+ prev: "*",
72
+ next: [
73
+ "class",
74
+ "do",
75
+ "for",
76
+ "function",
77
+ "switch",
78
+ "try",
79
+ "while"
80
+ ]
81
+ },
82
+ {
83
+ blankLine: "any",
84
+ prev: [
85
+ "const",
86
+ "let",
87
+ "var",
88
+ "for",
89
+ "while",
90
+ "do",
91
+ "block-like",
92
+ "multiline-block-like"
93
+ ],
94
+ next: [
95
+ "block-like",
96
+ "do",
97
+ "for",
98
+ "multiline-block-like",
99
+ "switch",
100
+ "try",
101
+ "while"
102
+ ]
103
+ }
104
+ ],
105
+ "@stylistic/quotes": [
106
+ "error",
107
+ "double"
108
+ ],
109
+ "@stylistic/semi": [
110
+ "error",
111
+ "always"
112
+ ]
113
+ };
114
+
115
+ const jsdocRules = {
116
+ "jsdoc/require-jsdoc": ["warn", { enableFixer: false }],
117
+ "jsdoc/require-param": ["warn", { enableFixer: false }],
118
+ "jsdoc/require-param-description": "warn",
119
+ "jsdoc/require-param-name": "warn",
120
+ "jsdoc/require-param-type": "warn",
121
+ "jsdoc/require-hyphen-before-param-description": [
122
+ "warn",
123
+ "always"
124
+ ],
125
+ "jsdoc/require-returns": "warn",
126
+ "jsdoc/require-returns-check": "error"
127
+ };
128
+
129
+ export default [
130
+ // Global configuration
131
+ {
132
+ ignores: ["coverage-report.lcov", "node_modules/"]
133
+ },
134
+ js.configs.recommended,
135
+ {
136
+ plugins: {
137
+ "@stylistic": stylistic
138
+ }
139
+ },
140
+ // Lib configuration
141
+ {
142
+ files: ["lib/**/*.js"],
143
+ plugins: {
144
+ jsdoc
145
+ },
146
+ languageOptions: {
147
+ ecmaVersion: "latest",
148
+ sourceType: "module",
149
+ globals: {
150
+ ...globals.es2021,
151
+ ...globals.node
152
+ }
153
+ },
154
+ rules: {
155
+ ...styleRules,
156
+ ...jsdocRules
157
+ }
158
+ },
159
+ // Test configuration
160
+ {
161
+ files: ["test/**/*.js"],
162
+ plugins: {
163
+ "chai-friendly": eslintPluginChaiFriendly,
164
+ "chai-expect": eslintPluginChaiExpect
165
+ },
166
+ languageOptions: {
167
+ ecmaVersion: "latest",
168
+ sourceType: "module",
169
+ globals: {
170
+ ...globals.es2021,
171
+ ...globals.node,
172
+ ...globals.mocha
173
+ }
174
+ },
175
+ rules: {
176
+ ...styleRules,
177
+ "no-unused-expressions": "off",
178
+ "chai-friendly/no-unused-expressions": "error"
179
+ }
180
+ }
181
+ ];
package/lib/fork.js CHANGED
@@ -1,18 +1,19 @@
1
- "use strict";
2
- const debug = require("debug")("sshClientWrapper:debug:fork");
3
- const debugVerbose = require("debug")("sshClientWrapper:verbose:fork");
4
- const debugSendPty = require("debug")("sshClientWrapperInsecure:fork_sendpty");
5
- const { spawn } = require("node-pty");
1
+ import Debug from "debug";
2
+ import { spawn } from "node-pty-prebuilt-multiarch";
6
3
 
7
- const { rePwPrompt, rePhPrompt, reNewHostPrompt, reRemoteHostIdentificationHasChanged, buff2String, sendPty, watchDogTimer } = require("./util.js");
4
+ import { rePwPrompt, rePhPrompt, reNewHostPrompt, reRemoteHostIdentificationHasChanged, buff2String, sendPty, watchDogTimer } from "./util.js";
5
+
6
+ const debug = Debug("sshClientWrapper:debug:fork");
7
+ const debugVerbose = Debug("sshClientWrapper:verbose:fork");
8
+ const debugSendPty = Debug("sshClientWrapper:insecure:fork");
8
9
 
9
10
  const nodePtyOpt = {
10
11
  shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
11
12
  windowsHide: true
12
13
  };
13
14
 
14
- function sshLoginCallback (output, ptyProcess, pw, ph, logger = debugSendPty) {
15
- return new Promise((resolve, reject) => {
15
+ export function sshLoginCallback(output, ptyProcess, pw, ph, logger = debugSendPty) {
16
+ return new Promise((resolve, reject)=>{
16
17
  if (reRemoteHostIdentificationHasChanged.test(output)) {
17
18
  debug("REMOTE HOST IDENTIFICATION HAS CHANGED");
18
19
  const rt = /known_hosts:(\d+)/.exec(output);
@@ -38,10 +39,10 @@ function sshLoginCallback (output, ptyProcess, pw, ph, logger = debugSendPty) {
38
39
  if (typeof pw === "function") {
39
40
  debug("call password callback function");
40
41
  pw()
41
- .then((v) => {
42
+ .then((v)=>{
42
43
  sendPty(ptyProcess, `${v}\n`, logger, 30);
43
44
  })
44
- .catch((e) => {
45
+ .catch((e)=>{
45
46
  reject(e);
46
47
  });
47
48
  resolve();
@@ -56,10 +57,10 @@ function sshLoginCallback (output, ptyProcess, pw, ph, logger = debugSendPty) {
56
57
  if (typeof ph === "function") {
57
58
  debug("call passphrase callback function");
58
59
  ph()
59
- .then((v) => {
60
+ .then((v)=>{
60
61
  sendPty(ptyProcess, `${v}\n`, logger, 45);
61
62
  })
62
- .catch((e) => {
63
+ .catch((e)=>{
63
64
  reject(e);
64
65
  });
65
66
  }
@@ -70,7 +71,7 @@ function sshLoginCallback (output, ptyProcess, pw, ph, logger = debugSendPty) {
70
71
  /**
71
72
  * create pty for master ssh session and store to hostInfo.masterPty
72
73
  */
73
- function createMasterPty (hostInfo) {
74
+ export function createMasterPty(hostInfo) {
74
75
  debug("fork pty for master connection");
75
76
 
76
77
  if (hostInfo.masterPty) {
@@ -81,13 +82,13 @@ function createMasterPty (hostInfo) {
81
82
  hostInfo.masterPty = ptyProcess;
82
83
  }
83
84
 
84
- async function fork (hostInfo, cmd, args, timeout, outputCallback, retryableExitCodes = []) {
85
- return new Promise((resolve, reject) => {
85
+ export async function fork(hostInfo, cmd, args, timeout, outputCallback, retryableExitCodes = [], expects = []) {
86
+ return new Promise((resolve, reject)=>{
86
87
  debug(`cmd=${cmd}: args=${args}`);
87
88
  const ptyProcess = spawn(cmd, args, nodePtyOpt);
88
89
  let ac = null;
89
90
  if (timeout > 0) {
90
- ac = watchDogTimer(timeout, cmd, () => {
91
+ ac = watchDogTimer(timeout, cmd, ()=>{
91
92
  ptyProcess.kill();
92
93
  const err = new Error(`watchdog timer expired ${timeout}`);
93
94
  err.code = "TIMEEXPIRE";
@@ -97,7 +98,7 @@ async function fork (hostInfo, cmd, args, timeout, outputCallback, retryableExit
97
98
  ptyProcess.onData(buff2String.bind(null, debugVerbose));
98
99
 
99
100
  if (hostInfo !== null) {
100
- ptyProcess.onData(async (data) => {
101
+ ptyProcess.onData((data)=>{
101
102
  const output = data.toString();
102
103
  sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase)
103
104
  .catch(reject);
@@ -108,13 +109,25 @@ async function fork (hostInfo, cmd, args, timeout, outputCallback, retryableExit
108
109
  err.retryable = true;
109
110
  reject(err);
110
111
  }
112
+
113
+ if (Array.isArray(expects) && expects.length > 0) {
114
+ const { reExpect, send, numKeep } = expects.shift();
115
+ if (reExpect.test(output)) {
116
+ sendPty(ptyProcess, `${send}\n`, debugSendPty, "userDefinedHandler").catch(reject);
117
+ if (numKeep > 0) {
118
+ expects.unshift({ reExpect, send, numKeep: numKeep - 1 });
119
+ }
120
+ } else {
121
+ expects.unshift({ reExpect, send, numKeep });
122
+ }
123
+ }
111
124
  });
112
125
  }
113
126
 
114
127
  if (typeof outputCallback === "function") {
115
128
  ptyProcess.onData(buff2String.bind(null, outputCallback));
116
129
  }
117
- ptyProcess.onExit(({ exitCode, signal }) => {
130
+ ptyProcess.onExit(({ exitCode, signal })=>{
118
131
  debug("onExit", exitCode, signal);
119
132
 
120
133
  if (ac !== null) {
@@ -138,8 +151,3 @@ async function fork (hostInfo, cmd, args, timeout, outputCallback, retryableExit
138
151
  });
139
152
  });
140
153
  }
141
- module.exports = {
142
- fork,
143
- createMasterPty,
144
- sshLoginCallback
145
- };