From 32ec992de5d8d5a526e7fd52e6d2dd4529ca3825 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 20 Jan 2026 16:07:22 -0600 Subject: [PATCH 1/8] feat: add one exception to default forceignore "no dot files" rules for IDE use --- src/resolve/forceIgnore.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/resolve/forceIgnore.ts b/src/resolve/forceIgnore.ts index f47c3ecf78..6fccd96866 100644 --- a/src/resolve/forceIgnore.ts +++ b/src/resolve/forceIgnore.ts @@ -28,7 +28,22 @@ export class ForceIgnore { private readonly parser?: Ignore; private readonly forceIgnoreDirectory?: string; - private DEFAULT_IGNORE = ['**/*.dup', '**/.*', '**/package2-descriptor.json', '**/package2-manifest.json']; + private DEFAULT_IGNORE = [ + '**/*.dup', + // I know it's ugly. But I want to be able to retrieve metadata to a local dir, segregated by org. + // and `.sf` is already ignored in projects, and we already have orgIds for STL + // so this nastiness is "ignore all dot files except this one directory" + // once you ignore a parent ex `**/.*` you can't unignore something inside that path, at least with the curent ignore library + '**/.*', + '!.sf', + '**/.sf/**', + '!**/.sf/orgs', + '!**/.sf/orgs/*', + '!**/.sf/orgs/*/remoteMetadata', + '!**/.sf/orgs/*/remoteMetadata/**', + '**/package2-descriptor.json', + '**/package2-manifest.json', + ]; public constructor(forceIgnorePath = '') { try { From 81a25e17e9d2ab26312a0a3072f5354f452d1caa Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 21 Jan 2026 09:20:31 -0600 Subject: [PATCH 2/8] fix: relative paths for searchUp/forceignore --- test/nuts/local/searchUp/searchUp.nut.ts | 60 +++++++++++++++++------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/test/nuts/local/searchUp/searchUp.nut.ts b/test/nuts/local/searchUp/searchUp.nut.ts index e07fe1a7e8..67c0b7e226 100644 --- a/test/nuts/local/searchUp/searchUp.nut.ts +++ b/test/nuts/local/searchUp/searchUp.nut.ts @@ -60,42 +60,70 @@ describe('searchUp nut test', () => { describe('relative paths', () => { it('finds file in parent directory', () => { - const startPath = path.join(session.project.dir, 'level1', 'level2', 'startDir'); - const result = searchUp(startPath, 'target.txt'); - const expected = path.join(session.project.dir, 'level1', 'level2', 'target.txt'); + const relativeStartPath = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'startDir') + ); + expect(path.isAbsolute(relativeStartPath)).to.be.false; + const result = searchUp(relativeStartPath, 'target.txt'); + const expected = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'target.txt') + ); expect(result).to.equal(expected); }); it('finds file multiple levels up', () => { - const startPath = path.join(session.project.dir, 'level1', 'level2', 'startDir'); - const result = searchUp(startPath, '.gitignore'); - const expected = path.join(session.project.dir, '.gitignore'); + const relativeStartPath = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'startDir') + ); + expect(path.isAbsolute(relativeStartPath)).to.be.false; + const result = searchUp(relativeStartPath, '.gitignore'); + const expected = path.relative(session.project.dir, path.join(session.project.dir, '.gitignore')); expect(result).to.equal(expected); }); it('finds file in current directory', () => { - const startPath = path.join(session.project.dir, 'level1', 'level2', 'startDir'); - fs.writeFileSync(path.join(startPath, 'localFile.txt'), 'local content'); - const result = searchUp(startPath, 'localFile.txt'); - const expected = path.join(startPath, 'localFile.txt'); + const relativeStartPath = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'startDir') + ); + expect(path.isAbsolute(relativeStartPath)).to.be.false; + const absoluteStartPath = path.resolve(session.project.dir, relativeStartPath); + fs.writeFileSync(path.join(absoluteStartPath, 'localFile.txt'), 'local content'); + const result = searchUp(relativeStartPath, 'localFile.txt'); + const expected = path.relative(session.project.dir, path.join(absoluteStartPath, 'localFile.txt')); expect(result).to.equal(expected); }); it('returns undefined when file not found', () => { - const startPath = path.join(session.project.dir, 'level1', 'level2', 'startDir'); - const result = searchUp(startPath, 'nonexistent.txt'); + const relativeStartPath = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'startDir') + ); + expect(path.isAbsolute(relativeStartPath)).to.be.false; + const result = searchUp(relativeStartPath, 'nonexistent.txt'); expect(result).to.be.undefined; }); it('works when starting from file path', () => { - const filePath = path.join(session.project.dir, 'level1', 'level2', 'startDir', 'someFile.txt'); - fs.writeFileSync(filePath, 'content'); - const result = searchUp(filePath, 'target.txt'); - const expected = path.join(session.project.dir, 'level1', 'level2', 'target.txt'); + const relativeFilePath = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'startDir', 'someFile.txt') + ); + expect(path.isAbsolute(relativeFilePath)).to.be.false; + const absoluteFilePath = path.resolve(session.project.dir, relativeFilePath); + fs.writeFileSync(absoluteFilePath, 'content'); + const result = searchUp(relativeFilePath, 'target.txt'); + const expected = path.relative( + session.project.dir, + path.join(session.project.dir, 'level1', 'level2', 'target.txt') + ); expect(result).to.equal(expected); }); From c0176599feccc9371f3f3e6920105acc15a7fede Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 26 Jan 2026 10:14:46 -0600 Subject: [PATCH 3/8] fix: relative path handling --- src/utils/fileSystemHandler.ts | 11 ++++++----- test/nuts/local/searchUp/searchUp.nut.ts | 14 ++++---------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/utils/fileSystemHandler.ts b/src/utils/fileSystemHandler.ts index cad382d519..3fcca58566 100644 --- a/src/utils/fileSystemHandler.ts +++ b/src/utils/fileSystemHandler.ts @@ -23,22 +23,23 @@ export const ensureFileExists = async (filePath: string): Promise => { }; /** - * Traverse up a file path and search for the given file name. + * Traverse up a file path and search for the given file name. Always returns an absolute path. * * @param start File or folder path to start searching from * @param fileName File name to search for */ export function searchUp(start: SourcePath, fileName: string): string | undefined { - const filePath = path.join(start, fileName); + const absoluteStart = path.isAbsolute(start) ? start : path.join(process.cwd(), start); + const filePath = path.join(absoluteStart, fileName); if (fs.existsSync(filePath)) { return filePath; } - const normalizedStart = path.normalize(start); - const parent = path.dirname(normalizedStart); + const normalizedAbsoluteStart = path.normalize(absoluteStart); + const parent = path.dirname(normalizedAbsoluteStart); // If we're at root, stop (don't try to go up with ..) - if (parent === normalizedStart || normalizedStart === path.parse(normalizedStart).root) { + if (parent === normalizedAbsoluteStart || normalizedAbsoluteStart === path.parse(normalizedAbsoluteStart).root) { return; } diff --git a/test/nuts/local/searchUp/searchUp.nut.ts b/test/nuts/local/searchUp/searchUp.nut.ts index 67c0b7e226..bbfa229eb9 100644 --- a/test/nuts/local/searchUp/searchUp.nut.ts +++ b/test/nuts/local/searchUp/searchUp.nut.ts @@ -66,10 +66,7 @@ describe('searchUp nut test', () => { ); expect(path.isAbsolute(relativeStartPath)).to.be.false; const result = searchUp(relativeStartPath, 'target.txt'); - const expected = path.relative( - session.project.dir, - path.join(session.project.dir, 'level1', 'level2', 'target.txt') - ); + const expected = path.join(session.project.dir, 'level1', 'level2', 'target.txt'); expect(result).to.equal(expected); }); @@ -81,7 +78,7 @@ describe('searchUp nut test', () => { ); expect(path.isAbsolute(relativeStartPath)).to.be.false; const result = searchUp(relativeStartPath, '.gitignore'); - const expected = path.relative(session.project.dir, path.join(session.project.dir, '.gitignore')); + const expected = path.join(session.project.dir, '.gitignore'); expect(result).to.equal(expected); }); @@ -95,7 +92,7 @@ describe('searchUp nut test', () => { const absoluteStartPath = path.resolve(session.project.dir, relativeStartPath); fs.writeFileSync(path.join(absoluteStartPath, 'localFile.txt'), 'local content'); const result = searchUp(relativeStartPath, 'localFile.txt'); - const expected = path.relative(session.project.dir, path.join(absoluteStartPath, 'localFile.txt')); + const expected = path.join(absoluteStartPath, 'localFile.txt'); expect(result).to.equal(expected); }); @@ -120,10 +117,7 @@ describe('searchUp nut test', () => { const absoluteFilePath = path.resolve(session.project.dir, relativeFilePath); fs.writeFileSync(absoluteFilePath, 'content'); const result = searchUp(relativeFilePath, 'target.txt'); - const expected = path.relative( - session.project.dir, - path.join(session.project.dir, 'level1', 'level2', 'target.txt') - ); + const expected = path.join(session.project.dir, 'level1', 'level2', 'target.txt'); expect(result).to.equal(expected); }); From ebe5622d5faf3f55e779f98928e795e0859409ec Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jan 2026 15:29:07 -0600 Subject: [PATCH 4/8] test: forceignore assertions for remoteMetadata --- test/resolve/forceIgnore.test.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/resolve/forceIgnore.test.ts b/test/resolve/forceIgnore.test.ts index ea82a8a0b3..21a9ff9ba3 100644 --- a/test/resolve/forceIgnore.test.ts +++ b/test/resolve/forceIgnore.test.ts @@ -116,11 +116,34 @@ describe('ForceIgnore', () => { it('Should ignore files starting with a dot', () => { const dotPath = join(root, '.xyz'); - expect(forceIgnore.accepts(dotPath)).to.be.false; expect(forceIgnore.denies(dotPath)).to.be.true; }); + it('Should NOT ignore .sf/orgs//remoteMetadata', () => { + const remoteMetadataPath = join(root, '.sf', 'orgs', '00D000000000000', 'remoteMetadata'); + expect(forceIgnore.accepts(remoteMetadataPath)).to.be.true; + expect(forceIgnore.denies(remoteMetadataPath)).to.be.false; + }); + + it('Should NOT ignore stuff in .sf/orgs//remoteMetadata', () => { + const remoteMetadataPath = join(root, '.sf', 'orgs', '00D000000000000', 'remoteMetadata', 'foo', 'bar'); + expect(forceIgnore.accepts(remoteMetadataPath)).to.be.true; + expect(forceIgnore.denies(remoteMetadataPath)).to.be.false; + }); + + it('Should ignore .sf/orgs//anythingElse', () => { + const dotSfNotInRemoteMetadata = join(root, '.sf', 'orgs', '00D000000000000', 'foo'); + expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; + expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; + }); + + it('Should ignore .sf/orgs/anythingElse', () => { + const dotSfNotInRemoteMetadata = join(root, '.sf', 'orgs', 'foo'); + expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; + expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; + }); + it('Should ignore files ending in .dup', () => { const dupPath = join(root, 'abc.dup'); From e4eda5bed4619560aaed9488f8f755b6e08f7e15 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jan 2026 15:34:06 -0600 Subject: [PATCH 5/8] test: more assertions --- test/resolve/forceIgnore.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/resolve/forceIgnore.test.ts b/test/resolve/forceIgnore.test.ts index 21a9ff9ba3..c160be2172 100644 --- a/test/resolve/forceIgnore.test.ts +++ b/test/resolve/forceIgnore.test.ts @@ -126,7 +126,7 @@ describe('ForceIgnore', () => { expect(forceIgnore.denies(remoteMetadataPath)).to.be.false; }); - it('Should NOT ignore stuff in .sf/orgs//remoteMetadata', () => { + it('Should NOT ignore stuff inside .sf/orgs//remoteMetadata', () => { const remoteMetadataPath = join(root, '.sf', 'orgs', '00D000000000000', 'remoteMetadata', 'foo', 'bar'); expect(forceIgnore.accepts(remoteMetadataPath)).to.be.true; expect(forceIgnore.denies(remoteMetadataPath)).to.be.false; @@ -138,8 +138,8 @@ describe('ForceIgnore', () => { expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; }); - it('Should ignore .sf/orgs/anythingElse', () => { - const dotSfNotInRemoteMetadata = join(root, '.sf', 'orgs', 'foo'); + it('Should ignore .sf/anythingElse', () => { + const dotSfNotInRemoteMetadata = join(root, '.sf', 'foo'); expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; }); From 488a6fea3b382bad810c4957c7b9c0fdd0bac25f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jan 2026 15:38:36 -0600 Subject: [PATCH 6/8] chore: even more specific ignore path --- src/resolve/forceIgnore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resolve/forceIgnore.ts b/src/resolve/forceIgnore.ts index 6fccd96866..dcbe01fb17 100644 --- a/src/resolve/forceIgnore.ts +++ b/src/resolve/forceIgnore.ts @@ -38,7 +38,8 @@ export class ForceIgnore { '!.sf', '**/.sf/**', '!**/.sf/orgs', - '!**/.sf/orgs/*', + '!**/.sf/orgs/**', + '**/.sf/orgs/*/**', '!**/.sf/orgs/*/remoteMetadata', '!**/.sf/orgs/*/remoteMetadata/**', '**/package2-descriptor.json', From 7e8098122f95453df65b3df1776ac9af90884a09 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jan 2026 15:41:52 -0600 Subject: [PATCH 7/8] test: more assertions --- test/resolve/forceIgnore.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/resolve/forceIgnore.test.ts b/test/resolve/forceIgnore.test.ts index c160be2172..73773ad9d7 100644 --- a/test/resolve/forceIgnore.test.ts +++ b/test/resolve/forceIgnore.test.ts @@ -138,6 +138,12 @@ describe('ForceIgnore', () => { expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; }); + it('Should ignore .sf/orgs/anythingElse', () => { + const dotSfNotInRemoteMetadata = join(root, '.sf', 'orgs', 'foo'); + expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; + expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; + }); + it('Should ignore .sf/anythingElse', () => { const dotSfNotInRemoteMetadata = join(root, '.sf', 'foo'); expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; From 24bfae190ad30df8562af797f51093ed709f7355 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jan 2026 15:55:47 -0600 Subject: [PATCH 8/8] test: remove missing case --- test/resolve/forceIgnore.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/resolve/forceIgnore.test.ts b/test/resolve/forceIgnore.test.ts index 73773ad9d7..c160be2172 100644 --- a/test/resolve/forceIgnore.test.ts +++ b/test/resolve/forceIgnore.test.ts @@ -138,12 +138,6 @@ describe('ForceIgnore', () => { expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; }); - it('Should ignore .sf/orgs/anythingElse', () => { - const dotSfNotInRemoteMetadata = join(root, '.sf', 'orgs', 'foo'); - expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false; - expect(forceIgnore.denies(dotSfNotInRemoteMetadata)).to.be.true; - }); - it('Should ignore .sf/anythingElse', () => { const dotSfNotInRemoteMetadata = join(root, '.sf', 'foo'); expect(forceIgnore.accepts(dotSfNotInRemoteMetadata)).to.be.false;