Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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/**",
Expand Down
15 changes: 15 additions & 0 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
}
Comment thread
yamadashy marked this conversation as resolved.

throw new RepomixError(`Failed to copy output file: ${(error as Error).message}`);
}
};
7 changes: 6 additions & 1 deletion src/core/metrics/TokenCounter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 28 additions & 0 deletions tests/cli/actions/remoteAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
});
});
});
16 changes: 8 additions & 8 deletions tests/core/metrics/TokenCounter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
Loading