dsh-remote-vs 0.8.3 → 0.8.6
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 +10 -7
- package/README.md +7 -5
- package/lib/client.js +81 -17
- package/lib/index.js +5 -5
- package/lib/p2-router-fs.js +1 -1
- package/lib/types/changes/recorder.d.ts +2 -2
- package/lib/types/changes/status.d.ts +2 -2
- package/lib/types/client/remote-file-open-target.d.ts +4 -4
- package/lib/types/client/scm/actions.d.ts +19 -2
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/p2/worlds.d.ts +1 -1
- package/lib/types/search-ops.d.ts +19 -1
- package/lib/{worlds-PUumRIh7.js → worlds-C6W225o9.js} +1 -1
- package/package.json +1 -3
- package/src/changes/recorder.ts +2 -2
- package/src/changes/status.ts +2 -2
- package/src/client/RemoteDirectoryPicker.tsx +1 -1
- package/src/client/RemoteRoot.tsx +1 -1
- package/src/client/changes/ChangesDock.tsx +2 -2
- package/src/client/remote-file-open-target.ts +4 -4
- package/src/client/scm/actions.ts +42 -11
- package/src/client/store.tsx +1 -1
- package/src/p2/router-fs.ts +1 -1
- package/src/p2/worlds.ts +3 -3
- package/src/search-ops.ts +33 -3
package/README.en.md
CHANGED
|
@@ -178,11 +178,13 @@ dsh_remote_vs/
|
|
|
178
178
|
When a new helper is built, swap the 4-piece set (tarball + sha256 + helper.json + patch notes)
|
|
179
179
|
into vendor and commit (details live in the local-only `docs/MAINTENANCE.md` §4, which is gitignored and not distributed).
|
|
180
180
|
|
|
181
|
-
`.spike/` (gitignored
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`
|
|
185
|
-
|
|
181
|
+
`.spike/` and `scripts/` (both gitignored, **never distributed with the repository**) hold the
|
|
182
|
+
development-time verification and self-check scripts: `.spike/` has the live-host ones — `p2-router.ts`
|
|
183
|
+
main regression (real remote + real Cordis + real router classes) and probes like `grep-probe.mjs`
|
|
184
|
+
(build with `tsconfig.scratch.json`), plus `model-check.mjs` / `reveal-check.mjs` /
|
|
185
|
+
`fallback-check.mjs` / `client-load-check.mjs` / `tool-schema-check.mjs` which need no host at all;
|
|
186
|
+
`scripts/` has the plain-node regression gates (`node scripts/check-*.mjs`, with `verify-lib.mjs`
|
|
187
|
+
checking the build artifact). Both are covered under "Development".
|
|
186
188
|
|
|
187
189
|
## Remote workbench (VS Code style)
|
|
188
190
|
|
|
@@ -322,8 +324,9 @@ dsh plugin --profile web add link:<absolute path of this directory>
|
|
|
322
324
|
**How a change goes live: `pnpm run build`, then restart `dsh web` — for the host half and the client half alike.**
|
|
323
325
|
**`lib/` is committed.** The package has no `prepare` script (pnpm 12 refuses to run one for a git
|
|
324
326
|
dependency), so a `dsh plugin add github:…` user receives the repository as it is: **after editing
|
|
325
|
-
source you must `npm run build` and commit `lib/` with it
|
|
326
|
-
|
|
327
|
+
source you must `npm run build` and commit `lib/` with it** (`node scripts/verify-lib.mjs` checks that the
|
|
328
|
+
entries are present, committed and loadable; the self-check scripts live on the maintainer's machine only,
|
|
329
|
+
see below).
|
|
327
330
|
The Host reads plugin bundles into memory at startup (`initialBundleSnapshot`: it stats mtime/ctime/size into a
|
|
328
331
|
revision, then reads the bytes; the response carries `Cache-Control: public, max-age=31536000, immutable`), so a
|
|
329
332
|
**client-only** change (`src/client/**`) behaves exactly the same way: a browser refresh — `Ctrl+Shift+R`
|
package/README.md
CHANGED
|
@@ -169,10 +169,12 @@ dsh_remote_vs/
|
|
|
169
169
|
新版本 helper 编好后把 4 件套(tarball + sha256 + helper.json + 补丁说明)
|
|
170
170
|
换进 vendor 并提交即可(详细步骤在本地维护文档 `docs/MAINTENANCE.md` §4,该文件被 .gitignore,不随仓库分发)。
|
|
171
171
|
|
|
172
|
-
`.spike
|
|
173
|
-
|
|
172
|
+
`.spike/` 与 `scripts/`(都 gitignored,**不随仓库分发**)是开发期的验证与自检脚本:
|
|
173
|
+
`.spike/` 里是真机验证脚本 —— `p2-router.ts` 主回归(真远端 + 真 Cordis + 真路由类)+
|
|
174
|
+
`grep-probe.mjs` 等探针(构建见 `tsconfig.scratch.json`),以及不需要真机的
|
|
174
175
|
`model-check.mjs` / `reveal-check.mjs` / `fallback-check.mjs` / `client-load-check.mjs` /
|
|
175
|
-
`tool-schema-check.mjs
|
|
176
|
+
`tool-schema-check.mjs`;`scripts/` 里是纯 node 的回归闸门(`node scripts/check-*.mjs`,
|
|
177
|
+
其中 `verify-lib.mjs` 核对构建产物)。两者都见下文"开发"。
|
|
176
178
|
|
|
177
179
|
## 远端工作台(VS Code 式)
|
|
178
180
|
|
|
@@ -291,8 +293,8 @@ dsh plugin --profile web add link:<本目录绝对路径>
|
|
|
291
293
|
|
|
292
294
|
**改完怎么生效:`pnpm run build` 之后重启 `dsh web` —— host 半与客户端半都是。**
|
|
293
295
|
**`lib/` 是入库的**:本包没有 `prepare`(pnpm 12 会拦),别人 `dsh plugin add github:…` 拿到的是仓库内容,
|
|
294
|
-
所以**改完源码必须 `npm run build` 并把 `lib/`
|
|
295
|
-
|
|
296
|
+
所以**改完源码必须 `npm run build` 并把 `lib/` 一起提交**(`node scripts/verify-lib.mjs` 会核对入口齐全、
|
|
297
|
+
已入库、能加载;自检脚本只在本机,见下)。
|
|
296
298
|
Host 在启动时把插件包读进内存(`initialBundleSnapshot`:先记录 mtime/ctime/size 作为 rev,再读字节;
|
|
297
299
|
响应头 `Cache-Control: public, max-age=31536000, immutable`),所以**只改 `src/client/**` 也一样**:
|
|
298
300
|
浏览器刷新——包括 `Ctrl+Shift+R`——只会取回启动时抓下的那批字节,rev 不变。判定方法:
|
package/lib/client.js
CHANGED
|
@@ -823,7 +823,7 @@ window.__ModuleLoader__.load({
|
|
|
823
823
|
onKeyDown: (e) => {
|
|
824
824
|
if (e.key === "Enter") go();
|
|
825
825
|
},
|
|
826
|
-
placeholder: "/home/
|
|
826
|
+
placeholder: "/home/user/project",
|
|
827
827
|
spellCheck: false,
|
|
828
828
|
style: {
|
|
829
829
|
flex: 1,
|
|
@@ -37978,7 +37978,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
37978
37978
|
const primary = host.querySelectorAll("[class*=\"rvs-ck-\"]").length;
|
|
37979
37979
|
const fallback = host.querySelectorAll("span[class^=\"rvs-t-\"]").length;
|
|
37980
37980
|
const count = primary > 0 ? primary : fallback;
|
|
37981
|
-
console.info(`[dsh-remote-vs] build
|
|
37981
|
+
console.info(`[dsh-remote-vs] build 1022-1607 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
|
|
37982
37982
|
propsRef.current.onTokens?.(count);
|
|
37983
37983
|
};
|
|
37984
37984
|
const armFallback = () => {
|
|
@@ -39920,13 +39920,47 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
39920
39920
|
return clean;
|
|
39921
39921
|
}
|
|
39922
39922
|
/**
|
|
39923
|
-
*
|
|
39924
|
-
*
|
|
39923
|
+
* Directory names a NAME search never walks.
|
|
39924
|
+
*
|
|
39925
|
+
* The old `find` prune list plus one addition: `out` is build output (the
|
|
39926
|
+
* content search already prunes it), and on an AOSP tree with a populated `out/`
|
|
39927
|
+
* walking it would cost far more than the depth cap it replaces ever did.
|
|
39925
39928
|
*/
|
|
39926
|
-
|
|
39929
|
+
const NAME_PRUNE_DIRS = [
|
|
39930
|
+
".git",
|
|
39931
|
+
"node_modules",
|
|
39932
|
+
".venv",
|
|
39933
|
+
"dist",
|
|
39934
|
+
"build",
|
|
39935
|
+
"out"
|
|
39936
|
+
];
|
|
39937
|
+
/**
|
|
39938
|
+
* Name search under a remote directory: `rg` when the host has it, `find` when
|
|
39939
|
+
* it does not — decided BY THE HOST inside one command, so there is no probe
|
|
39940
|
+
* round trip and no per-alias cache to invalidate.
|
|
39941
|
+
*
|
|
39942
|
+
* Why the fallback exists at all: the previous implementation was `find` with a
|
|
39943
|
+
* hard `-maxdepth 6`, and that cap silently hid real files — in a monorepo of a
|
|
39944
|
+
* few million files, a source file ten levels down is simply invisible to it.
|
|
39945
|
+
* Measured on such a tree: `find -maxdepth 6` → 0 hits (2.0 s), `find -maxdepth
|
|
39946
|
+
* 12` → the hit, but 13.5 s, an uncapped `find` → does not finish at all, and
|
|
39947
|
+
* `rg --files --iglob` → the hit in ~4 s with no depth limit, because it walks
|
|
39948
|
+
* the tree once with a prefiltered glob instead of stat-ing every entry.
|
|
39949
|
+
*
|
|
39950
|
+
* Coverage: `--no-ignore --hidden` keeps what `find` did — it never consulted
|
|
39951
|
+
* .gitignore — so a generated or ignored file is still findable by name. The
|
|
39952
|
+
* fallback keeps a cap (raised from 6 to 12) because an uncapped walk is not
|
|
39953
|
+
* viable on a tree that size.
|
|
39954
|
+
*
|
|
39955
|
+
* The query is a GLOB fragment (as it always was): `*.java` still works as a
|
|
39956
|
+
* pattern, and `--iglob` matches case-insensitively like `find -iname`.
|
|
39957
|
+
*/
|
|
39958
|
+
async function searchFiles(alias, root, query, limit = 120, depth = 12) {
|
|
39927
39959
|
const clean = assertQuery$1(query);
|
|
39928
39960
|
const cap = Math.min(Math.max(Math.floor(limit), 1), 400);
|
|
39929
|
-
const
|
|
39961
|
+
const pattern = `*${clean}*`;
|
|
39962
|
+
const prunes = NAME_PRUNE_DIRS.map((name) => `-g ${shQuote(`!**/${name}/**`)}`).join(" ");
|
|
39963
|
+
const lines = (await tryRun(alias, `if command -v rg >/dev/null 2>&1; then ${`rg --files --no-ignore --hidden --iglob ${shQuote(pattern)} ${prunes} ${shQuote(root)} 2>/dev/null`}; else ${`find ${shQuote(root)} -maxdepth ${depth} \\( ${NAME_PRUNE_DIRS.map((name) => `-name ${shQuote(name)}`).join(" -o ")} \\) -prune -o \\( -type f -o -type d \\) -iname ${shQuote(pattern)} -printf '%p\\t%y\\n' 2>/dev/null`}; fi | head -n ${cap + 1}`, 9e4)).split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
39930
39964
|
const truncated = lines.length > cap;
|
|
39931
39965
|
const base = root.replace(/\/+$/, "");
|
|
39932
39966
|
const hits = [];
|
|
@@ -40356,7 +40390,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
40356
40390
|
* path decides between the bindings on it.
|
|
40357
40391
|
*
|
|
40358
40392
|
* One alias routinely carries SEVERAL bindings (the same host mounted at
|
|
40359
|
-
* `/
|
|
40393
|
+
* `/srv/one/…` and `/srv/two/…`), so an alias-only match hands
|
|
40360
40394
|
* back whichever was created first: the editor then opens the file the caller
|
|
40361
40395
|
* named while the sidebar, the tree and the workspace selector stay on the
|
|
40362
40396
|
* other workspace. The remote root the caller already holds (`summary.root`, a
|
|
@@ -40531,8 +40565,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
40531
40565
|
* `bindingForWorkspacePath` answers only for the workspace path ITSELF (exact
|
|
40532
40566
|
* alias-dir match). A session cwd is weaker evidence: the model cd'ed into a
|
|
40533
40567
|
* project sub-directory, and the host commits whichever spelling the session
|
|
40534
|
-
* ran in — the local alias (`C:\…\aliases
|
|
40535
|
-
* (`/
|
|
40568
|
+
* ran in — the local alias dir (`C:\…\aliases\<alias>\src`) or the REMOTE
|
|
40569
|
+
* path (`/srv/one/src`). Both are still inside exactly
|
|
40536
40570
|
* one binding, so the deepest containing alias dir / remote root decides.
|
|
40537
40571
|
*
|
|
40538
40572
|
* No fallback to "the first binding": a cwd that names no known binding answers
|
|
@@ -41170,7 +41204,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
41170
41204
|
* workspace showed the sidebar on another — the selector kept whatever
|
|
41171
41205
|
* localStorage held (or the FIRST binding, on a fresh profile), and a click in
|
|
41172
41206
|
* the change panel matched its binding by alias alone, which is ambiguous as
|
|
41173
|
-
* soon as one host carries two workspaces
|
|
41207
|
+
* soon as one host carries two workspaces under one alias).
|
|
41174
41208
|
*
|
|
41175
41209
|
* Resolution order, strongest evidence first (all of it inside
|
|
41176
41210
|
* `planWorkspaceFollow`, so the policy has exactly one home):
|
|
@@ -43037,8 +43071,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
43037
43071
|
}),
|
|
43038
43072
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
43039
43073
|
className: "rvs-build",
|
|
43040
|
-
title: `dsh-remote-vs build
|
|
43041
|
-
children: "
|
|
43074
|
+
title: `dsh-remote-vs build 1022-1607`,
|
|
43075
|
+
children: "1022-1607"
|
|
43042
43076
|
}),
|
|
43043
43077
|
tokenCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
43044
43078
|
className: "rvs-build",
|
|
@@ -44859,7 +44893,25 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
44859
44893
|
return trimmed;
|
|
44860
44894
|
}
|
|
44861
44895
|
/**
|
|
44862
|
-
* Recursive content search under one absolute remote directory
|
|
44896
|
+
* Recursive content search under one absolute remote directory: `rg` when the
|
|
44897
|
+
* host has it, `grep -r` when it does not.
|
|
44898
|
+
*
|
|
44899
|
+
* Measured on a monorepo of a few million files (two kernel source trees among
|
|
44900
|
+
* them): the `grep` form was still running when a 300 s timeout killed it, while
|
|
44901
|
+
* the same query scoped to one subtree answers in milliseconds — on a tree that
|
|
44902
|
+
* size the SCOPE matters more than the tool, but rg is the faster one, so it is
|
|
44903
|
+
* preferred, with the previous grep command kept verbatim as the fallback so
|
|
44904
|
+
* behaviour can never get worse than today.
|
|
44905
|
+
*
|
|
44906
|
+
* Two deliberate differences on the rg path:
|
|
44907
|
+
* - it honours .gitignore (grep never did), which is what keeps build outputs
|
|
44908
|
+
* and kernel artifacts out of an interactive search — the same default VS
|
|
44909
|
+
* Code ships, and the reason `--no-ignore` is NOT passed here;
|
|
44910
|
+
* - `--hidden` keeps dotfiles searchable (`.github/workflows/*.yml`), with
|
|
44911
|
+
* `.git` excluded explicitly so `--hidden` cannot walk a nested repository.
|
|
44912
|
+
*
|
|
44913
|
+
* The output SHAPE is identical either way (`path\0line:text`, verified byte for
|
|
44914
|
+
* byte against both tools on a real host), so `parseContentHits` is unchanged.
|
|
44863
44915
|
*
|
|
44864
44916
|
* `--null` puts a NUL between the path and the `line:text` half so a path
|
|
44865
44917
|
* containing `:` still parses; `cut` bounds each line and `head` bounds the
|
|
@@ -44869,16 +44921,28 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
44869
44921
|
const pattern = assertQuery(query);
|
|
44870
44922
|
const max = Math.min(Math.max(Math.floor(options.maxResults), 1), 2e3);
|
|
44871
44923
|
const lineChars = Math.min(Math.max(Math.floor(options.maxLineChars), 80), 4e3);
|
|
44872
|
-
|
|
44924
|
+
const prune = [...PRUNE_DIRS, ...options.prune ?? []];
|
|
44925
|
+
const grepParts = [
|
|
44873
44926
|
"grep -rIn --null --binary-files=without-match",
|
|
44874
|
-
...
|
|
44927
|
+
...prune.map((name) => `--exclude-dir=${shQuote(name)}`),
|
|
44875
44928
|
options.caseSensitive ? "" : "-i",
|
|
44876
44929
|
options.wholeWord ? "-w" : "",
|
|
44877
44930
|
options.regex ? "-E" : "-F",
|
|
44878
44931
|
`-e ${shQuote(pattern)}`,
|
|
44879
44932
|
"--",
|
|
44880
44933
|
shQuote(assertRemoteDir(dir))
|
|
44881
|
-
].filter((part) => part !== "")
|
|
44934
|
+
].filter((part) => part !== "");
|
|
44935
|
+
const rgParts = [
|
|
44936
|
+
"rg -n --no-heading --null --color=never --with-filename --hidden",
|
|
44937
|
+
...prune.map((name) => `-g ${shQuote(`!**/${name}/**`)}`),
|
|
44938
|
+
options.caseSensitive ? "" : "-i",
|
|
44939
|
+
options.wholeWord ? "-w" : "",
|
|
44940
|
+
options.regex ? "" : "-F",
|
|
44941
|
+
`-e ${shQuote(pattern)}`,
|
|
44942
|
+
shQuote(assertRemoteDir(dir))
|
|
44943
|
+
].filter((part) => part !== "");
|
|
44944
|
+
const grep = `${grepParts.join(" ")} 2>/dev/null`;
|
|
44945
|
+
return `if command -v rg >/dev/null 2>&1; then ${`${rgParts.join(" ")} 2>/dev/null`}; else ${grep}; fi | cut -c1-${lineChars} | head -n ${max + 1}`;
|
|
44882
44946
|
}
|
|
44883
44947
|
/**
|
|
44884
44948
|
* Parse the NUL-separated grep output (tolerating a fallback colon format).
|
|
@@ -60869,7 +60933,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
|
|
|
60869
60933
|
* Without this the panel opened on whatever localStorage held — and on a
|
|
60870
60934
|
* fresh profile on `workspaces[0]` — so a session working in one remote
|
|
60871
60935
|
* workspace showed another workspace's tree until the user corrected it by
|
|
60872
|
-
* hand (the reported bug: a
|
|
60936
|
+
* hand (the reported bug: a session browsing another workspace on one host). The host's own
|
|
60873
60937
|
* workspace membership for the on-screen session is the only sanctioned
|
|
60874
60938
|
* source (`useRemoteChatTarget`), and it is a soft read: while it answers
|
|
60875
60939
|
* nothing the current selection stands.
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as pushPipeline, c as require_lib, d as ensurePrivateDir, f as keysDir, i as probe, l as aliasRoot, o as seedHelperCache, p as storePath, r as helperAvailability, s as RemoteFiles, t as RemoteWorlds, u as dshHome } from "./worlds-
|
|
1
|
+
import { a as pushPipeline, c as require_lib, d as ensurePrivateDir, f as keysDir, i as probe, l as aliasRoot, o as seedHelperCache, p as storePath, r as helperAvailability, s as RemoteFiles, t as RemoteWorlds, u as dshHome } from "./worlds-C6W225o9.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeSync } from "node:fs";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -2529,8 +2529,8 @@ function readHeadCommand(root, paths, capBytes, totalCapBytes) {
|
|
|
2529
2529
|
/**
|
|
2530
2530
|
* Name every immediate subdirectory that is a git repository.
|
|
2531
2531
|
*
|
|
2532
|
-
* A session working directory is often a CONTAINER for repositories (
|
|
2533
|
-
*
|
|
2532
|
+
* A session working directory is often a CONTAINER for repositories (a root
|
|
2533
|
+
* that holds another repository inside it), and
|
|
2534
2534
|
* `git status` at the container answers 128. This is a one-level `ls`-style
|
|
2535
2535
|
* probe — bounded work, no `find` — so it can never become the expensive part
|
|
2536
2536
|
* of a scan.
|
|
@@ -3357,8 +3357,8 @@ var SessionChanges = class {
|
|
|
3357
3357
|
* index (1.4s measured on the reference monorepo).
|
|
3358
3358
|
*
|
|
3359
3359
|
* A session working directory is often a CONTAINER for repositories rather
|
|
3360
|
-
* than one itself (
|
|
3361
|
-
*
|
|
3360
|
+
* than one itself (a session root that holds another repository inside it),
|
|
3361
|
+
* and `git status` there answers exit 128 — "not a git
|
|
3362
3362
|
* repository (or any parent up to mount point /)". When that happens the
|
|
3363
3363
|
* ledger looks one level down once and adopts the repository it finds, which
|
|
3364
3364
|
* is the same idea the SCM panel's `repoSubdir` expresses by hand.
|
package/lib/p2-router-fs.js
CHANGED
|
@@ -148,8 +148,8 @@ export declare class SessionChanges {
|
|
|
148
148
|
* index (1.4s measured on the reference monorepo).
|
|
149
149
|
*
|
|
150
150
|
* A session working directory is often a CONTAINER for repositories rather
|
|
151
|
-
* than one itself (
|
|
152
|
-
*
|
|
151
|
+
* than one itself (a session root that holds another repository inside it),
|
|
152
|
+
* and `git status` there answers exit 128 — "not a git
|
|
153
153
|
* repository (or any parent up to mount point /)". When that happens the
|
|
154
154
|
* ledger looks one level down once and adopts the repository it finds, which
|
|
155
155
|
* is the same idea the SCM panel's `repoSubdir` expresses by hand.
|
|
@@ -175,8 +175,8 @@ export declare function readHeadCommand(root: string, paths: readonly string[],
|
|
|
175
175
|
/**
|
|
176
176
|
* Name every immediate subdirectory that is a git repository.
|
|
177
177
|
*
|
|
178
|
-
* A session working directory is often a CONTAINER for repositories (
|
|
179
|
-
*
|
|
178
|
+
* A session working directory is often a CONTAINER for repositories (a root
|
|
179
|
+
* that holds another repository inside it), and
|
|
180
180
|
* `git status` at the container answers 128. This is a one-level `ls`-style
|
|
181
181
|
* probe — bounded work, no `find` — so it can never become the expensive part
|
|
182
182
|
* of a scan.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* opens it through its own right-sidebar preview. That preview resolves the
|
|
7
7
|
* address path against the SESSION WORKSPACE ROOT. A model that quotes a path
|
|
8
8
|
* relative to its CURRENT directory (e.g. `_xfer/reports/x.md` while working
|
|
9
|
-
* in
|
|
9
|
+
* in `<workspace>/<inner-repo>`) therefore misses by one segment, and a
|
|
10
10
|
* remote session's workspace root only exists as an EMPTY local alias dir —
|
|
11
11
|
* the click dies with the host-side ENOENT the user sees.
|
|
12
12
|
*
|
|
@@ -67,7 +67,7 @@ export declare function bindingForWorkspacePath(table: RemoteOpenBindings, works
|
|
|
67
67
|
* path decides between the bindings on it.
|
|
68
68
|
*
|
|
69
69
|
* One alias routinely carries SEVERAL bindings (the same host mounted at
|
|
70
|
-
* `/
|
|
70
|
+
* `/srv/one/…` and `/srv/two/…`), so an alias-only match hands
|
|
71
71
|
* back whichever was created first: the editor then opens the file the caller
|
|
72
72
|
* named while the sidebar, the tree and the workspace selector stay on the
|
|
73
73
|
* other workspace. The remote root the caller already holds (`summary.root`, a
|
|
@@ -199,8 +199,8 @@ export declare function selectionIsSessionBinding(options: {
|
|
|
199
199
|
* `bindingForWorkspacePath` answers only for the workspace path ITSELF (exact
|
|
200
200
|
* alias-dir match). A session cwd is weaker evidence: the model cd'ed into a
|
|
201
201
|
* project sub-directory, and the host commits whichever spelling the session
|
|
202
|
-
* ran in — the local alias (`C:\…\aliases
|
|
203
|
-
* (`/
|
|
202
|
+
* ran in — the local alias dir (`C:\…\aliases\<alias>\src`) or the REMOTE
|
|
203
|
+
* path (`/srv/one/src`). Both are still inside exactly
|
|
204
204
|
* one binding, so the deepest containing alias dir / remote root decides.
|
|
205
205
|
*
|
|
206
206
|
* No fallback to "the first binding": a cwd that names no known binding answers
|
|
@@ -157,8 +157,25 @@ export interface SearchHit {
|
|
|
157
157
|
isDir: boolean;
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
|
-
* Name search under a remote directory
|
|
161
|
-
*
|
|
160
|
+
* Name search under a remote directory: `rg` when the host has it, `find` when
|
|
161
|
+
* it does not — decided BY THE HOST inside one command, so there is no probe
|
|
162
|
+
* round trip and no per-alias cache to invalidate.
|
|
163
|
+
*
|
|
164
|
+
* Why the fallback exists at all: the previous implementation was `find` with a
|
|
165
|
+
* hard `-maxdepth 6`, and that cap silently hid real files — in a monorepo of a
|
|
166
|
+
* few million files, a source file ten levels down is simply invisible to it.
|
|
167
|
+
* Measured on such a tree: `find -maxdepth 6` → 0 hits (2.0 s), `find -maxdepth
|
|
168
|
+
* 12` → the hit, but 13.5 s, an uncapped `find` → does not finish at all, and
|
|
169
|
+
* `rg --files --iglob` → the hit in ~4 s with no depth limit, because it walks
|
|
170
|
+
* the tree once with a prefiltered glob instead of stat-ing every entry.
|
|
171
|
+
*
|
|
172
|
+
* Coverage: `--no-ignore --hidden` keeps what `find` did — it never consulted
|
|
173
|
+
* .gitignore — so a generated or ignored file is still findable by name. The
|
|
174
|
+
* fallback keeps a cap (raised from 6 to 12) because an uncapped walk is not
|
|
175
|
+
* viable on a tree that size.
|
|
176
|
+
*
|
|
177
|
+
* The query is a GLOB fragment (as it always was): `*.java` still works as a
|
|
178
|
+
* pattern, and `--iglob` matches case-insensitively like `find -iname`.
|
|
162
179
|
*/
|
|
163
180
|
export declare function searchFiles(alias: string, root: string, query: string, limit?: number, depth?: number): Promise<{
|
|
164
181
|
hits: SearchHit[];
|
|
@@ -222,7 +222,7 @@ export declare function setFollowSession(sessionId: string | undefined): void;
|
|
|
222
222
|
* workspace showed the sidebar on another — the selector kept whatever
|
|
223
223
|
* localStorage held (or the FIRST binding, on a fresh profile), and a click in
|
|
224
224
|
* the change panel matched its binding by alias alone, which is ambiguous as
|
|
225
|
-
* soon as one host carries two workspaces
|
|
225
|
+
* soon as one host carries two workspaces under one alias).
|
|
226
226
|
*
|
|
227
227
|
* Resolution order, strongest evidence first (all of it inside
|
|
228
228
|
* `planWorkspaceFollow`, so the policy has exactly one home):
|
package/lib/types/p2/worlds.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export declare class RemoteWorlds {
|
|
|
103
103
|
* service canonicalizes through it), and the remote fs view answers the remote
|
|
104
104
|
* absolute path. That is the right spelling for display and for remote
|
|
105
105
|
* processes, but it is not always a spelling the router can route BACK: two
|
|
106
|
-
* bindings may share one remote root (
|
|
106
|
+
* bindings may share one remote root (two aliases both mounted at `/home`), and
|
|
107
107
|
* `/home` is then deliberately ambiguous and resolves local. A spawned child
|
|
108
108
|
* session inherits exactly that committed spelling — so every subagent of a
|
|
109
109
|
* remote session in one of those workspaces silently ran on the Host (and a
|
|
@@ -16,7 +16,25 @@ export declare const DEFAULT_CONTENT_OPTIONS: ContentSearchOptions;
|
|
|
16
16
|
/** Assert a search query (printable, bounded, no NUL/newline). */
|
|
17
17
|
export declare function assertQuery(query: string): string;
|
|
18
18
|
/**
|
|
19
|
-
* Recursive content search under one absolute remote directory
|
|
19
|
+
* Recursive content search under one absolute remote directory: `rg` when the
|
|
20
|
+
* host has it, `grep -r` when it does not.
|
|
21
|
+
*
|
|
22
|
+
* Measured on a monorepo of a few million files (two kernel source trees among
|
|
23
|
+
* them): the `grep` form was still running when a 300 s timeout killed it, while
|
|
24
|
+
* the same query scoped to one subtree answers in milliseconds — on a tree that
|
|
25
|
+
* size the SCOPE matters more than the tool, but rg is the faster one, so it is
|
|
26
|
+
* preferred, with the previous grep command kept verbatim as the fallback so
|
|
27
|
+
* behaviour can never get worse than today.
|
|
28
|
+
*
|
|
29
|
+
* Two deliberate differences on the rg path:
|
|
30
|
+
* - it honours .gitignore (grep never did), which is what keeps build outputs
|
|
31
|
+
* and kernel artifacts out of an interactive search — the same default VS
|
|
32
|
+
* Code ships, and the reason `--no-ignore` is NOT passed here;
|
|
33
|
+
* - `--hidden` keeps dotfiles searchable (`.github/workflows/*.yml`), with
|
|
34
|
+
* `.git` excluded explicitly so `--hidden` cannot walk a nested repository.
|
|
35
|
+
*
|
|
36
|
+
* The output SHAPE is identical either way (`path\0line:text`, verified byte for
|
|
37
|
+
* byte against both tools on a real host), so `parseContentHits` is unchanged.
|
|
20
38
|
*
|
|
21
39
|
* `--null` puts a NUL between the path and the `line:text` half so a path
|
|
22
40
|
* containing `:` still parses; `cut` bounds each line and `head` bounds the
|
|
@@ -18432,7 +18432,7 @@ var RemoteWorlds = class {
|
|
|
18432
18432
|
* service canonicalizes through it), and the remote fs view answers the remote
|
|
18433
18433
|
* absolute path. That is the right spelling for display and for remote
|
|
18434
18434
|
* processes, but it is not always a spelling the router can route BACK: two
|
|
18435
|
-
* bindings may share one remote root (
|
|
18435
|
+
* bindings may share one remote root (two aliases both mounted at `/home`), and
|
|
18436
18436
|
* `/home` is then deliberately ambiguous and resolves local. A spawned child
|
|
18437
18437
|
* session inherits exactly that committed spelling — so every subagent of a
|
|
18438
18438
|
* remote session in one of those workspaces silently ran on the Host (and a
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-vs",
|
|
3
3
|
"description": "VSCode-style remote development for DSH: SSH host manager (key/password), helper deploy pipeline with version check, remote file explorer + editor + terminal in dsh-better-sidebar.",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.6",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -182,8 +182,6 @@
|
|
|
182
182
|
},
|
|
183
183
|
"scripts": {
|
|
184
184
|
"build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
|
|
185
|
-
"verify:lib": "node scripts/verify-lib.mjs",
|
|
186
|
-
"check": "node scripts/verify-lib.mjs && node scripts/check-changes.mjs && node scripts/check-ledger.mjs && node scripts/check-status.mjs && node scripts/check-text-diff.mjs && node scripts/check-workspace-follow.mjs && node scripts/check-scm-stage.mjs",
|
|
187
185
|
"bundle": "tsdown",
|
|
188
186
|
"watch": "tsdown --watch",
|
|
189
187
|
"typecheck": "tsc --noEmit"
|
package/src/changes/recorder.ts
CHANGED
|
@@ -600,8 +600,8 @@ export class SessionChanges {
|
|
|
600
600
|
* index (1.4s measured on the reference monorepo).
|
|
601
601
|
*
|
|
602
602
|
* A session working directory is often a CONTAINER for repositories rather
|
|
603
|
-
* than one itself (
|
|
604
|
-
*
|
|
603
|
+
* than one itself (a session root that holds another repository inside it),
|
|
604
|
+
* and `git status` there answers exit 128 — "not a git
|
|
605
605
|
* repository (or any parent up to mount point /)". When that happens the
|
|
606
606
|
* ledger looks one level down once and adopts the repository it finds, which
|
|
607
607
|
* is the same idea the SCM panel's `repoSubdir` expresses by hand.
|
package/src/changes/status.ts
CHANGED
|
@@ -367,8 +367,8 @@ export function readHeadCommand(
|
|
|
367
367
|
/**
|
|
368
368
|
* Name every immediate subdirectory that is a git repository.
|
|
369
369
|
*
|
|
370
|
-
* A session working directory is often a CONTAINER for repositories (
|
|
371
|
-
*
|
|
370
|
+
* A session working directory is often a CONTAINER for repositories (a root
|
|
371
|
+
* that holds another repository inside it), and
|
|
372
372
|
* `git status` at the container answers 128. This is a one-level `ls`-style
|
|
373
373
|
* probe — bounded work, no `find` — so it can never become the expensive part
|
|
374
374
|
* of a scan.
|
|
@@ -108,7 +108,7 @@ export function RemoteDirectoryPicker(props: {
|
|
|
108
108
|
onKeyDown={(e) => {
|
|
109
109
|
if (e.key === 'Enter') go()
|
|
110
110
|
}}
|
|
111
|
-
placeholder="/home/
|
|
111
|
+
placeholder="/home/user/project"
|
|
112
112
|
spellCheck={false}
|
|
113
113
|
style={{ flex: 1, background: 'rgba(127,127,127,.12)', color: 'inherit', border: '1px solid rgba(127,127,127,.35)', borderRadius: 6, padding: '4px 8px', fontSize: 13, fontFamily: 'inherit', outline: 'none' }}
|
|
114
114
|
/>
|
|
@@ -133,7 +133,7 @@ function Shell(props: { ctx: Context; visible: boolean }): ReactNode {
|
|
|
133
133
|
* Without this the panel opened on whatever localStorage held — and on a
|
|
134
134
|
* fresh profile on `workspaces[0]` — so a session working in one remote
|
|
135
135
|
* workspace showed another workspace's tree until the user corrected it by
|
|
136
|
-
* hand (the reported bug: a
|
|
136
|
+
* hand (the reported bug: a session browsing another workspace on one host). The host's own
|
|
137
137
|
* workspace membership for the on-screen session is the only sanctioned
|
|
138
138
|
* source (`useRemoteChatTarget`), and it is a soft read: while it answers
|
|
139
139
|
* nothing the current selection stands.
|
|
@@ -234,8 +234,8 @@ export function ChangesDock(props: ChangesDockProps): React.ReactNode {
|
|
|
234
234
|
// sees what the load wrote.
|
|
235
235
|
//
|
|
236
236
|
// Resolved by the session's REMOTE root, never by the alias alone: one
|
|
237
|
-
// alias carries several bindings (
|
|
238
|
-
//
|
|
237
|
+
// alias carries several bindings (two workspaces can live on one alias),
|
|
238
|
+
// and the alias-only form picked whichever came first —
|
|
239
239
|
// the file opened while the tree and the selector stayed on the other
|
|
240
240
|
// workspace. That was the reported bug.
|
|
241
241
|
let binding = bindingForChangedFile(remoteOpenBindings(), summary, file.path)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* opens it through its own right-sidebar preview. That preview resolves the
|
|
7
7
|
* address path against the SESSION WORKSPACE ROOT. A model that quotes a path
|
|
8
8
|
* relative to its CURRENT directory (e.g. `_xfer/reports/x.md` while working
|
|
9
|
-
* in
|
|
9
|
+
* in `<workspace>/<inner-repo>`) therefore misses by one segment, and a
|
|
10
10
|
* remote session's workspace root only exists as an EMPTY local alias dir —
|
|
11
11
|
* the click dies with the host-side ENOENT the user sees.
|
|
12
12
|
*
|
|
@@ -181,7 +181,7 @@ function isRemoteAbsolute(path: string | undefined): path is string {
|
|
|
181
181
|
* path decides between the bindings on it.
|
|
182
182
|
*
|
|
183
183
|
* One alias routinely carries SEVERAL bindings (the same host mounted at
|
|
184
|
-
* `/
|
|
184
|
+
* `/srv/one/…` and `/srv/two/…`), so an alias-only match hands
|
|
185
185
|
* back whichever was created first: the editor then opens the file the caller
|
|
186
186
|
* named while the sidebar, the tree and the workspace selector stay on the
|
|
187
187
|
* other workspace. The remote root the caller already holds (`summary.root`, a
|
|
@@ -396,8 +396,8 @@ function bindingForHostPath(
|
|
|
396
396
|
* `bindingForWorkspacePath` answers only for the workspace path ITSELF (exact
|
|
397
397
|
* alias-dir match). A session cwd is weaker evidence: the model cd'ed into a
|
|
398
398
|
* project sub-directory, and the host commits whichever spelling the session
|
|
399
|
-
* ran in — the local alias (`C:\…\aliases
|
|
400
|
-
* (`/
|
|
399
|
+
* ran in — the local alias dir (`C:\…\aliases\<alias>\src`) or the REMOTE
|
|
400
|
+
* path (`/srv/one/src`). Both are still inside exactly
|
|
401
401
|
* one binding, so the deepest containing alias dir / remote root decides.
|
|
402
402
|
*
|
|
403
403
|
* No fallback to "the first binding": a cwd that names no known binding answers
|
|
@@ -411,26 +411,57 @@ function assertQuery(query: string): string {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
414
|
+
* Directory names a NAME search never walks.
|
|
415
|
+
*
|
|
416
|
+
* The old `find` prune list plus one addition: `out` is build output (the
|
|
417
|
+
* content search already prunes it), and on an AOSP tree with a populated `out/`
|
|
418
|
+
* walking it would cost far more than the depth cap it replaces ever did.
|
|
419
|
+
*/
|
|
420
|
+
const NAME_PRUNE_DIRS = ['.git', 'node_modules', '.venv', 'dist', 'build', 'out']
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Name search under a remote directory: `rg` when the host has it, `find` when
|
|
424
|
+
* it does not — decided BY THE HOST inside one command, so there is no probe
|
|
425
|
+
* round trip and no per-alias cache to invalidate.
|
|
426
|
+
*
|
|
427
|
+
* Why the fallback exists at all: the previous implementation was `find` with a
|
|
428
|
+
* hard `-maxdepth 6`, and that cap silently hid real files — in a monorepo of a
|
|
429
|
+
* few million files, a source file ten levels down is simply invisible to it.
|
|
430
|
+
* Measured on such a tree: `find -maxdepth 6` → 0 hits (2.0 s), `find -maxdepth
|
|
431
|
+
* 12` → the hit, but 13.5 s, an uncapped `find` → does not finish at all, and
|
|
432
|
+
* `rg --files --iglob` → the hit in ~4 s with no depth limit, because it walks
|
|
433
|
+
* the tree once with a prefiltered glob instead of stat-ing every entry.
|
|
434
|
+
*
|
|
435
|
+
* Coverage: `--no-ignore --hidden` keeps what `find` did — it never consulted
|
|
436
|
+
* .gitignore — so a generated or ignored file is still findable by name. The
|
|
437
|
+
* fallback keeps a cap (raised from 6 to 12) because an uncapped walk is not
|
|
438
|
+
* viable on a tree that size.
|
|
439
|
+
*
|
|
440
|
+
* The query is a GLOB fragment (as it always was): `*.java` still works as a
|
|
441
|
+
* pattern, and `--iglob` matches case-insensitively like `find -iname`.
|
|
416
442
|
*/
|
|
417
443
|
export async function searchFiles(
|
|
418
444
|
alias: string,
|
|
419
445
|
root: string,
|
|
420
446
|
query: string,
|
|
421
447
|
limit = 120,
|
|
422
|
-
depth =
|
|
448
|
+
depth = 12,
|
|
423
449
|
): Promise<{ hits: SearchHit[]; truncated: boolean }> {
|
|
424
450
|
const clean = assertQuery(query)
|
|
425
451
|
const cap = Math.min(Math.max(Math.floor(limit), 1), 400)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
452
|
+
const pattern = `*${clean}*`
|
|
453
|
+
const prunes = NAME_PRUNE_DIRS.map(name => `-g ${shQuote(`!**/${name}/**`)}`).join(' ')
|
|
454
|
+
const rg = `rg --files --no-ignore --hidden --iglob ${shQuote(pattern)} ${prunes} ${shQuote(root)} 2>/dev/null`
|
|
455
|
+
// `-printf '%p\t%y'` carries the entry type, because `find -print` never emits
|
|
456
|
+
// a trailing slash for directories — the old `line.endsWith('/')` test could
|
|
457
|
+
// therefore never be true and every hit was reported as a file (so a directory
|
|
458
|
+
// hit was routed into openFile). GNU/BusyBox find support -printf; the
|
|
459
|
+
// content-search builder already depends on GNU grep flags. rg's `--files`
|
|
460
|
+
// prints bare paths (files only), which the parser below already tolerates.
|
|
461
|
+
const find = `find ${shQuote(root)} -maxdepth ${depth} `
|
|
462
|
+
+ `\\( ${NAME_PRUNE_DIRS.map(name => `-name ${shQuote(name)}`).join(' -o ')} \\) -prune -o `
|
|
463
|
+
+ `\\( -type f -o -type d \\) -iname ${shQuote(pattern)} -printf '%p\\t%y\\n' 2>/dev/null`
|
|
464
|
+
const command = `if command -v rg >/dev/null 2>&1; then ${rg}; else ${find}; fi | head -n ${cap + 1}`
|
|
434
465
|
const out = await tryRun(alias, command, 90_000)
|
|
435
466
|
const lines = out.split('\n').map(line => line.trim()).filter(line => line !== '')
|
|
436
467
|
const truncated = lines.length > cap
|
package/src/client/store.tsx
CHANGED
|
@@ -761,7 +761,7 @@ let followFlight: { sessionId: string; promise: Promise<string | undefined> } |
|
|
|
761
761
|
* workspace showed the sidebar on another — the selector kept whatever
|
|
762
762
|
* localStorage held (or the FIRST binding, on a fresh profile), and a click in
|
|
763
763
|
* the change panel matched its binding by alias alone, which is ambiguous as
|
|
764
|
-
* soon as one host carries two workspaces
|
|
764
|
+
* soon as one host carries two workspaces under one alias).
|
|
765
765
|
*
|
|
766
766
|
* Resolution order, strongest evidence first (all of it inside
|
|
767
767
|
* `planWorkspaceFollow`, so the policy has exactly one home):
|
package/src/p2/router-fs.ts
CHANGED
|
@@ -189,7 +189,7 @@ export default class TransparentFileSystem extends FileSystem {
|
|
|
189
189
|
// for remote processes), but that spelling is not guaranteed to route BACK:
|
|
190
190
|
// a session path derived from it (working-directory.set commits THIS value)
|
|
191
191
|
// would resolve local whenever two bindings share the remote root — that is
|
|
192
|
-
// how subagents of
|
|
192
|
+
// how subagents of a session bound to another host ended up running on the Host.
|
|
193
193
|
// toSessionPath keeps the remote spelling where it is unambiguous and uses
|
|
194
194
|
// the binding's unique alias spelling only where it is not.
|
|
195
195
|
return this.remote.toSessionPath(world, fs.processPath(target))
|
package/src/p2/worlds.ts
CHANGED
|
@@ -173,8 +173,8 @@ export class RemoteWorlds {
|
|
|
173
173
|
if (byRemotePath.length === 0) return undefined
|
|
174
174
|
const longest = Math.max(...byRemotePath.map(candidate => candidate.length))
|
|
175
175
|
const top = byRemotePath.filter(candidate => candidate.length === longest)
|
|
176
|
-
// Two bindings can share one remote directory — here
|
|
177
|
-
//
|
|
176
|
+
// Two bindings can share one remote directory — here two aliases both
|
|
177
|
+
// mounted at `/home` do. Picking either would send a write to the wrong host,
|
|
178
178
|
// and a wrong-host write is silent and destructive in a way a local one is
|
|
179
179
|
// not, so an ambiguous remote-form cwd stays local instead of guessing.
|
|
180
180
|
//
|
|
@@ -276,7 +276,7 @@ export class RemoteWorlds {
|
|
|
276
276
|
* service canonicalizes through it), and the remote fs view answers the remote
|
|
277
277
|
* absolute path. That is the right spelling for display and for remote
|
|
278
278
|
* processes, but it is not always a spelling the router can route BACK: two
|
|
279
|
-
* bindings may share one remote root (
|
|
279
|
+
* bindings may share one remote root (two aliases both mounted at `/home`), and
|
|
280
280
|
* `/home` is then deliberately ambiguous and resolves local. A spawned child
|
|
281
281
|
* session inherits exactly that committed spelling — so every subagent of a
|
|
282
282
|
* remote session in one of those workspaces silently ran on the Host (and a
|
package/src/search-ops.ts
CHANGED
|
@@ -61,7 +61,25 @@ export function assertQuery(query: string): string {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Recursive content search under one absolute remote directory
|
|
64
|
+
* Recursive content search under one absolute remote directory: `rg` when the
|
|
65
|
+
* host has it, `grep -r` when it does not.
|
|
66
|
+
*
|
|
67
|
+
* Measured on a monorepo of a few million files (two kernel source trees among
|
|
68
|
+
* them): the `grep` form was still running when a 300 s timeout killed it, while
|
|
69
|
+
* the same query scoped to one subtree answers in milliseconds — on a tree that
|
|
70
|
+
* size the SCOPE matters more than the tool, but rg is the faster one, so it is
|
|
71
|
+
* preferred, with the previous grep command kept verbatim as the fallback so
|
|
72
|
+
* behaviour can never get worse than today.
|
|
73
|
+
*
|
|
74
|
+
* Two deliberate differences on the rg path:
|
|
75
|
+
* - it honours .gitignore (grep never did), which is what keeps build outputs
|
|
76
|
+
* and kernel artifacts out of an interactive search — the same default VS
|
|
77
|
+
* Code ships, and the reason `--no-ignore` is NOT passed here;
|
|
78
|
+
* - `--hidden` keeps dotfiles searchable (`.github/workflows/*.yml`), with
|
|
79
|
+
* `.git` excluded explicitly so `--hidden` cannot walk a nested repository.
|
|
80
|
+
*
|
|
81
|
+
* The output SHAPE is identical either way (`path\0line:text`, verified byte for
|
|
82
|
+
* byte against both tools on a real host), so `parseContentHits` is unchanged.
|
|
65
83
|
*
|
|
66
84
|
* `--null` puts a NUL between the path and the `line:text` half so a path
|
|
67
85
|
* containing `:` still parses; `cut` bounds each line and `head` bounds the
|
|
@@ -72,7 +90,7 @@ export function contentSearchCommand(dir: string, query: string, options: Conten
|
|
|
72
90
|
const max = Math.min(Math.max(Math.floor(options.maxResults), 1), 2000)
|
|
73
91
|
const lineChars = Math.min(Math.max(Math.floor(options.maxLineChars), 80), 4000)
|
|
74
92
|
const prune = [...PRUNE_DIRS, ...(options.prune ?? [])]
|
|
75
|
-
const
|
|
93
|
+
const grepParts = [
|
|
76
94
|
'grep -rIn --null --binary-files=without-match',
|
|
77
95
|
...prune.map(name => `--exclude-dir=${shQuote(name)}`),
|
|
78
96
|
options.caseSensitive ? '' : '-i',
|
|
@@ -82,7 +100,19 @@ export function contentSearchCommand(dir: string, query: string, options: Conten
|
|
|
82
100
|
'--',
|
|
83
101
|
shQuote(assertRemoteDir(dir)),
|
|
84
102
|
].filter(part => part !== '')
|
|
85
|
-
|
|
103
|
+
const rgParts = [
|
|
104
|
+
'rg -n --no-heading --null --color=never --with-filename --hidden',
|
|
105
|
+
...prune.map(name => `-g ${shQuote(`!**/${name}/**`)}`),
|
|
106
|
+
options.caseSensitive ? '' : '-i',
|
|
107
|
+
options.wholeWord ? '-w' : '',
|
|
108
|
+
// rg is regex by default, so literal mode is the one that needs a flag.
|
|
109
|
+
options.regex ? '' : '-F',
|
|
110
|
+
`-e ${shQuote(pattern)}`,
|
|
111
|
+
shQuote(assertRemoteDir(dir)),
|
|
112
|
+
].filter(part => part !== '')
|
|
113
|
+
const grep = `${grepParts.join(' ')} 2>/dev/null`
|
|
114
|
+
const rg = `${rgParts.join(' ')} 2>/dev/null`
|
|
115
|
+
return `if command -v rg >/dev/null 2>&1; then ${rg}; else ${grep}; fi | cut -c1-${lineChars} | head -n ${max + 1}`
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
/** One content-search hit. */
|