diff --git a/src/cli/cliRun.ts b/src/cli/cliRun.ts index a97ab2aca..384af84d6 100644 --- a/src/cli/cliRun.ts +++ b/src/cli/cliRun.ts @@ -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'; @@ -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); }; diff --git a/src/core/git/gitRemoteParse.ts b/src/core/git/gitRemoteParse.ts index 323294949..fc027d022 100644 --- a/src/core/git/gitRemoteParse.ts +++ b/src/core/git/gitRemoteParse.ts @@ -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)); +}; + export const isValidRemoteValue = (remoteValue: string, refs: string[] = []): boolean => { try { parseRemoteValue(remoteValue, refs); diff --git a/src/index.ts b/src/index.ts index 7a695c5d2..72a740ba1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/tests/cli/cliRun.test.ts b/tests/cli/cliRun.test.ts index 084f88416..aeab8f4c9 100644 --- a/tests/cli/cliRun.test.ts +++ b/tests/cli/cliRun.test.ts @@ -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', () => { diff --git a/tests/core/git/gitRemoteParse.test.ts b/tests/core/git/gitRemoteParse.test.ts index d8959d54f..643ffd92a 100644 --- a/tests/core/git/gitRemoteParse.test.ts +++ b/tests/core/git/gitRemoteParse.test.ts @@ -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'); @@ -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);