dsh-remote 0.8.67 → 0.8.70
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/README.en.md +389 -389
- package/README.md +462 -463
- package/lib/client.js +58 -47
- package/lib/exec-job.js +14 -0
- package/lib/index.js +4548 -4494
- package/lib/instructions-dynamic.js +80 -7
- package/lib/instructions.js +288 -288
- package/lib/openssh.js +40 -8
- package/lib/paths.js +27 -0
- package/package.json +2 -2
package/lib/openssh.js
CHANGED
|
@@ -208,6 +208,10 @@ export function baseSshArgs(config, { forSecret = false } = {}) {
|
|
|
208
208
|
const port = proxy.port && Number(proxy.port) !== 22 ? `:${Number(proxy.port)}` : ''
|
|
209
209
|
args.push('-J', `${user}${proxy.host}${port}`)
|
|
210
210
|
}
|
|
211
|
+
// An explicit known_hosts file (tests keep loopback keys OUT of the user's
|
|
212
|
+
// real ~/.ssh/known_hosts with this; ssh ignores $HOME on some platforms).
|
|
213
|
+
// Checking strength is unchanged — StrictHostKeyChecking below still applies.
|
|
214
|
+
if (config.knownHostsFile) args.push('-o', `UserKnownHostsFile=${config.knownHostsFile}`)
|
|
211
215
|
args.push(...hostKeyArgs(config))
|
|
212
216
|
const connectSecs = Math.max(1, Math.round((Number(config.connectTimeoutMs) || 15000) / 1000))
|
|
213
217
|
args.push('-o', `ConnectTimeout=${connectSecs}`, '-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3')
|
|
@@ -244,7 +248,7 @@ export class OpenSshConnection extends EventEmitter {
|
|
|
244
248
|
spawnSsh(extraArgs, { tty = false } = {}) {
|
|
245
249
|
if (this.closed) throw new Error('ssh connection closed')
|
|
246
250
|
const args = [
|
|
247
|
-
...baseSshArgs(this.pool.
|
|
251
|
+
...baseSshArgs(this.pool._argConfig(), { forSecret: !!this.secret }),
|
|
248
252
|
...extraArgs,
|
|
249
253
|
]
|
|
250
254
|
const child = spawn(this.sshBinary, args, {
|
|
@@ -395,6 +399,19 @@ export class SystemOpenSshPool {
|
|
|
395
399
|
this._secretPromise = null
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
/**
|
|
403
|
+
* The config baseSshArgs should see. `opts.knownHostsFile` (a path, or a
|
|
404
|
+
* function returning one, exactly like the native pool) used to be stored
|
|
405
|
+
* and silently dropped — so a custom known_hosts never reached the CLI, and
|
|
406
|
+
* tests had no way to keep loopback keys out of the user's real file.
|
|
407
|
+
* Resolved per call so a function value is honored as late as the native
|
|
408
|
+
* backend resolves it.
|
|
409
|
+
*/
|
|
410
|
+
_argConfig() {
|
|
411
|
+
const kh = typeof this._knownHostsFile === 'function' ? this._knownHostsFile() : this._knownHostsFile
|
|
412
|
+
return kh ? { ...this.config, knownHostsFile: kh } : this.config
|
|
413
|
+
}
|
|
414
|
+
|
|
398
415
|
/** The destination OpenSSH should connect to (an alias passes through). */
|
|
399
416
|
destination() {
|
|
400
417
|
// A bare alias resolves through ~/.ssh/config inside ssh itself, so it is
|
|
@@ -502,7 +519,12 @@ export class SystemOpenSshPool {
|
|
|
502
519
|
* Run one `ssh` invocation and collect its result.
|
|
503
520
|
*
|
|
504
521
|
* @param {string[]} extraArgs - arguments appended after the shared base.
|
|
505
|
-
* @param {object} [opts] - `timeoutMs`, `signal` (AbortSignal), `stdin
|
|
522
|
+
* @param {object} [opts] - `timeoutMs`, `signal` (AbortSignal), `stdin`,
|
|
523
|
+
* `onData(channel, chunk)` — a streaming tap the native backend has had
|
|
524
|
+
* since issue #70. A background job's value is its LIVE output, which the
|
|
525
|
+
* job ring must receive as it arrives; a backend that only returns the
|
|
526
|
+
* accumulated text loses every byte the caller stopped waiting for
|
|
527
|
+
* (issue #80). Tap errors are not command errors, so the call is guarded.
|
|
506
528
|
* @returns {Promise<{code: number|null, signal: string|null, stdout: string, stderr: string}>}
|
|
507
529
|
*/
|
|
508
530
|
async _runSsh(extraArgs, opts = {}) {
|
|
@@ -510,7 +532,7 @@ export class SystemOpenSshPool {
|
|
|
510
532
|
const secret = await this._resolveSecret()
|
|
511
533
|
const timeoutMs = opts.timeoutMs || this.config.commandTimeoutMs
|
|
512
534
|
const sshBinary = resolveSshBinary(this.config)
|
|
513
|
-
const args = [...baseSshArgs(this.
|
|
535
|
+
const args = [...baseSshArgs(this._argConfig(), { forSecret: !!secret }), ...extraArgs]
|
|
514
536
|
return new Promise((resolve) => {
|
|
515
537
|
let child
|
|
516
538
|
try {
|
|
@@ -524,6 +546,7 @@ export class SystemOpenSshPool {
|
|
|
524
546
|
let exitCode = null
|
|
525
547
|
let exitSignal = null
|
|
526
548
|
const cap = Math.max(this.config.maxOutputChars * 4, 1024 * 1024)
|
|
549
|
+
const onData = typeof opts.onData === 'function' ? opts.onData : null
|
|
527
550
|
const cleanupAbort = () => { if (opts.signal && onAbort) opts.signal.removeEventListener('abort', onAbort) }
|
|
528
551
|
let onAbort = null
|
|
529
552
|
const finish = () => {
|
|
@@ -562,8 +585,17 @@ export class SystemOpenSshPool {
|
|
|
562
585
|
}
|
|
563
586
|
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
564
587
|
}
|
|
565
|
-
|
|
566
|
-
|
|
588
|
+
// The tap fires even past the local cap, exactly like the native
|
|
589
|
+
// backend's: a promoted job's ring has its own bounds and must keep
|
|
590
|
+
// receiving output the truncated return value no longer holds.
|
|
591
|
+
child.stdout.on('data', (d) => {
|
|
592
|
+
if (stdout.length < cap) stdout += d
|
|
593
|
+
if (onData) { try { onData('stdout', d) } catch { /* tap errors are not command errors */ } }
|
|
594
|
+
})
|
|
595
|
+
child.stderr.on('data', (d) => {
|
|
596
|
+
if (stderr.length < cap) stderr += d
|
|
597
|
+
if (onData) { try { onData('stderr', d) } catch { /* tap errors are not command errors */ } }
|
|
598
|
+
})
|
|
567
599
|
child.on('error', (e) => {
|
|
568
600
|
if (settled) return
|
|
569
601
|
stderr += String((e && e.message) || e)
|
|
@@ -651,9 +683,9 @@ export class SystemOpenSshPool {
|
|
|
651
683
|
if (this.platform === 'windows' && this.gitBashPath) {
|
|
652
684
|
// Pipe the script to `bash -s` over stdin: no quoting round-trip, so
|
|
653
685
|
// quotes/backslashes/newlines survive verbatim (same as the native pool).
|
|
654
|
-
return this._runSsh(['--', this.destination(), `"${this.gitBashPath}" -s`], { timeoutMs, signal: opts.signal, stdin: String(command) })
|
|
686
|
+
return this._runSsh(['--', this.destination(), `"${this.gitBashPath}" -s`], { timeoutMs, signal: opts.signal, stdin: String(command), onData: opts.onData })
|
|
655
687
|
}
|
|
656
|
-
return this._runSsh(['--', this.destination(), String(command)], { timeoutMs, signal: opts.signal })
|
|
688
|
+
return this._runSsh(['--', this.destination(), String(command)], { timeoutMs, signal: opts.signal, onData: opts.onData })
|
|
657
689
|
})
|
|
658
690
|
}
|
|
659
691
|
|
|
@@ -662,7 +694,7 @@ export class SystemOpenSshPool {
|
|
|
662
694
|
const Sftp = loadSftpEngine()
|
|
663
695
|
const secret = await this._resolveSecret()
|
|
664
696
|
const sshBinary = resolveSshBinary(this.config)
|
|
665
|
-
const args = [...baseSshArgs(this.
|
|
697
|
+
const args = [...baseSshArgs(this._argConfig(), { forSecret: !!secret }), '-s', this.destination(), 'sftp']
|
|
666
698
|
const child = spawn(sshBinary, args, { stdio: ['pipe', 'pipe', 'pipe'], env: secret ? askpassEnv(secret) : undefined })
|
|
667
699
|
|
|
668
700
|
// ssh2's SFTP engine only reaches `_protocol` for channelData/channelClose,
|
package/lib/paths.js
CHANGED
|
@@ -184,6 +184,33 @@ export function remotePathBase(p) {
|
|
|
184
184
|
return base
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Workspace title for a REMOTE mirror: `<machine>-<remote basename>`.
|
|
189
|
+
*
|
|
190
|
+
* The Harness derives a Workspace title from the basename of the directory it
|
|
191
|
+
* registers, and a remote workspace is registered on its LOCAL mirror
|
|
192
|
+
* (`$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<basename>`). That last
|
|
193
|
+
* segment is still the remote basename, so a local `/mnt/xdfs` and a remote
|
|
194
|
+
* `/mnt/xdfs` both read "xdfs". dsh-remote pre-registers the mirror with this
|
|
195
|
+
* host-qualified title, so the two are distinguishable from the start.
|
|
196
|
+
*
|
|
197
|
+
* The machine's display name wins; a machine saved without one (or named after
|
|
198
|
+
* the folder itself) falls back to its host. A workspace title must be
|
|
199
|
+
* non-empty, so the bare basename is the last resort.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} remotePath - remote workspace directory (POSIX or Windows).
|
|
202
|
+
* @param {{name?: string, host?: string}} [machine] - the machine registry record.
|
|
203
|
+
* @returns {string} a non-empty, host-qualified title.
|
|
204
|
+
*/
|
|
205
|
+
export function remoteWorkspaceTitle(remotePath, machine) {
|
|
206
|
+
const base = remotePathBase(remotePath)
|
|
207
|
+
const label = String((machine && machine.name) || '').trim()
|
|
208
|
+
const host = String((machine && machine.host) || '').trim()
|
|
209
|
+
if (label && label !== base) return label + '-' + base
|
|
210
|
+
if (host && host !== base) return host + '-' + base
|
|
211
|
+
return base
|
|
212
|
+
}
|
|
213
|
+
|
|
187
214
|
/** Relative path of `p` under `root` (POSIX form, no leading slash), or null
|
|
188
215
|
* when `p` is not inside `root`. Both POSIX and Windows drive paths work
|
|
189
216
|
* (Windows is compared in its `/D:/…` POSIX-equivalent form). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), pick a remote workspace, operate on it with
|
|
3
|
+
"version": "0.8.70",
|
|
4
|
+
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), list hosts from ~/.ssh/config, pick a remote workspace, operate on it with 21 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, remote DSH web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|
|
7
7
|
"readme": "README.md",
|