Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- run: node bin/repomix
- run: node bin/repomix.cjs
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
Expand Down
54 changes: 51 additions & 3 deletions bin/repomix.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
#!/usr/bin/env node
'use strict';

const nodeVersion = process.versions.node;
const [major] = nodeVersion.split('.').map(Number);

const EXIT_CODES = {
SUCCESS: 0,
ERROR: 1,
};

if (major < 16) {
console.error(`Repomix requires Node.js version 16 or higher. Current version: ${nodeVersion}\n`);
process.exit(EXIT_CODES.ERROR);
}

function setupErrorHandlers() {
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(EXIT_CODES.ERROR);
});

process.on('unhandledRejection', (reason) => {
console.error('Unhandled Promise Rejection:', reason);
process.exit(EXIT_CODES.ERROR);
});

function shutdown() {
process.exit(EXIT_CODES.SUCCESS);
}

process.on('SIGINT', () => {
console.log('\nReceived SIGINT. Shutting down...');
shutdown();
});
process.on('SIGTERM', shutdown);
}
Comment thread
yamadashy marked this conversation as resolved.

(async () => {
const { run } = await import('../lib/cli/cliRun.js');
run();
try {
setupErrorHandlers();

const { run } = await import('../lib/cli/cliRun.js');
Comment thread
yamadashy marked this conversation as resolved.
await run();
} catch (error) {
if (error instanceof Error) {
console.error('Fatal Error:', {
name: error.name,
message: error.message,
stack: error.stack,
});
} else {
console.error('Fatal Error:', error);
}
}
Comment thread
yamadashy marked this conversation as resolved.
})();
14 changes: 0 additions & 14 deletions bin/repomix.js

This file was deleted.

1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"files": {
"include": [
"./bin/**",
"./src/**",
"./tests/**",
"package.json",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"lint-secretlint": "secretlint \"**/*\" --secretlintignore .gitignore",
"test": "vitest",
"test-coverage": "vitest run --coverage",
"cli-run": "npm run build && node --trace-warnings bin/repomix",
"cli-run": "npm run build && node --trace-warnings bin/repomix.cjs",
"npm-publish": "npm run lint && npm run test-coverage && npm run build && npm publish",
"npm-release-patch": "npm version patch && npm run npm-publish",
"npm-release-minor": "npm version minor && npm run npm-publish",
Expand Down
8 changes: 4 additions & 4 deletions src/cli/actions/initAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const runInitAction = async (rootDir: string, isGlobal: boolean): Promise
}
};

export async function createConfigFile(rootDir: string, isGlobal: boolean): Promise<boolean> {
export const createConfigFile = async (rootDir: string, isGlobal: boolean): Promise<boolean> => {
const configPath = path.resolve(isGlobal ? getGlobalDirectory() : rootDir, 'repomix.config.json');

const isCreateConfig = await prompts.confirm({
Expand Down Expand Up @@ -121,9 +121,9 @@ export async function createConfigFile(rootDir: string, isGlobal: boolean): Prom
);

return true;
}
};

export async function createIgnoreFile(rootDir: string, isGlobal: boolean): Promise<boolean> {
export const createIgnoreFile = async (rootDir: string, isGlobal: boolean): Promise<boolean> => {
if (isGlobal) {
prompts.log.info(`Skipping ${pc.green('.repomixignore')} file creation for global configuration.`);
return false;
Expand Down Expand Up @@ -173,4 +173,4 @@ export async function createIgnoreFile(rootDir: string, isGlobal: boolean): Prom
);

return true;
}
};
10 changes: 5 additions & 5 deletions src/cli/actions/remoteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export const formatGitUrl = (url: string): string => {
return url;
};

const createTempDirectory = async (): Promise<string> => {
export const createTempDirectory = async (): Promise<string> => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'repomix-'));
logger.trace(`Created temporary directory. (path: ${pc.dim(tempDir)})`);
return tempDir;
};
Comment thread
yamadashy marked this conversation as resolved.

const cloneRepository = async (url: string, directory: string): Promise<void> => {
export const cloneRepository = async (url: string, directory: string): Promise<void> => {
logger.log(`Clone repository: ${url} to temporary directory. ${pc.dim(`path: ${directory}`)}`);
logger.log('');

Expand All @@ -69,12 +69,12 @@ const cloneRepository = async (url: string, directory: string): Promise<void> =>
}
};

