dsh-remote 0.8.71 → 0.8.73
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/LICENSE +21 -21
- package/README.en.md +389 -389
- package/README.md +462 -462
- package/cordis.patch.yml +9 -9
- package/lib/audit.js +45 -0
- package/lib/binding.js +172 -172
- package/lib/client.js +4022 -3990
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -118
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4550 -4548
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -886
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/package.json +100 -99
package/lib/remote-fs.js
CHANGED
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
// Shared remote SFTP helpers used by both rw_* tools and the sidebar HTTP routes.
|
|
2
|
-
import { joinRemotePath, normalizeRemotePath } from './paths.js'
|
|
3
|
-
|
|
4
|
-
/** Structured listing: name + type + size + mtime + mode (SFTP protocol-level). */
|
|
5
|
-
export async function listDirStructured(sftp, p) {
|
|
6
|
-
const target = normalizeRemotePath(p || '/')
|
|
7
|
-
let list
|
|
8
|
-
try {
|
|
9
|
-
list = await sftp.readdir(target)
|
|
10
|
-
} catch (err) {
|
|
11
|
-
const msg = String((err && err.message) || err)
|
|
12
|
-
if (/no such file|not found|does not exist|ENOENT/i.test(msg)) {
|
|
13
|
-
return { path: target, items: [], missing: true }
|
|
14
|
-
}
|
|
15
|
-
throw new Error('browse failed: ' + msg)
|
|
16
|
-
}
|
|
17
|
-
const items = []
|
|
18
|
-
const symIdx = []
|
|
19
|
-
for (const e of list) {
|
|
20
|
-
const name = String(e.filename)
|
|
21
|
-
if (name === '.' || name === '..' || !name) continue
|
|
22
|
-
const a = e.attrs || {}
|
|
23
|
-
let type
|
|
24
|
-
if (a.isSymbolicLink && a.isSymbolicLink()) {
|
|
25
|
-
type = 'symlink'
|
|
26
|
-
symIdx.push(items.length)
|
|
27
|
-
} else if (a.isDirectory && a.isDirectory()) {
|
|
28
|
-
type = 'dir'
|
|
29
|
-
} else {
|
|
30
|
-
type = 'file'
|
|
31
|
-
}
|
|
32
|
-
items.push({
|
|
33
|
-
type,
|
|
34
|
-
name,
|
|
35
|
-
size: typeof a.size === 'number' ? a.size : 0,
|
|
36
|
-
mtime: typeof a.mtime === 'number' ? a.mtime : 0,
|
|
37
|
-
mode: typeof a.mode === 'number' ? a.mode.toString(8) : '',
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
if (symIdx.length) {
|
|
41
|
-
// Resolve what each symlink POINTS TO. This must be stat (follows links),
|
|
42
|
-
// not lstat: lstat describes the link itself, so a symlink to a directory
|
|
43
|
-
// was misclassified as 'file' and the workspace picker refused to enter
|
|
44
|
-
// it (issue #59). A dangling link fails stat and degrades to 'file'.
|
|
45
|
-
await Promise.all(symIdx.map(async (i) => {
|
|
46
|
-
const full = joinRemotePath(target, items[i].name)
|
|
47
|
-
try {
|
|
48
|
-
const st = await sftp.stat(full)
|
|
49
|
-
items[i].type = st && st.isDirectory && st.isDirectory() ? 'dir' : 'file'
|
|
50
|
-
items[i].link = true
|
|
51
|
-
} catch { items[i].type = 'file' /* dangling link: degrade to file */ }
|
|
52
|
-
}))
|
|
53
|
-
}
|
|
54
|
-
return { path: target, items }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Windows drive letters as display-form dir entries ("This PC" root view). */
|
|
58
|
-
export async function listRemoteDrives(sshPool) {
|
|
59
|
-
let letters = []
|
|
60
|
-
try {
|
|
61
|
-
const res = await sshPool.exec('cmd /c fsutil fsinfo drives', { timeoutMs: Math.min(sshPool.config.commandTimeoutMs || 8000, 8000) })
|
|
62
|
-
letters = [...String(res.stdout || '').matchAll(/([a-zA-Z]):\\?/g)].map((mm) => mm[1].toUpperCase())
|
|
63
|
-
} catch {}
|
|
64
|
-
if (!letters.length) {
|
|
65
|
-
try {
|
|
66
|
-
const res = await sshPool.exec('ls -d /[a-z] 2>/dev/null', { timeoutMs: Math.min(sshPool.config.commandTimeoutMs || 8000, 8000) })
|
|
67
|
-
letters = String(res.stdout || '')
|
|
68
|
-
.split(/\s+/)
|
|
69
|
-
.map((s) => s.replace(/^\/+|\/+$/g, ''))
|
|
70
|
-
.filter((s) => /^[a-zA-Z]$/.test(s))
|
|
71
|
-
.map((s) => s.toUpperCase())
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
|
-
return [...new Set(letters)].sort().map((l) => ({ name: l + ':\\', path: l + ':\\', type: 'dir', drive: true, size: 0, mtime: 0 }))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Recursive remote delete (bounded): unlink files bottom-up, then rmdir. */
|
|
78
|
-
export async function removeRemoteTree(sftp, p, maxFiles = 2000) {
|
|
79
|
-
let removed = 0
|
|
80
|
-
const walk = async (dir) => {
|
|
81
|
-
if (removed >= maxFiles) return
|
|
82
|
-
let entries = []
|
|
83
|
-
try { entries = (await sftp.readdir(dir)) || [] } catch { return }
|
|
84
|
-
for (const e of entries) {
|
|
85
|
-
if (removed >= maxFiles) return
|
|
86
|
-
const name = String(e.filename)
|
|
87
|
-
if (name === '.' || name === '..') continue
|
|
88
|
-
const fp = joinRemotePath(dir, name)
|
|
89
|
-
const isDir = !!(e.attrs && e.attrs.isDirectory && e.attrs.isDirectory())
|
|
90
|
-
if (isDir) await walk(fp)
|
|
91
|
-
else {
|
|
92
|
-
try { await sftp.unlink(fp); removed++ } catch {}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
try { await sftp.rmdir(dir); removed++ } catch { /* already gone or non-empty */ }
|
|
96
|
-
}
|
|
97
|
-
await walk(p)
|
|
98
|
-
return removed
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** MIME types for image formats the preview panel can render inline. */
|
|
102
|
-
const IMAGE_MIME_TYPES = {
|
|
103
|
-
png: 'image/png',
|
|
104
|
-
jpg: 'image/jpeg',
|
|
105
|
-
jpeg: 'image/jpeg',
|
|
106
|
-
gif: 'image/gif',
|
|
107
|
-
webp: 'image/webp',
|
|
108
|
-
svg: 'image/svg+xml',
|
|
109
|
-
bmp: 'image/bmp',
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function looksBinary(buf) {
|
|
113
|
-
const n = Math.min(buf.length, 8192)
|
|
114
|
-
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Read a remote file for the sidebar/editor. Oversized files use a range read
|
|
120
|
-
* (`readPartial`) so we never `fastGet` the whole blob onto disk.
|
|
121
|
-
*
|
|
122
|
-
* Images are the exception (issue #66): a PREFIX of an image is a broken
|
|
123
|
-
* picture, not a preview, so for a recognised image extension the caller's
|
|
124
|
-
* text-oriented cap is skipped and the file is read whole up to the hard 2MB
|
|
125
|
-
* ceiling; the panel renders it inline as `<img src="data:…">`. An image past
|
|
126
|
-
* the ceiling degrades to the plain binary stub + download button rather than
|
|
127
|
-
* shipping a guaranteed-broken half render. (`<img>` is also the safe sink
|
|
128
|
-
* for SVG: unlike inline markup it cannot run the file's scripts.)
|
|
129
|
-
*/
|
|
130
|
-
export async function previewRemoteFile(sftp, target, opts = {}) {
|
|
131
|
-
const {
|
|
132
|
-
maxBytes = 256 * 1024,
|
|
133
|
-
encoding = 'utf-8',
|
|
134
|
-
decodeBuf = (buf) => buf.toString('utf8'),
|
|
135
|
-
} = opts
|
|
136
|
-
const hardCap = 2 * 1024 * 1024
|
|
137
|
-
const name = String(target).split(/[\\/]/).pop() || ''
|
|
138
|
-
const imageMime = IMAGE_MIME_TYPES[(name.split('.').pop() || '').toLowerCase()]
|
|
139
|
-
const cap = imageMime ? hardCap : Math.min(Math.max(Number(maxBytes) || 256 * 1024, 1024), hardCap)
|
|
140
|
-
const st = await sftp.stat(target)
|
|
141
|
-
const mtime = st && typeof st.mtime === 'number' ? st.mtime : 0
|
|
142
|
-
const size = st && typeof st.size === 'number' ? st.size : 0
|
|
143
|
-
let buf
|
|
144
|
-
let truncated = false
|
|
145
|
-
if (size > cap) {
|
|
146
|
-
truncated = true
|
|
147
|
-
if (typeof sftp.readPartial === 'function') {
|
|
148
|
-
buf = await sftp.readPartial(target, 0, cap)
|
|
149
|
-
} else {
|
|
150
|
-
const full = await sftp.readFile(target)
|
|
151
|
-
buf = full.subarray(0, cap)
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
buf = await sftp.readFile(target)
|
|
155
|
-
}
|
|
156
|
-
if (imageMime && !truncated) {
|
|
157
|
-
return { ok: true, binary: false, image: true, mime: imageMime, content: buf.toString('base64'), truncated, size, mtime }
|
|
158
|
-
}
|
|
159
|
-
if (looksBinary(buf)) {
|
|
160
|
-
return {
|
|
161
|
-
ok: true, binary: true, size, mtime,
|
|
162
|
-
head: buf.slice(0, Math.min(buf.length, 4096)).toString('base64'),
|
|
163
|
-
truncated,
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
let content = decodeBuf(buf, encoding).replace(/\r\n/g, '\n')
|
|
167
|
-
if (!truncated && content.length > cap) {
|
|
168
|
-
truncated = true
|
|
169
|
-
content = content.slice(0, cap)
|
|
170
|
-
}
|
|
171
|
-
if (truncated) content += `\n…[truncated: ${Math.max(0, size - cap)} more bytes]`
|
|
172
|
-
return { ok: true, binary: false, content, truncated, size, mtime }
|
|
173
|
-
}
|
|
1
|
+
// Shared remote SFTP helpers used by both rw_* tools and the sidebar HTTP routes.
|
|
2
|
+
import { joinRemotePath, normalizeRemotePath } from './paths.js'
|
|
3
|
+
|
|
4
|
+
/** Structured listing: name + type + size + mtime + mode (SFTP protocol-level). */
|
|
5
|
+
export async function listDirStructured(sftp, p) {
|
|
6
|
+
const target = normalizeRemotePath(p || '/')
|
|
7
|
+
let list
|
|
8
|
+
try {
|
|
9
|
+
list = await sftp.readdir(target)
|
|
10
|
+
} catch (err) {
|
|
11
|
+
const msg = String((err && err.message) || err)
|
|
12
|
+
if (/no such file|not found|does not exist|ENOENT/i.test(msg)) {
|
|
13
|
+
return { path: target, items: [], missing: true }
|
|
14
|
+
}
|
|
15
|
+
throw new Error('browse failed: ' + msg)
|
|
16
|
+
}
|
|
17
|
+
const items = []
|
|
18
|
+
const symIdx = []
|
|
19
|
+
for (const e of list) {
|
|
20
|
+
const name = String(e.filename)
|
|
21
|
+
if (name === '.' || name === '..' || !name) continue
|
|
22
|
+
const a = e.attrs || {}
|
|
23
|
+
let type
|
|
24
|
+
if (a.isSymbolicLink && a.isSymbolicLink()) {
|
|
25
|
+
type = 'symlink'
|
|
26
|
+
symIdx.push(items.length)
|
|
27
|
+
} else if (a.isDirectory && a.isDirectory()) {
|
|
28
|
+
type = 'dir'
|
|
29
|
+
} else {
|
|
30
|
+
type = 'file'
|
|
31
|
+
}
|
|
32
|
+
items.push({
|
|
33
|
+
type,
|
|
34
|
+
name,
|
|
35
|
+
size: typeof a.size === 'number' ? a.size : 0,
|
|
36
|
+
mtime: typeof a.mtime === 'number' ? a.mtime : 0,
|
|
37
|
+
mode: typeof a.mode === 'number' ? a.mode.toString(8) : '',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
if (symIdx.length) {
|
|
41
|
+
// Resolve what each symlink POINTS TO. This must be stat (follows links),
|
|
42
|
+
// not lstat: lstat describes the link itself, so a symlink to a directory
|
|
43
|
+
// was misclassified as 'file' and the workspace picker refused to enter
|
|
44
|
+
// it (issue #59). A dangling link fails stat and degrades to 'file'.
|
|
45
|
+
await Promise.all(symIdx.map(async (i) => {
|
|
46
|
+
const full = joinRemotePath(target, items[i].name)
|
|
47
|
+
try {
|
|
48
|
+
const st = await sftp.stat(full)
|
|
49
|
+
items[i].type = st && st.isDirectory && st.isDirectory() ? 'dir' : 'file'
|
|
50
|
+
items[i].link = true
|
|
51
|
+
} catch { items[i].type = 'file' /* dangling link: degrade to file */ }
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
return { path: target, items }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Windows drive letters as display-form dir entries ("This PC" root view). */
|
|
58
|
+
export async function listRemoteDrives(sshPool) {
|
|
59
|
+
let letters = []
|
|
60
|
+
try {
|
|
61
|
+
const res = await sshPool.exec('cmd /c fsutil fsinfo drives', { timeoutMs: Math.min(sshPool.config.commandTimeoutMs || 8000, 8000) })
|
|
62
|
+
letters = [...String(res.stdout || '').matchAll(/([a-zA-Z]):\\?/g)].map((mm) => mm[1].toUpperCase())
|
|
63
|
+
} catch {}
|
|
64
|
+
if (!letters.length) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await sshPool.exec('ls -d /[a-z] 2>/dev/null', { timeoutMs: Math.min(sshPool.config.commandTimeoutMs || 8000, 8000) })
|
|
67
|
+
letters = String(res.stdout || '')
|
|
68
|
+
.split(/\s+/)
|
|
69
|
+
.map((s) => s.replace(/^\/+|\/+$/g, ''))
|
|
70
|
+
.filter((s) => /^[a-zA-Z]$/.test(s))
|
|
71
|
+
.map((s) => s.toUpperCase())
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
return [...new Set(letters)].sort().map((l) => ({ name: l + ':\\', path: l + ':\\', type: 'dir', drive: true, size: 0, mtime: 0 }))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Recursive remote delete (bounded): unlink files bottom-up, then rmdir. */
|
|
78
|
+
export async function removeRemoteTree(sftp, p, maxFiles = 2000) {
|
|
79
|
+
let removed = 0
|
|
80
|
+
const walk = async (dir) => {
|
|
81
|
+
if (removed >= maxFiles) return
|
|
82
|
+
let entries = []
|
|
83
|
+
try { entries = (await sftp.readdir(dir)) || [] } catch { return }
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
if (removed >= maxFiles) return
|
|
86
|
+
const name = String(e.filename)
|
|
87
|
+
if (name === '.' || name === '..') continue
|
|
88
|
+
const fp = joinRemotePath(dir, name)
|
|
89
|
+
const isDir = !!(e.attrs && e.attrs.isDirectory && e.attrs.isDirectory())
|
|
90
|
+
if (isDir) await walk(fp)
|
|
91
|
+
else {
|
|
92
|
+
try { await sftp.unlink(fp); removed++ } catch {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try { await sftp.rmdir(dir); removed++ } catch { /* already gone or non-empty */ }
|
|
96
|
+
}
|
|
97
|
+
await walk(p)
|
|
98
|
+
return removed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** MIME types for image formats the preview panel can render inline. */
|
|
102
|
+
const IMAGE_MIME_TYPES = {
|
|
103
|
+
png: 'image/png',
|
|
104
|
+
jpg: 'image/jpeg',
|
|
105
|
+
jpeg: 'image/jpeg',
|
|
106
|
+
gif: 'image/gif',
|
|
107
|
+
webp: 'image/webp',
|
|
108
|
+
svg: 'image/svg+xml',
|
|
109
|
+
bmp: 'image/bmp',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function looksBinary(buf) {
|
|
113
|
+
const n = Math.min(buf.length, 8192)
|
|
114
|
+
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read a remote file for the sidebar/editor. Oversized files use a range read
|
|
120
|
+
* (`readPartial`) so we never `fastGet` the whole blob onto disk.
|
|
121
|
+
*
|
|
122
|
+
* Images are the exception (issue #66): a PREFIX of an image is a broken
|
|
123
|
+
* picture, not a preview, so for a recognised image extension the caller's
|
|
124
|
+
* text-oriented cap is skipped and the file is read whole up to the hard 2MB
|
|
125
|
+
* ceiling; the panel renders it inline as `<img src="data:…">`. An image past
|
|
126
|
+
* the ceiling degrades to the plain binary stub + download button rather than
|
|
127
|
+
* shipping a guaranteed-broken half render. (`<img>` is also the safe sink
|
|
128
|
+
* for SVG: unlike inline markup it cannot run the file's scripts.)
|
|
129
|
+
*/
|
|
130
|
+
export async function previewRemoteFile(sftp, target, opts = {}) {
|
|
131
|
+
const {
|
|
132
|
+
maxBytes = 256 * 1024,
|
|
133
|
+
encoding = 'utf-8',
|
|
134
|
+
decodeBuf = (buf) => buf.toString('utf8'),
|
|
135
|
+
} = opts
|
|
136
|
+
const hardCap = 2 * 1024 * 1024
|
|
137
|
+
const name = String(target).split(/[\\/]/).pop() || ''
|
|
138
|
+
const imageMime = IMAGE_MIME_TYPES[(name.split('.').pop() || '').toLowerCase()]
|
|
139
|
+
const cap = imageMime ? hardCap : Math.min(Math.max(Number(maxBytes) || 256 * 1024, 1024), hardCap)
|
|
140
|
+
const st = await sftp.stat(target)
|
|
141
|
+
const mtime = st && typeof st.mtime === 'number' ? st.mtime : 0
|
|
142
|
+
const size = st && typeof st.size === 'number' ? st.size : 0
|
|
143
|
+
let buf
|
|
144
|
+
let truncated = false
|
|
145
|
+
if (size > cap) {
|
|
146
|
+
truncated = true
|
|
147
|
+
if (typeof sftp.readPartial === 'function') {
|
|
148
|
+
buf = await sftp.readPartial(target, 0, cap)
|
|
149
|
+
} else {
|
|
150
|
+
const full = await sftp.readFile(target)
|
|
151
|
+
buf = full.subarray(0, cap)
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
buf = await sftp.readFile(target)
|
|
155
|
+
}
|
|
156
|
+
if (imageMime && !truncated) {
|
|
157
|
+
return { ok: true, binary: false, image: true, mime: imageMime, content: buf.toString('base64'), truncated, size, mtime }
|
|
158
|
+
}
|
|
159
|
+
if (looksBinary(buf)) {
|
|
160
|
+
return {
|
|
161
|
+
ok: true, binary: true, size, mtime,
|
|
162
|
+
head: buf.slice(0, Math.min(buf.length, 4096)).toString('base64'),
|
|
163
|
+
truncated,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let content = decodeBuf(buf, encoding).replace(/\r\n/g, '\n')
|
|
167
|
+
if (!truncated && content.length > cap) {
|
|
168
|
+
truncated = true
|
|
169
|
+
content = content.slice(0, cap)
|
|
170
|
+
}
|
|
171
|
+
if (truncated) content += `\n…[truncated: ${Math.max(0, size - cap)} more bytes]`
|
|
172
|
+
return { ok: true, binary: false, content, truncated, size, mtime }
|
|
173
|
+
}
|