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
7 changes: 7 additions & 0 deletions src/cli/cliRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import process from 'node:process';
import { Option, program } from 'commander';
import pc from 'picocolors';
import { getVersion } from '../core/file/packageJsonParse.js';
import { isExplicitRemoteUrl } from '../core/git/gitRemoteParse.js';
import { handleError, RepomixError } from '../shared/errorHandle.js';
import { logger, repomixLogLevels } from '../shared/logger.js';
import { parseHumanSizeToBytes } from '../shared/sizeParse.js';
Expand Down Expand Up @@ -275,5 +276,11 @@ export const runCli = async (directories: string[], cwd: string, options: CliOpt
return await runRemoteAction(options.remote, options);
}

// Auto-detect explicit remote URLs (https://, git@, ssh://, git://) in positional arguments
if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) {
logger.trace(`Auto-detected remote URL from positional argument: ${directories[0]}`);
return await runRemoteAction(directories[0], options);
}

return await runDefaultAction(directories, cwd, options);
};
8 changes: 8 additions & 0 deletions src/core/git/gitRemoteParse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export const parseRemoteValue = (
}
};

/**
* Checks if a string is an explicit remote URL (e.g., https://, git@, ssh://, git://).
* This intentionally does NOT match shorthand (owner/repo) to avoid ambiguity with local directory paths.
*/
export const isExplicitRemoteUrl = (value: string): boolean => {
return ['https://', 'git@', 'ssh://', 'git://'].some((prefix) => value.startsWith(prefix));
};
Comment thread
yamadashy marked this conversation as resolved.

export const isValidRemoteValue = (remoteValue: string, refs: string[] = []): boolean => {
try {
parseRemoteValue(remoteValue, refs);
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export type { FileSearchResult } from './core/file/fileSearch.js';
export { generateFileTree, generateTreeString, treeToString, type TreeNode } from './core/file/fileTreeGenerate.js';

// Git
export { isValidRemoteValue, isValidShorthand, parseRemoteValue } from './core/git/gitRemoteParse.js';
export {
isExplicitRemoteUrl,
isValidRemoteValue,
isValidShorthand,
parseRemoteValue,
} from './core/git/gitRemoteParse.js';

// Security
export { runSecurityCheck } from './core/security/securityCheck.js';
Expand Down
46 changes: 46 additions & 0 deletions tests/cli/cliRun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,52 @@ describe('cliRun', () => {
expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('yamadashy/repomix', expect.any(Object));
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
});

test('should auto-detect HTTPS URL and execute remote action', async () => {
await runCli(['https://github.com/user/repo'], process.cwd(), {});

expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('https://github.com/user/repo', expect.any(Object));
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
});

test('should auto-detect git@ SSH URL and execute remote action', async () => {
await runCli(['git@github.com:user/repo.git'], process.cwd(), {});

expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('git@github.com:user/repo.git', expect.any(Object));
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
});

test('should auto-detect ssh:// URL and execute remote action', async () => {
await runCli(['ssh://git@github.com/user/repo.git'], process.cwd(), {});

expect(remoteAction.runRemoteAction).toHaveBeenCalledWith(
'ssh://git@github.com/user/repo.git',
expect.any(Object),
);
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
});

test('should auto-detect git:// URL and execute remote action', async () => {
await runCli(['git://github.com/user/repo.git'], process.cwd(), {});

expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('git://github.com/user/repo.git', expect.any(Object));
expect(defaultAction.runDefaultAction).not.toHaveBeenCalled();
});

test('should not auto-detect shorthand format as remote URL', async () => {
await runCli(['user/repo'], process.cwd(), {});

expect(defaultAction.runDefaultAction).toHaveBeenCalledWith(['user/repo'], process.cwd(), expect.any(Object));
expect(remoteAction.runRemoteAction).not.toHaveBeenCalled();
});

test('should prioritize explicit --remote flag over auto-detected URL', async () => {
await runCli(['https://github.com/other/repo'], process.cwd(), {
remote: 'yamadashy/repomix',
});

expect(remoteAction.runRemoteAction).toHaveBeenCalledWith('yamadashy/repomix', expect.any(Object));
});
});

describe('parsable style flag', () => {
Expand Down
51 changes: 50 additions & 1 deletion tests/core/git/gitRemoteParse.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { isGitHubRepository, parseGitHubRepoInfo, parseRemoteValue } from '../../../src/core/git/gitRemoteParse.js';
import {
isExplicitRemoteUrl,
isGitHubRepository,
parseGitHubRepoInfo,
parseRemoteValue,
} from '../../../src/core/git/gitRemoteParse.js';
import { isValidRemoteValue } from '../../../src/index.js';

vi.mock('../../../src/shared/logger');
Expand Down Expand Up @@ -264,6 +269,50 @@ describe('remoteAction functions', () => {
});
});

describe('isExplicitRemoteUrl', () => {
test('should return true for HTTPS URLs', () => {
expect(isExplicitRemoteUrl('https://github.com/user/repo')).toBe(true);
expect(isExplicitRemoteUrl('https://gitlab.com/user/repo')).toBe(true);
expect(isExplicitRemoteUrl('https://bitbucket.org/user/repo')).toBe(true);
});

test('should return true for git@ SSH URLs', () => {
expect(isExplicitRemoteUrl('git@github.com:user/repo.git')).toBe(true);
expect(isExplicitRemoteUrl('git@gitlab.com:user/repo.git')).toBe(true);
expect(isExplicitRemoteUrl('git@ssh.dev.azure.com:v3/org/project/repo')).toBe(true);
});

test('should return true for ssh:// URLs', () => {
expect(isExplicitRemoteUrl('ssh://git@github.com/user/repo.git')).toBe(true);
expect(isExplicitRemoteUrl('ssh://git@gitlab.com/user/repo.git')).toBe(true);
});

test('should return true for git:// URLs', () => {
expect(isExplicitRemoteUrl('git://github.com/user/repo.git')).toBe(true);
expect(isExplicitRemoteUrl('git://gitlab.com/user/repo.git')).toBe(true);
});

test('should return false for shorthand format', () => {
expect(isExplicitRemoteUrl('user/repo')).toBe(false);
expect(isExplicitRemoteUrl('yamadashy/repomix')).toBe(false);
});

test('should return false for local paths', () => {
expect(isExplicitRemoteUrl('.')).toBe(false);
expect(isExplicitRemoteUrl('./src')).toBe(false);
expect(isExplicitRemoteUrl('/absolute/path')).toBe(false);
expect(isExplicitRemoteUrl('relative/path')).toBe(false);
});

test('should return false for http:// URLs', () => {
expect(isExplicitRemoteUrl('http://example.com/repo')).toBe(false);
});

test('should return false for empty string', () => {
expect(isExplicitRemoteUrl('')).toBe(false);
});
});

describe('isGitHubRepository', () => {
test('should return true for GitHub repositories', () => {
expect(isGitHubRepository('yamadashy/repomix')).toBe(true);
Expand Down
Loading