const cleanupTempDirectory = async (directory: string): Promise<void> => {
export const cleanupTempDirectory = async (directory: string): Promise<void> => {
logger.trace(`Cleaning up temporary directory: ${directory}`);
await fs.rm(directory, { recursive: true, force: true });
};
Comment thread
yamadashy marked this conversation as resolved.

const checkGitInstallation = async (): Promise<boolean> => {
export const checkGitInstallation = async (): Promise<boolean> => {
try {
const result = await execAsync('git --version');
return !result.stderr;
Expand All @@ -84,7 +84,7 @@ const checkGitInstallation = async (): Promise<boolean> => {
}
};

const copyOutputToCurrentDirectory = async (
export const copyOutputToCurrentDirectory = async (
sourceDir: string,
targetDir: string,
outputFileName: string,
Expand Down
8 changes: 3 additions & 5 deletions src/cli/cliRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ export interface CliOptions extends OptionValues {
remote?: string;
}

export async function run() {
export const run = async () => {
try {
const version = await getVersion();

program
.description('Repomix - Pack your repository into a single AI-friendly file')
.arguments('[directory]')
Expand All @@ -52,9 +50,9 @@ export async function run() {
} catch (error) {
handleError(error);
}
}
};

const executeAction = async (directory: string, cwd: string, options: CliOptions) => {
export const executeAction = async (directory: string, cwd: string, options: CliOptions) => {
logger.setVerbose(options.verbose || false);

if (options.version) {
Expand Down
8 changes: 4 additions & 4 deletions src/core/file/permissionCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class PermissionError extends Error {
}
}

export async function checkDirectoryPermissions(dirPath: string): Promise<PermissionCheckResult> {
export const checkDirectoryPermissions = async (dirPath: string): Promise<PermissionCheckResult> => {
try {
// First try to read directory contents
await fs.readdir(dirPath);
Expand Down Expand Up @@ -87,9 +87,9 @@ export async function checkDirectoryPermissions(dirPath: string): Promise<Permis
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
};

function getMacOSPermissionMessage(dirPath: string, errorCode?: string): string {
const getMacOSPermissionMessage = (dirPath: string, errorCode?: string): string => {
if (platform() === 'darwin') {
return `Permission denied: Cannot access '${dirPath}', error code: ${errorCode}.

Expand All @@ -109,4 +109,4 @@ If your terminal app is not listed:
}

return `Permission denied: Cannot access '${dirPath}'`;
}
};
85 changes: 65 additions & 20 deletions tests/cli/actions/remoteAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { formatGitUrl } from '../../../src/cli/actions/remoteAction.js';
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
cleanupTempDirectory,
copyOutputToCurrentDirectory,
createTempDirectory,
formatGitUrl,
} from '../../../src/cli/actions/remoteAction.js';

vi.mock('node:fs/promises');
vi.mock('node:child_process');
vi.mock('../../../src/cli/actions/defaultAction.js');
vi.mock('../../../src/shared/logger.js');
vi.mock('node:fs/promises');
vi.mock('node:os');
vi.mock('../../../src/shared/logger');

describe('remoteAction', () => {
describe('remoteAction functions', () => {
beforeEach(() => {
vi.resetAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('formatGitUrl', () => {
it('should format GitHub shorthand correctly', () => {
test('should convert GitHub shorthand to full URL', () => {
expect(formatGitUrl('user/repo')).toBe('https://github.com/user/repo.git');
expect(formatGitUrl('user-name/repo-name')).toBe('https://github.com/user-name/repo-name.git');
expect(formatGitUrl('user_name/repo_name')).toBe('https://github.com/user_name/repo_name.git');
});

it('should add .git to HTTPS URLs if missing', () => {
test('should handle HTTPS URLs', () => {
expect(formatGitUrl('https://github.com/user/repo')).toBe('https://github.com/user/repo.git');
expect(formatGitUrl('https://github.com/user/repo.git')).toBe('https://github.com/user/repo.git');
});

it('should not modify URLs that are already correctly formatted', () => {
expect(formatGitUrl('https://github.com/user/repo.git')).toBe('https://github.com/user/repo.git');
expect(formatGitUrl('git@github.com:user/repo.git')).toBe('git@github.com:user/repo.git');
test('should not modify SSH URLs', () => {
const sshUrl = 'git@github.com:user/repo.git';
expect(formatGitUrl(sshUrl)).toBe(sshUrl);
});
});

describe('createTempDirectory', () => {
test('should create temporary directory', async () => {
const mockTempDir = '/mock/temp/dir';
vi.mocked(os.tmpdir).mockReturnValue('/mock/temp');
vi.mocked(fs.mkdtemp).mockResolvedValue(mockTempDir);

it('should not modify SSH URLs', () => {
expect(formatGitUrl('git@github.com:user/repo.git')).toBe('git@github.com:user/repo.git');
const result = await createTempDirectory();
expect(result).toBe(mockTempDir);
expect(fs.mkdtemp).toHaveBeenCalledWith(path.join('/mock/temp', 'repomix-'));
});
});

describe('cleanupTempDirectory', () => {
test('should cleanup directory', async () => {
const mockDir = '/mock/temp/dir';
vi.mocked(fs.rm).mockResolvedValue();

await cleanupTempDirectory(mockDir);

expect(fs.rm).toHaveBeenCalledWith(mockDir, { recursive: true, force: true });
});
});

describe('copyOutputToCurrentDirectory', () => {
test('should copy output file', async () => {
const sourceDir = '/source/dir';
const targetDir = '/target/dir';
const fileName = 'output.txt';

vi.mocked(fs.copyFile).mockResolvedValue();

await copyOutputToCurrentDirectory(sourceDir, targetDir, fileName);

expect(fs.copyFile).toHaveBeenCalledWith(path.join(sourceDir, fileName), path.join(targetDir, fileName));
});

test('should throw error when copy fails', async () => {
const sourceDir = '/source/dir';
const targetDir = '/target/dir';
const fileName = 'output.txt';

vi.mocked(fs.copyFile).mockRejectedValue(new Error('Permission denied'));

it('should not modify URLs from other Git hosting services', () => {
expect(formatGitUrl('https://gitlab.com/user/repo.git')).toBe('https://gitlab.com/user/repo.git');
expect(formatGitUrl('https://bitbucket.org/user/repo.git')).toBe('https://bitbucket.org/user/repo.git');
await expect(copyOutputToCurrentDirectory(sourceDir, targetDir, fileName)).rejects.toThrow(
'Failed to copy output file',
);
});
});
});
Loading