77} from "node:fs" ;
88import { dirname , join , relative , resolve } from "node:path" ;
99import type { ScopedPattern } from "../core/shadow.ts" ;
10+ import { getStateDir } from "./state-dir.ts" ;
1011
1112// --- Types ---
1213
@@ -22,8 +23,15 @@ export const DEFAULT_IGNORE_FILE_REFS = [
2223export type IgnoreFileDeps = {
2324 readFile : ( path : string ) => string ;
2425 pathExists : ( path : string ) => boolean ;
25- /** Find all files named `filename` under `rootDir`, returning absolute paths. */
26- walkFiles : ( rootDir : string , filename : string ) => string [ ] ;
26+ /**
27+ * Find all files named `filename` under `rootDir`, returning absolute paths.
28+ * Directories whose absolute path is in `excludeDirs` are not descended into.
29+ */
30+ walkFiles : (
31+ rootDir : string ,
32+ filename : string ,
33+ excludeDirs : ReadonlySet < string > ,
34+ ) => string [ ] ;
2735} ;
2836
2937export function parseIgnoreFileRef ( ref : string ) : IgnoreFileRef {
@@ -72,6 +80,11 @@ export function collectIgnorePatterns(
7280) : ScopedPattern [ ] {
7381 const result : ScopedPattern [ ] = [ ] ;
7482
83+ // Never scan Tuor's own state dir: it holds serialized overlay upper layers
84+ // (whiteout markers, plus symlinks whose targets only resolve inside the
85+ // sandbox), which is internal state, not user content.
86+ const excludeDirs = new Set ( [ getStateDir ( configDir ) ] ) ;
87+
7588 for ( const ref of refs ) {
7689 switch ( ref . source ) {
7790 case "host" : {
@@ -88,7 +101,11 @@ export function collectIgnorePatterns(
88101 }
89102 case "mount" : {
90103 if ( ref . recursive ) {
91- for ( const absPath of deps . walkFiles ( hostPath , ref . path ) ) {
104+ for ( const absPath of deps . walkFiles (
105+ hostPath ,
106+ ref . path ,
107+ excludeDirs ,
108+ ) ) {
92109 const dir = relative ( hostPath , dirname ( absPath ) ) ;
93110 const scope = dir === "" ? "/" : `/${ dir } ` ;
94111 result . push (
@@ -125,13 +142,27 @@ export function _parseIgnoreFile(contents: string): string[] {
125142
126143// --- Default deps (real filesystem) ---
127144
128- function walkFilesRecursive ( rootDir : string , filename : string ) : string [ ] {
145+ function walkFilesRecursive (
146+ rootDir : string ,
147+ filename : string ,
148+ excludeDirs : ReadonlySet < string > = new Set ( ) ,
149+ ) : string [ ] {
129150 const results : string [ ] = [ ] ;
130151 const walk = ( dir : string ) => {
131152 for ( const entry of readdirSync ( dir , { withFileTypes : true } ) ) {
132153 const full = join ( dir , entry . name ) ;
154+ if ( excludeDirs . has ( full ) ) continue ;
133155 if ( entry . isSymbolicLink ( ) ) {
134- if ( ! statSync ( full ) . isDirectory ( ) ) continue ;
156+ // statSync follows the link to its target. A dangling symlink (target
157+ // missing) throws ENOENT, so guard against it and skip rather than
158+ // letting one broken link crash the entire walk.
159+ let isDir : boolean ;
160+ try {
161+ isDir = statSync ( full ) . isDirectory ( ) ;
162+ } catch {
163+ continue ;
164+ }
165+ if ( ! isDir ) continue ;
135166 const real = realpathSync ( full ) ;
136167 if ( dir . startsWith ( real + "/" ) || dir === real ) {
137168 throw new Error (
0 commit comments