dsh-remote 0.8.47 → 0.8.48
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/lib/ignore.js +46 -0
- package/lib/index.js +31 -23
- package/lib/search.js +8 -3
- package/package.json +2 -1
package/lib/ignore.js
CHANGED
|
@@ -139,6 +139,52 @@ export const DEFAULT_SEARCH_IGNORE = [
|
|
|
139
139
|
'Library/Caches/',
|
|
140
140
|
]
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Whether a normalized remote path is a FILESYSTEM ROOT: POSIX `/`, a Windows
|
|
144
|
+
* drive root (`D:\`), or a UNC share root (`\\server\share`).
|
|
145
|
+
*
|
|
146
|
+
* Root workspaces are legal (issue #60) but dangerous to walk naively: `/proc`
|
|
147
|
+
* and `/sys` are synthetic (new pids appear between readdirs, `/proc/kcore`
|
|
148
|
+
* advertises 128 TB), `/dev` holds never-ending character devices, and
|
|
149
|
+
* `/var/lib/docker` is tens of GB of image layers. ROOT_IGNORE exists so a
|
|
150
|
+
* root workspace skips exactly those trees.
|
|
151
|
+
*/
|
|
152
|
+
export function isFilesystemRoot(p) {
|
|
153
|
+
const s = String(p || '')
|
|
154
|
+
return s === '/' || /^[a-zA-Z]:\\?$/.test(s) || /^\\\\[^\\]+\\[^\\]+$/.test(s)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Patterns merged in ONLY when the workspace is the filesystem root
|
|
159
|
+
* (issue #60). Every entry is root-anchored AND directory-only, so even when
|
|
160
|
+
* merged they can only match at the workspace top level: `/` skips `/proc`,
|
|
161
|
+
* while a hypothetical `/home/u/proc` under a non-root workspace is ordinary
|
|
162
|
+
* content (and non-root workspaces never receive these patterns at all, so
|
|
163
|
+
* their behaviour is byte-identical to before).
|
|
164
|
+
*/
|
|
165
|
+
export const ROOT_IGNORE = [
|
|
166
|
+
'/proc/',
|
|
167
|
+
'/sys/',
|
|
168
|
+
'/dev/',
|
|
169
|
+
'/run/',
|
|
170
|
+
'/boot/',
|
|
171
|
+
'/var/lib/docker/',
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Effective ignore patterns for a workspace rooted at `root`:
|
|
176
|
+
* the defaults, plus ROOT_IGNORE only for a root workspace, plus the user's
|
|
177
|
+
* own `.dsh-remote-ignore` lines (always last, they stack on top).
|
|
178
|
+
*
|
|
179
|
+
* @param {string} root - workspace root as passed to sync/push/search.
|
|
180
|
+
* @param {{search?: boolean, fromFile?: string[]}} [opts]
|
|
181
|
+
*/
|
|
182
|
+
export function ignorePatternsFor(root, { search = false, fromFile = [] } = {}) {
|
|
183
|
+
const base = search ? DEFAULT_SEARCH_IGNORE : DEFAULT_IGNORE
|
|
184
|
+
const extra = isFilesystemRoot(root) ? ROOT_IGNORE : []
|
|
185
|
+
return base.concat(extra, fromFile)
|
|
186
|
+
}
|
|
187
|
+
|
|
142
188
|
/**
|
|
143
189
|
* Load the user ignore file (gitignore syntax) and merge with defaults.
|
|
144
190
|
* Returns `{ patterns, fromFile }`.
|
package/lib/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
toShellPath, toDisplayPath, remotePathBase, truncate, shortHash,
|
|
32
32
|
} from './paths.js'
|
|
33
33
|
import { createHostKeyGuard, isHostKeyKnown as _isHostKeyKnown } from './hostkey.js'
|
|
34
|
-
import { compileIgnore,
|
|
34
|
+
import { compileIgnore, ignorePatternsFor, DEFAULT_IGNORE } from './ignore.js'
|
|
35
35
|
import { friendlyMessage } from './errors.js'
|
|
36
36
|
import { importableEntries, sshConfigPath, readSshConfigText, resolveMachineSshConfig } from './sshconfig.js'
|
|
37
37
|
import { createRemoteFileReference, installFileReferenceOverlay, DEFAULT_EXCLUDED_DIRECTORIES } from './file-reference.js'
|
|
@@ -927,26 +927,21 @@ export async function apply(ctx, config) {
|
|
|
927
927
|
// NOTE: this matcher is shared by mirror sync (rw_sync/rw_push) and search.
|
|
928
928
|
// Do NOT add search-only cache excludes here: widening it would silently stop
|
|
929
929
|
// syncing directories a user may rely on. Search uses searchIgnoreMatcher().
|
|
930
|
-
const
|
|
930
|
+
const readUserIgnore = () => {
|
|
931
931
|
try {
|
|
932
|
-
|
|
933
|
-
return compileIgnore(DEFAULT_IGNORE.concat(fromFile))
|
|
932
|
+
return readFileSync(ignoreFile(), 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
|
|
934
933
|
} catch {
|
|
935
|
-
return
|
|
934
|
+
return []
|
|
936
935
|
}
|
|
937
936
|
}
|
|
937
|
+
/** Mirror-sync matcher for the workspace rooted at `root` (ROOT_IGNORE is
|
|
938
|
+
* merged only for a filesystem-root workspace — issue #60). */
|
|
939
|
+
const ignoreMatcher = (root) => compileIgnore(ignorePatternsFor(root, { fromFile: readUserIgnore() }))
|
|
938
940
|
/** Search-only matcher: same as ignoreMatcher() plus machine-local cache trees
|
|
939
941
|
* (`~/.npm`, `~/.cache`, …) that are huge, almost never what a human is
|
|
940
942
|
* looking for, and the main cause of the issue #44 hang. Mirror sync is
|
|
941
943
|
* deliberately unaffected. An explicit `path` still searches anywhere. */
|
|
942
|
-
const searchIgnoreMatcher = () => {
|
|
943
|
-
try {
|
|
944
|
-
const fromFile = readFileSync(ignoreFile(), 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
|
|
945
|
-
return compileIgnore(DEFAULT_SEARCH_IGNORE.concat(fromFile))
|
|
946
|
-
} catch {
|
|
947
|
-
return compileIgnore(DEFAULT_SEARCH_IGNORE)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
944
|
+
const searchIgnoreMatcher = (root) => compileIgnore(ignorePatternsFor(root, { search: true, fromFile: readUserIgnore() }))
|
|
950
945
|
|
|
951
946
|
// ── task + forward managers ───────────────────────────────────────────────
|
|
952
947
|
const tasks = new TaskManager()
|
|
@@ -991,7 +986,7 @@ export async function apply(ctx, config) {
|
|
|
991
986
|
const target = poolForMachine(machine)
|
|
992
987
|
let sftp
|
|
993
988
|
try { sftp = await target.sftp() } catch { return }
|
|
994
|
-
const matcher = ignoreMatcher()
|
|
989
|
+
const matcher = ignoreMatcher(ws)
|
|
995
990
|
const state = loadSyncState(localDir)
|
|
996
991
|
const next = { ...state }
|
|
997
992
|
if (rels.includes('*')) {
|
|
@@ -1711,8 +1706,15 @@ export async function apply(ctx, config) {
|
|
|
1711
1706
|
},
|
|
1712
1707
|
output: textOut,
|
|
1713
1708
|
async execute(args, exec) {
|
|
1714
|
-
|
|
1715
|
-
|
|
1709
|
+
// Blank input is the only upfront rejection: normalizeRemotePath('')
|
|
1710
|
+
// returns '/', so the old `!p || p === '/'` guard actually meant "no
|
|
1711
|
+
// root" — but a filesystem root is a legal workspace (issue #60), and
|
|
1712
|
+
// its synthetic-tree risk is handled by ROOT_IGNORE in sync/search.
|
|
1713
|
+
// Any non-blank path, root included, is answered by the real directory
|
|
1714
|
+
// probe below (a missing path still fails there).
|
|
1715
|
+
const rawPath = String(args.path || '').trim()
|
|
1716
|
+
if (!rawPath) throw new Error('rw_pick_workspace: path must be an absolute directory')
|
|
1717
|
+
const p = normalizeRemotePath(rawPath)
|
|
1716
1718
|
// Picking a workspace targets the machine this session is already bound
|
|
1717
1719
|
// to when it has one. A LOCAL session deliberately falls back to the
|
|
1718
1720
|
// active machine here: this is the setup flow that runs right after
|
|
@@ -1755,7 +1757,7 @@ export async function apply(ctx, config) {
|
|
|
1755
1757
|
mkdirSync(local, { recursive: true })
|
|
1756
1758
|
const depth = Math.min(Math.max(Number(args.depth) || 8, 1), 16)
|
|
1757
1759
|
const maxFiles = Math.min(Math.max(Number(args.maxFiles) || 2000, 1), 20000)
|
|
1758
|
-
const isIgnored = ignoreMatcher()
|
|
1760
|
+
const isIgnored = ignoreMatcher(p)
|
|
1759
1761
|
const body = { depth, maxFiles, dryRun: !!args.dryRun, force: !!args.force, isIgnored }
|
|
1760
1762
|
const runSync = async () => {
|
|
1761
1763
|
let sftp
|
|
@@ -1805,7 +1807,7 @@ export async function apply(ctx, config) {
|
|
|
1805
1807
|
const local = b.mirrorDir || mirrorDirFor(p, b.host, b.username, b.port)
|
|
1806
1808
|
if (!existsSync(local)) throw new Error(`rw_push: local mirror does not exist — run rw_sync first (${local})`)
|
|
1807
1809
|
const maxFiles = Math.min(Math.max(Number(args.maxFiles) || 2000, 1), 20000)
|
|
1808
|
-
const isIgnored = ignoreMatcher()
|
|
1810
|
+
const isIgnored = ignoreMatcher(p)
|
|
1809
1811
|
const body = { maxFiles, dryRun: !!args.dryRun, force: !!args.force, isIgnored }
|
|
1810
1812
|
const runPush = async () => {
|
|
1811
1813
|
let sftp
|
|
@@ -2227,7 +2229,7 @@ export async function apply(ctx, config) {
|
|
|
2227
2229
|
// `running` and cancel/steer did nothing (issue #44).
|
|
2228
2230
|
const maxFiles = Math.min(Math.max(Number(args.maxEntries) || config.searchMaxEntries, 1), 500000)
|
|
2229
2231
|
const maxDurationMs = Math.min(Math.max(Number(args.maxDurationMs) || config.searchTimeoutMs, 1000), 600000)
|
|
2230
|
-
const matcher = searchIgnoreMatcher()
|
|
2232
|
+
const matcher = searchIgnoreMatcher(dir)
|
|
2231
2233
|
const { matches, scanned, truncated, cancelled } = await searchRemote(b.pool, dir, {
|
|
2232
2234
|
pattern,
|
|
2233
2235
|
regex,
|
|
@@ -2832,8 +2834,11 @@ export async function apply(ctx, config) {
|
|
|
2832
2834
|
if (req.method !== 'POST') return sendJson(res, 405, { error: 'method not allowed' })
|
|
2833
2835
|
try {
|
|
2834
2836
|
const payload = JSON.parse((await readBody(req)) || '{}')
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
+
// Blank-only guard: a filesystem root is a legal workspace (issue
|
|
2838
|
+
// #60); the real directory probe below answers everything else.
|
|
2839
|
+
const rawPath = String(payload.path || '').trim()
|
|
2840
|
+
if (!rawPath) return sendJson(res, 400, { error: 'path must be an absolute directory' })
|
|
2841
|
+
const p = normalizeRemotePath(rawPath)
|
|
2837
2842
|
const okDir = await isRemoteDir(p)
|
|
2838
2843
|
if (!okDir) return sendJson(res, 400, { ok: false, error: `not a directory: ${p}` })
|
|
2839
2844
|
persistWorkspace(p)
|
|
@@ -2852,8 +2857,11 @@ export async function apply(ctx, config) {
|
|
|
2852
2857
|
if (req.method !== 'POST') return sendJson(res, 405, { error: 'method not allowed' })
|
|
2853
2858
|
try {
|
|
2854
2859
|
const payload = JSON.parse((await readBody(req)) || '{}')
|
|
2855
|
-
|
|
2856
|
-
|
|
2860
|
+
// Blank-only guard: a filesystem root is a legal workspace (issue
|
|
2861
|
+
// #60); the real directory probe below answers everything else.
|
|
2862
|
+
const rawPath = String(payload.path || '').trim()
|
|
2863
|
+
if (!rawPath) return sendJson(res, 400, { ok: false, error: 'path must be an absolute directory' })
|
|
2864
|
+
const p = normalizeRemotePath(rawPath)
|
|
2857
2865
|
if (!config.host) return sendJson(res, 400, { ok: false, error: 'no remote host configured/connected — connect first' })
|
|
2858
2866
|
// The picker names the machine it was browsing. Re-assert it so a
|
|
2859
2867
|
// switch made elsewhere between the listing and this commit cannot
|
package/lib/search.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Replaces `find … -exec grep` (which needs a POSIX shell and dies on
|
|
3
3
|
// Windows remotes). Walks the remote tree over SFTP readdir/stat/readFile with
|
|
4
4
|
// the same bounded-parallelism + ignore rules as the mirror sync.
|
|
5
|
-
import { joinRemotePath, shq } from './paths.js'
|
|
5
|
+
import { joinRemotePath, relPathUnder, shq } from './paths.js'
|
|
6
6
|
|
|
7
7
|
function mapLimit(items, limit, fn) {
|
|
8
8
|
const limitN = Math.max(1, Math.min(Math.floor(limit) || 1, items.length))
|
|
@@ -105,7 +105,12 @@ export async function searchTree(sftp, rootDir, opts = {}) {
|
|
|
105
105
|
if (shouldStop() || scanned >= maxFiles) return
|
|
106
106
|
if (depth <= 0) continue
|
|
107
107
|
const sub = joinRemotePath(dir, name)
|
|
108
|
-
|
|
108
|
+
// isIgnored receives the path RELATIVE to the search root (the same
|
|
109
|
+
// contract sync has), so root-anchored ignore rules — `/proc/` when the
|
|
110
|
+
// workspace is the filesystem root (issue #60), `Library/Caches/` in the
|
|
111
|
+
// default search set — actually match here; a bare basename would never
|
|
112
|
+
// satisfy an anchored pattern and those rules were silently dead.
|
|
113
|
+
if (isIgnored(relPathUnder(rootDir, sub) || name, true)) continue
|
|
109
114
|
await walk(sub, depth - 1)
|
|
110
115
|
}
|
|
111
116
|
if (shouldStop() || scanned >= maxFiles) return
|
|
@@ -118,8 +123,8 @@ export async function searchTree(sftp, rootDir, opts = {}) {
|
|
|
118
123
|
// Checked per entry: this is the loop that used to run for hours.
|
|
119
124
|
if (shouldStop() || scanned >= maxFiles || matches.length >= maxMatches) return
|
|
120
125
|
if (glob && !matchGlob(name, glob)) return
|
|
121
|
-
if (isIgnored(name, false)) return
|
|
122
126
|
const fp = joinRemotePath(dir, name)
|
|
127
|
+
if (isIgnored(relPathUnder(rootDir, fp) || name, false)) return
|
|
123
128
|
scanned++
|
|
124
129
|
let st
|
|
125
130
|
try { st = await sftp.stat(fp) } catch { return }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.48",
|
|
4
4
|
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 22 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, one-click remote dsh deploy + web UI attach, port forwarding, optional side-bar remote file editing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|
|
@@ -93,3 +93,4 @@
|
|
|
93
93
|
"test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=20000 --test-force-exit"
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
|