mirror of
https://github.com/yamadashy/repomix.git
synced 2026-02-03 11:33:39 +01:00
Add new "Binary Files Detected" section to CLI output that shows files which were skipped due to binary content detection (not extension-based). This addresses issue #752 where users were not informed about files being silently excluded. Changes: - Update fileRead.ts to return detailed skip reasons (binary-extension, binary-content, size-limit, encoding-error) - Modify file collection pipeline to track and propagate skipped files - Add reportSkippedFiles function to display binary-content detected files - Show files with relative paths and helpful exclusion messages - Only display section when binary-content files are found - Add comprehensive test coverage for new functionality The implementation follows existing security check reporting patterns and provides users clear visibility into why files were excluded from output.
425 lines
12 KiB
TypeScript
425 lines
12 KiB
TypeScript
import { program } from 'commander';
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import * as defaultAction from '../../src/cli/actions/defaultAction.js';
|
|
import * as initAction from '../../src/cli/actions/initAction.js';
|
|
import * as remoteAction from '../../src/cli/actions/remoteAction.js';
|
|
import * as versionAction from '../../src/cli/actions/versionAction.js';
|
|
import { run, runCli } from '../../src/cli/cliRun.js';
|
|
import type { CliOptions } from '../../src/cli/types.js';
|
|
import type { RepomixConfigMerged } from '../../src/config/configSchema.js';
|
|
import type { PackResult } from '../../src/core/packager.js';
|
|
import { type RepomixLogLevel, logger, repomixLogLevels } from '../../src/shared/logger.js';
|
|
import { createMockConfig } from '../testing/testUtils.js';
|
|
|
|
let logLevel: RepomixLogLevel;
|
|
|
|
vi.mock('../../src/shared/logger', () => ({
|
|
repomixLogLevels: {
|
|
SILENT: -1,
|
|
ERROR: 0,
|
|
WARN: 1,
|
|
INFO: 2,
|
|
DEBUG: 3,
|
|
},
|
|
logger: {
|
|
log: vi.fn(),
|
|
trace: vi.fn(),
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
success: vi.fn(),
|
|
note: vi.fn(),
|
|
setLogLevel: vi.fn((level: RepomixLogLevel) => {
|
|
logLevel = level;
|
|
}),
|
|
getLogLevel: vi.fn(() => logLevel),
|
|
},
|
|
setLogLevelByWorkerData: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/cli/actions/defaultAction');
|
|
vi.mock('../../src/cli/actions/initAction');
|
|
vi.mock('../../src/cli/actions/remoteAction');
|
|
vi.mock('../../src/cli/actions/versionAction');
|
|
|
|
describe('cliRun', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
|
|
vi.mocked(defaultAction.runDefaultAction).mockResolvedValue({
|
|
config: createMockConfig({
|
|
cwd: process.cwd(),
|
|
input: {
|
|
maxFileSize: 50 * 1024 * 1024,
|
|
},
|
|
output: {
|
|
filePath: 'repomix-output.txt',
|
|
style: 'plain',
|
|
stdout: false,
|
|
parsableStyle: false,
|
|
fileSummary: true,
|
|
directoryStructure: true,
|
|
topFilesLength: 5,
|
|
showLineNumbers: false,
|
|
removeComments: false,
|
|
removeEmptyLines: false,
|
|
compress: false,
|
|
copyToClipboard: false,
|
|
files: true,
|
|
git: {
|
|
sortByChanges: true,
|
|
sortByChangesMaxCommits: 100,
|
|
includeDiffs: false,
|
|
},
|
|
},
|
|
include: [],
|
|
ignore: {
|
|
useGitignore: true,
|
|
useDefaultPatterns: true,
|
|
customPatterns: [],
|
|
},
|
|
security: {
|
|
enableSecurityCheck: true,
|
|
},
|
|
tokenCount: {
|
|
encoding: 'o200k_base',
|
|
},
|
|
}),
|
|
packResult: {
|
|
totalFiles: 0,
|
|
totalCharacters: 0,
|
|
totalTokens: 0,
|
|
fileCharCounts: {},
|
|
fileTokenCounts: {},
|
|
suspiciousFilesResults: [],
|
|
gitDiffTokenCount: 0,
|
|
gitLogTokenCount: 0,
|
|
suspiciousGitDiffResults: [],
|
|
suspiciousGitLogResults: [],
|
|
processedFiles: [],
|
|
safeFilePaths: [],
|
|
skippedFiles: [],
|
|
} satisfies PackResult,
|
|
});
|
|
vi.mocked(initAction.runInitAction).mockResolvedValue();
|
|
vi.mocked(remoteAction.runRemoteAction).mockResolvedValue({
|
|
config: createMockConfig({
|
|
cwd: process.cwd(),
|
|
input: {
|
|
maxFileSize: 50 * 1024 * 1024,
|
|
},
|
|
output: {
|
|
filePath: 'repomix-output.txt',
|
|
stdout: false,
|
|
style: 'plain',
|
|
parsableStyle: false,
|
|
fileSummary: true,
|
|
directoryStructure: true,
|
|
topFilesLength: 5,
|
|
showLineNumbers: false,
|
|
removeComments: false,
|
|
removeEmptyLines: false,
|
|
compress: false,
|
|
copyToClipboard: false,
|
|
files: true,
|
|
git: {
|
|
sortByChanges: true,
|
|
sortByChangesMaxCommits: 100,
|
|
includeDiffs: false,
|
|
},
|
|
},
|
|
include: [],
|
|
ignore: {
|
|
useGitignore: true,
|
|
useDefaultPatterns: true,
|
|
customPatterns: [],
|
|
},
|
|
security: {
|
|
enableSecurityCheck: true,
|
|
},
|
|
tokenCount: {
|
|
encoding: 'o200k_base',
|
|
},
|
|
}),
|
|
packResult: {
|
|
totalFiles: 0,
|
|
totalCharacters: 0,
|
|
totalTokens: 0,
|
|
fileCharCounts: {},
|
|
fileTokenCounts: {},
|
|
suspiciousFilesResults: [],
|
|
gitDiffTokenCount: 0,
|
|
gitLogTokenCount: 0,
|
|
suspiciousGitDiffResults: [],
|
|
suspiciousGitLogResults: [],
|
|
processedFiles: [],
|
|
safeFilePaths: [],
|
|
skippedFiles: [],
|
|
} satisfies PackResult,
|
|
});
|
|
vi.mocked(versionAction.runVersionAction).mockResolvedValue();
|
|
});
|
|
|
|
test('should call process.exit(1) on error', async () => {
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementationOnce(() => undefined as never);
|
|
const parseSpy = vi.spyOn(program, 'description').mockImplementationOnce(() => {
|
|
throw Error();
|
|
});
|
|
const handleErrorSpy = vi.spyOn(logger, 'error');
|
|
await expect(run()).resolves.not.toThrow();
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
expect(handleErrorSpy).toHaveBeenCalled();
|
|
exitSpy.mockReset();
|
|
parseSpy.mockReset();
|
|
handleErrorSpy.mockReset();
|
|
});
|
|
|
|
describe('executeAction', () => {
|
|
test('should execute default action when no special options provided', async () => {
|
|
await runCli(['.'], process.cwd(), {});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(['.'], process.cwd(), expect.any(Object));
|
|
});
|
|
|
|
test('should enable verbose logging when verbose option is true', async () => {
|
|
await runCli(['.'], process.cwd(), { verbose: true });
|
|
|
|
expect(logger.setLogLevel).toHaveBeenCalledWith(repomixLogLevels.DEBUG);
|
|
});
|
|
|
|
test('should execute version action when version option is true', async () => {
|
|
await runCli(['.'], process.cwd(), { version: true });
|
|
|
|
expect(versionAction.runVersionAction).toHaveBeenCalled();
|
|
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should execute init action when init option is true', async () => {
|
|
await runCli(['.'], process.cwd(), { init: true });
|
|
|
|
expect(initAction.runInitAction).toHaveBeenCalledWith(process.cwd(), false);
|
|
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should execute remote action when remote option is provided', async () => {
|
|
await runCli(['.'], process.cwd(), {
|
|
remote: 'yamadashy/repomix',
|
|
});
|
|
|
|
expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('yamadashy/repomix', expect.any(Object));
|
|
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('parsable style flag', () => {
|
|
test('should disable parsable style by default', async () => {
|
|
await runCli(['.'], process.cwd(), {});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.not.objectContaining({
|
|
parsableStyle: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle --parsable-style flag', async () => {
|
|
await runCli(['.'], process.cwd(), { parsableStyle: true });
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
parsableStyle: true,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('security check flag', () => {
|
|
test('should enable security check by default', async () => {
|
|
await runCli(['.'], process.cwd(), {});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.not.objectContaining({
|
|
securityCheck: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle --no-security-check flag', async () => {
|
|
await runCli(['.'], process.cwd(), { securityCheck: false });
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
securityCheck: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle explicit --security-check flag', async () => {
|
|
await runCli(['.'], process.cwd(), { securityCheck: true });
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
securityCheck: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle explicit --no-gitignore flag', async () => {
|
|
await runCli(['.'], process.cwd(), { gitignore: false });
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
gitignore: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle explicit --no-default-patterns flag', async () => {
|
|
await runCli(['.'], process.cwd(), { defaultPatterns: false });
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
defaultPatterns: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle explicit --header-text flag', async () => {
|
|
await runCli(['.'], process.cwd(), {
|
|
headerText: 'I am a good header text',
|
|
});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
headerText: 'I am a good header text',
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle --instruction-file-path flag', async () => {
|
|
await runCli(['.'], process.cwd(), {
|
|
instructionFilePath: 'path/to/instruction.txt',
|
|
});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
instructionFilePath: 'path/to/instruction.txt',
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should handle --include-empty-directories flag', async () => {
|
|
await runCli(['.'], process.cwd(), {
|
|
includeEmptyDirectories: true,
|
|
});
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
includeEmptyDirectories: true,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('quiet mode', () => {
|
|
test('should set log level to SILENT when quiet option is true', async () => {
|
|
const options: CliOptions = {
|
|
quiet: true,
|
|
};
|
|
|
|
await runCli(['.'], process.cwd(), options);
|
|
|
|
expect(logger.getLogLevel()).toBe(repomixLogLevels.SILENT);
|
|
});
|
|
|
|
test('should set log level to DEBUG when verbose option is true', async () => {
|
|
const options: CliOptions = {
|
|
verbose: true,
|
|
};
|
|
|
|
await runCli(['.'], process.cwd(), options);
|
|
|
|
expect(logger.getLogLevel()).toBe(repomixLogLevels.DEBUG);
|
|
});
|
|
|
|
test('should set log level to INFO by default', async () => {
|
|
const options: CliOptions = {};
|
|
|
|
await runCli(['.'], process.cwd(), options);
|
|
|
|
expect(logger.getLogLevel()).toBe(repomixLogLevels.INFO);
|
|
});
|
|
});
|
|
|
|
describe('stdout mode', () => {
|
|
const originalIsTTY = process.stdout.isTTY;
|
|
|
|
afterEach(() => {
|
|
process.stdout.isTTY = originalIsTTY;
|
|
});
|
|
|
|
test('should handle --stdout flag', async () => {
|
|
const options: CliOptions = {
|
|
stdout: true,
|
|
};
|
|
|
|
await runCli(['.'], process.cwd(), options);
|
|
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
stdout: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('should not enable stdout mode when explicitly setting output', async () => {
|
|
// Mock pipe detection
|
|
process.stdout.isTTY = false;
|
|
const options: CliOptions = {
|
|
output: 'custom-output.txt',
|
|
};
|
|
|
|
await runCli(['.'], process.cwd(), options);
|
|
|
|
// stdout should not be set
|
|
expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
output: 'custom-output.txt',
|
|
}),
|
|
);
|
|
expect(defaultAction.runDefaultAction).not.toHaveBeenCalledWith(
|
|
['.'],
|
|
process.cwd(),
|
|
expect.objectContaining({
|
|
stdout: true,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|