diff --git a/biome.json b/biome.json index 8e1a66d98..d734c6b29 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json", + "$schema": "https://biomejs.dev/schemas/latest/schema.json", "files": { "includes": [ "bin/**", diff --git a/src/cli/actions/remoteAction.ts b/src/cli/actions/remoteAction.ts index d081fdb83..ade5e7a49 100644 --- a/src/cli/actions/remoteAction.ts +++ b/src/cli/actions/remoteAction.ts @@ -206,6 +206,21 @@ export const copyOutputToCurrentDirectory = async ( await fs.copyFile(sourcePath, targetPath); } catch (error) { + const nodeError = error as NodeJS.ErrnoException; + + // Provide helpful message for permission errors + if (nodeError.code === 'EPERM' || nodeError.code === 'EACCES') { + throw new RepomixError( + `Failed to copy output file to ${targetPath}: Permission denied. + +The current directory may be protected or require elevated permissions. +Please try one of the following: + • Run from a different directory (e.g., your home directory or Documents folder) + • Use the --output flag to specify a writable location: --output ~/repomix-output.xml + • Use --stdout to print output directly to the console`, + ); + } + throw new RepomixError(`Failed to copy output file: ${(error as Error).message}`); } }; diff --git a/src/core/metrics/TokenCounter.ts b/src/core/metrics/TokenCounter.ts index e9b8f52a4..7ae1dcb46 100644 --- a/src/core/metrics/TokenCounter.ts +++ b/src/core/metrics/TokenCounter.ts @@ -18,7 +18,12 @@ export class TokenCounter { public countTokens(content: string, filePath?: string): number { try { - return this.encoding.encode(content).length; + // Disable special token validation to handle files that may contain + // special token sequences (e.g., tokenizer configs with <|endoftext|>). + // This treats special tokens as ordinary text rather than control tokens, + // which is appropriate for general code/text analysis where we're not + // actually sending the content to an LLM API. + return this.encoding.encode(content, [], []).length; } catch (error) { let message = ''; if (error instanceof Error) { diff --git a/tests/cli/actions/remoteAction.test.ts b/tests/cli/actions/remoteAction.test.ts index 04967c62e..c8736458e 100644 --- a/tests/cli/actions/remoteAction.test.ts +++ b/tests/cli/actions/remoteAction.test.ts @@ -256,5 +256,33 @@ describe('remoteAction functions', () => { 'Failed to copy output file', ); }); + + test('should throw helpful error message for EPERM permission errors', async () => { + const sourceDir = '/tmp/repomix-123'; + const targetDir = 'C:\\Windows\\System32'; + const fileName = 'output.xml'; + + const epermError = new Error('operation not permitted') as NodeJS.ErrnoException; + epermError.code = 'EPERM'; + vi.mocked(fs.copyFile).mockRejectedValue(epermError); + + await expect(copyOutputToCurrentDirectory(sourceDir, targetDir, fileName)).rejects.toThrow( + /Permission denied.*protected.*--output.*--stdout/s, + ); + }); + + test('should throw helpful error message for EACCES permission errors', async () => { + const sourceDir = '/tmp/repomix-123'; + const targetDir = '/protected/dir'; + const fileName = 'output.xml'; + + const eaccesError = new Error('permission denied') as NodeJS.ErrnoException; + eaccesError.code = 'EACCES'; + vi.mocked(fs.copyFile).mockRejectedValue(eaccesError); + + await expect(copyOutputToCurrentDirectory(sourceDir, targetDir, fileName)).rejects.toThrow( + /Permission denied.*protected.*--output.*--stdout/s, + ); + }); }); }); diff --git a/tests/core/metrics/TokenCounter.test.ts b/tests/core/metrics/TokenCounter.test.ts index 847dc901f..dedc8dbcf 100644 --- a/tests/core/metrics/TokenCounter.test.ts +++ b/tests/core/metrics/TokenCounter.test.ts @@ -47,7 +47,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(3); // Length of mockTokens - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle empty string', () => { @@ -56,7 +56,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(''); expect(count).toBe(0); - expect(mockEncoder.encode).toHaveBeenCalledWith(''); + expect(mockEncoder.encode).toHaveBeenCalledWith('', [], []); }); test('should handle multi-line text', () => { @@ -67,7 +67,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(6); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle special characters', () => { @@ -78,7 +78,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(3); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle unicode characters', () => { @@ -89,7 +89,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(4); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle code snippets', () => { @@ -104,7 +104,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(10); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle markdown text', () => { @@ -122,7 +122,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(15); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should handle very long text', () => { @@ -133,7 +133,7 @@ describe('TokenCounter', () => { const count = tokenCounter.countTokens(text); expect(count).toBe(100); - expect(mockEncoder.encode).toHaveBeenCalledWith(text); + expect(mockEncoder.encode).toHaveBeenCalledWith(text, [], []); }); test('should properly handle encoding errors without file path', () => {