feat(cli): bdrive grep — find the file by what's inside it (BEA-99) - #136
Open
ssowonny wants to merge 1 commit into
Open
feat(cli): bdrive grep — find the file by what's inside it (BEA-99)#136ssowonny wants to merge 1 commit into
ssowonny wants to merge 1 commit into
Conversation
…yncs (BEA-99) The ⌘K palette searches file names, projects and actions; nothing in the product searched file contents. Three personas independently typed a phrase that lives inside a synced file and got "No matches". `bdrive grep <pattern> [folder]` searches the working folder — RE2 or -F literal, -i, -l, -n (default 200, 0 = all), output `path:line: text`, exit 0 on match and 1 on none. It searches exactly what the project syncs, via a new syncer.SyncedFiles that wraps the existing walkFolder: the one copy of the sync predicate, so an ignore rule or a narrowed scope excludes a file from search the same way it excludes it from sync, and .bdrive/ state can never surface. Not Explain, which countFiles every pruned dir — a grep in a repo with node_modules/ would walk it in full for a count it discards. A read stays a read: LoadProject, not ResolveMount (no registry self-heal, no device enrollment), no session, no flock, and the volume store is opened for IgnoreAccepted only when it already exists, so a search creates nothing. Both the path and the matched line go through safeField — a matched line is a teammate's file content, the widest version of the surface that function exists for. The hub-side content index stays deliberately unbuilt; the issue records its cost. ROADMAP's "Search across the hub" line is reworded rather than removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
bdrive grep 'retention.*fold'. Until now typing a phrase that's literally in a file got you "No matches", which three personas hit independently.bdrive scopehides a file from search the same way it hides it from the hub.What shipped, and what didn't
The issue split one want into two builds. Only the left one is in this PR.
flowchart TB Q["find the file by what is inside it"] CLI["<div style='text-align:left'><b>bdrive grep</b> (CLI)<br/>walks the working folder<br/>reuses the sync predicate<br/>no index, no hub work<br/><b>THIS PR</b></div>"] HUB["<div style='text-align:left'><b>content index</b> (hub)<br/>nothing searches content<br/>MetaStore excludes blobs<br/>N GETs + N sha256 per query<br/><b>not this milestone</b></div>"] Q --> CLI Q --> HUB classDef built fill:#22c55e22,stroke:#22c55e,stroke-width:2px classDef deferred fill:#88888822,stroke:#888888,stroke-dasharray:4 3 class CLI built class HUB deferredThe hub half stays gated
no, and the issue keeps its cost record: seam fit 1/5, and on any S3/GCS hubverify()re-hashes a blob on every read, so a server-side content search is N GETs plus N full sha256 passes per query — 5k files × 20 KB is ~100 MB egressed and hashed, serialized, for one keystroke. Two landmines are recorded there too (a cross-project search box would be the first handler to re-derive membership itself instead of usingproj(); match counts across aPermNoneprefix are a content oracle, while snippets from readable files are fine). Don't let a "small"/api/p/<id>/searchsneak in later without reading that.The command
<pattern> [folder]folderresolves likebdrive log's-i, --ignore-case-F, --fixed-strings-l, --files-with-matches-n, --limitExit status is 0 on match and 1 on no match, with nothing printed — grep's convention, so
bdrive grep x || echo nonecomposes. Real errors (bad pattern, not a project) still print and still exit non-zero.-nmeans "max rows out", the same asbdrive log -n. That collision with GNU grep's--max-countis deliberate consistency within this CLI, not an oversight.Three things that make it correct rather than just working
It searches the sync set, not the folder.
syncer.SyncedFilesis a ~15-line wrapper over the existingwalkFolder— the single copy of the sync predicate that scan,ExplainandMeasureall go through. That's what gets.bdrive/,.bdrive-tmp-*, nested mounts,journal.SafePathand teammate-negation rules right for free; a hand-rolled walk incmd/bdriveis how three of the acceptance criteria would have quietly failed. It's deliberately notExplain, which already returns this list but alsocountFilesevery pruned directory — a grep in a repo withnode_modules/would walk it in full for a count it discards.A read stays a read.
config.LoadProject, neverResolveMount, becauseResolveMountself-heals the registry path — i.e. it enrolls this device — and a search must not have that side effect. Same rulelogReadsfollows. No session, no flock, so it can't block behind a daemon mid-cycle (there's a test that takes the volume flock and asserts grep still answers).Output is treated as hostile. Every matched line is content a teammate wrote and synced — the same trust level as a peer's journal strings, over a strictly wider surface. Both the path and the matched text go through
safeField, so a lone CR can't repaint the row, U+202E can't reverse it, and OSC 52 can't write your clipboard.Deviations from the reviewed plan
Two, both small, both flagged rather than slipped in:
Statguard before opening the volume store. The plan copiedscope --explain's best-effortstore.OpenforIgnoreAccepted. Butstore.OpenMkdirAlls the volume directory, so a search on a never-synced project created one — a write from a read-only command. One line: open it only if it already exists. No store means no accepted rules, which is what""already says. (scope --explainstill has the original behavior; not touched here.)DialogTitleretitle. The spec noted the sr-only title still reads "Search and quick actions" while the visible placeholder is accurate, and said to fix it "if you are in the file anyway". Doing it would pull annpm run buildand a committedstatic/diff into an otherwise CLI-only PR. Left for whoever next touches the frontend.Verification
go test ./...— all packages pass.internal/syncer/syncedfiles_test.go(sync set matchesExplain; pruned dirs are not descended; unaccepted teammate!rules don't widen the set) andcmd/bdrive/grep_test.go(output format, every flag, the ignore rule hiding a previously-matching file,.bdrive/and.bdrive-tmp-*never appearing, binary skip via a NUL in the first 8 KB, ESC/CR/U+202E stripped from both path and text, no registry write outside a project, exit codes,-nlimit and-n 0, and no blocking under the volume flock).cat -vcheck on planted terminal-escape content.internal/webappor the frontend changed.Architecture changes
internal/syncergains one exported function andcmd/bdriveone command, soarchitecture/cli-sync.mdis the only affected diagram (committed on the branch).overview.mddoesn't change — no new package, no new wiring.SyncedFilesis new: a filter-aware pure read over the samewalkFolderpredicate asExplainandMeasure, with its own freshFilter;Commandsgainsgrep, which is its only caller.flowchart TB walkFolder["<div style='text-align:left'><b>walkFolder</b><br/>+walkFolder(folder, filter, fn)<br/>verdict: vSync vSkipFile vDescend vPruneDir vNested</div>"] Measure["<div style='text-align:left'><b>Measure</b><br/>+Measure(folder, include) files, bytes</div>"] SyncedFiles["<div style='text-align:left'><b>SyncedFiles</b><br/>+SyncedFiles(folder, include, accepted) paths</div>"] Explain["<div style='text-align:left'><b>Explain</b><br/>+Explain(folder, include, accepted) two lists<br/>+NotSyncedFiles(entries) int</div>"] Filter["Filter"] Commands["<div style='text-align:left'><b>Commands</b><br/>init login logout<br/>sync stop scope <span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ grep</span> forget status log<br/>restore url share export import</div>"] Why["Not Explain: Explain countFiles every pruned dir,<br/>so a grep in a repo with node_modules/<br/>would walk it in full for a count it discards"] Explain --> walkFolder Measure --> walkFolder SyncedFiles -- "<span style='background:#22c55e55;padding:0 5px;border-radius:3px'>✅ same predicate</span>" --> walkFolder Explain --> Filter Measure --> Filter SyncedFiles -- "<span style='background:#22c55e55;padding:0 5px;border-radius:3px'>✅ own fresh instance</span>" --> Filter walkFolder -- "SkipUp / PruneDir / addNestedMount" --> Filter Commands -- "<span style='background:#22c55e55;padding:0 5px;border-radius:3px'>✅ bdrive grep</span>" --> SyncedFiles SyncedFiles -.- Why classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2 class SyncedFiles added class Why noteBox linkStyle 2 stroke:#22c55e,stroke-width:2px linkStyle 5 stroke:#22c55e,stroke-width:2px linkStyle 7 stroke:#22c55e,stroke-width:2pxDocs
README.mdandweb/docs/.../reference/cli.mddocument the command, its flags and the output format; the docs page gets a worked example plus a note that this is local, not hub-wide.ROADMAP.md's "Search across the hub" line is reworded, not deleted — content search now works locally, but the hub half is still exploring, and the public file shouldn't imply either that nothing exists or that the hub half shipped.The open question this ships to answer
Priya and Mira live in the browser; this is a terminal command. If they don't reach for it, the XL hub version was the only version that ever mattered — and that's worth knowing for the price of an M.
Build session
(Only works on the machine that ran the build.)