fix(cli): skip doc extraction in ensureCheckout; fix symlinks in cpDirAtomic#96
Merged
fix(cli): skip doc extraction in ensureCheckout; fix symlinks in cpDirAtomic#96
Conversation
… in cpDirAtomic Fixes `ask src github:gitbutlerapp/gitbutler` failing with "No docs directory found" and a latent ENOENT on subsequent cache hits. Bug 1 — GithubSource.fetch always called extractDocsFromDir, which is correct for runInstall but wrong for ask src / ask docs (only the checkout path is needed). Repos without a conventional docs/ directory threw through both the shallow-clone and tar.gz paths. Added `skipDocExtraction?: boolean` to GithubSourceOptions; three call sites in github.ts now skip extraction when the flag is set, returning files: [] with the populated storePath. ensureCheckout sets skipDocExtraction: true; runInstall is untouched. Bug 2 — cpDirAtomic called fs.cpSync without verbatimSymlinks, so Node resolved relative symlinks against the tmp source dir, baking absolute paths pointing at the soon-deleted ask-gh-clone-* tmpdir into the store. Every subsequent verifyEntry -> hashDir -> readFileSync then hit ENOENT on the broken symlink. Gitbutler's claude.md/CLAUDE.md -> AGENTS.md relative symlinks triggered this. Added verbatimSymlinks: true to the fs.cpSync call. Regression tests added: - 3 new tests in github.test.ts under "skipDocExtraction — callers that only need the checkout path": success without docs dir, master fallback + no docs, guardrail that default behavior still throws. - 1 new test in ensure-checkout.test.ts verifying skipDocExtraction: true is forwarded to the fetcher. Two CLAUDE.md gotchas added to prevent regression of both fixes.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Deploying ask-registry with
|
| Latest commit: |
a2e2d51
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://20786671.ask-registry.pages.dev |
| Branch Preview URL: | https://amondnet-src-docs-fallback.ask-registry.pages.dev |
Contributor
There was a problem hiding this comment.
No issues found across 7 files
Requires human review: Modifies core fetcher and filesystem cache logic (symlink handling), which impacts the CLI's central data storage and retrieval paths. Requires human review for potential side effects.
Architecture diagram
sequenceDiagram
participant CLI as CLI (ask src/docs)
participant EC as ensureCheckout()
participant GS as GithubSource
participant Store as cpDirAtomic()
participant FS as Node.js fs
participant Remote as GitHub/Git
Note over CLI,Remote: Request Flow for Checkout (ask src / ask docs)
CLI->>EC: Request checkout for repo
EC->>GS: fetch(repo, options)
Note right of EC: NEW: Sets skipDocExtraction = true
GS->>Remote: git clone / download tarball
Remote-->>GS: Source files (in tmp dir)
GS->>Store: Atomic copy to cache
Store->>FS: fs.cpSync(src, dest)
Note right of Store: CHANGED: verbatimSymlinks = true<br/>prevents absolute path resolution
alt skipDocExtraction is TRUE
GS->>GS: Skip extractDocsFromDir()
Note over GS: Fixes crash on repos<br/>without /docs folder
else skipDocExtraction is FALSE (e.g. ask install)
GS->>GS: internal: extractDocsFromDir()
alt No docs directory found
GS-->>EC: Throw "No docs directory" Error
else docs found
GS->>GS: Generate file list
end
end
GS-->>EC: Return { storePath, files: [] }
EC-->>CLI: Return cached checkout path
Note over CLI,FS: Cache Hit Flow (Next Request)
CLI->>EC: Request same repo
EC->>GS: fetch(repo)
GS->>GS: verifyEntry(storeDir)
GS->>FS: readFileSync() / hashDir()
Note right of FS: Validates symlinks.<br/>Now succeeds because links<br/>remained relative.
GS-->>EC: Return cached path
EC-->>CLI: Return path
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.
Summary
Bug 1 — spurious "No docs directory found" error in
ask src/ask docs:GithubSource.fetchalways ranextractDocsFromDir, correct forrunInstallbut wrong for path-only callers. Repos without a conventionaldocs/dir (e.g. gitbutler) threw through both the shallow-clone and tar.gz paths. AddedskipDocExtraction?: booleantoGithubSourceOptions;ensureCheckoutnow sets this flag, so only the checkout path is returned (files: []).runInstallis untouched —skipDocExtractionis strictly opt-in.Bug 2 — ENOENT on cache hits caused by broken symlinks in the store:
cpDirAtomiccalledfs.cpSyncwithoutverbatimSymlinks: true, so Node resolved relative symlinks against the temporary source dir, baking absolute paths pointing at the soon-deletedask-gh-clone-*tmpdir into the store. Every subsequentverifyEntry → hashDir → readFileSynchit ENOENT. Gitbutler'sclaude.md/CLAUDE.md → AGENTS.mdrelative symlinks triggered this. Fixed by addingverbatimSymlinks: trueto thefs.cpSynccall inpackages/cli/src/store/index.ts.Both bugs manifest as a single user-visible failure:
ask src github:gitbutlerapp/gitbutlerreturns an error instead of the cached master checkout path.Test plan
packages/cli/test/sources/github.test.ts: 3 new tests underdescribe('skipDocExtraction — callers that only need the checkout path'):runInstallpath is unaffectedpackages/cli/test/commands/ensure-checkout.test.ts: 1 new test verifyingskipDocExtraction: trueis forwarded to the fetcherask src— runask src github:gitbutlerapp/gitbutler; should print the master checkout path without errorask docscache hit — runask docs github:gitbutlerapp/gitbutlerafter a cached checkout exists; should emit doc-like subdirectory paths without ENOENTrunInstallunaffected — runask installfor a spec with a docs dir; normal doc extraction must still work (skipDocExtraction is not set by runInstall)Files changed
packages/cli/src/sources/index.ts—skipDocExtraction?: booleanadded toGithubSourceOptionspackages/cli/src/sources/github.ts— three call sites skipextractDocsFromDirwhen flag is setpackages/cli/src/commands/ensure-checkout.ts— setsskipDocExtraction: trueon fetch optspackages/cli/src/store/index.ts—verbatimSymlinks: trueadded tofs.cpSyncpackages/cli/test/sources/github.test.ts— 3 new regression testspackages/cli/test/commands/ensure-checkout.test.ts— 1 new regression testCLAUDE.md— two gotchas added to prevent regression of both fixesSummary by cubic
Skip doc extraction during GitHub checkout so
ask src/ask docsdon’t fail on repos withoutdocs/. Preserve symlinks when copying store entries to prevent ENOENT on cache hits.ensureCheckoutnow setsskipDocExtraction: trueonGithubSource.fetch, returning only the checkout path (files: []) for path-only callers;runInstallbehavior is unchanged.cpDirAtomicusesfs.cpSync(..., { verbatimSymlinks: true })to keep relative symlinks intact and avoid broken links in the store.ensureCheckout.Written for commit a2e2d51. Summary will update on new commits.