From 540e8dd2a3b5efd759c37504b85d6aabdebf6e7b Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sun, 8 Feb 2026 22:51:01 +0900 Subject: [PATCH 1/3] feat(cli): Auto-detect explicit remote URLs in positional arguments Allow users to run `repomix https://github.com/user/repo` or `repomix git@github.com:user/repo.git` without the `--remote` flag. Only explicit URL formats (https:// and git@) are auto-detected. Shorthand format (owner/repo) is not auto-detected to avoid ambiguity with local directory paths. Closes #1120 --- src/cli/cliRun.ts | 7 +++++ src/core/git/gitRemoteParse.ts | 8 ++++++ src/index.ts | 7 ++++- tests/cli/cliRun.test.ts | 29 +++++++++++++++++++ tests/core/git/gitRemoteParse.test.ts | 41 ++++++++++++++++++++++++++- 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/cli/cliRun.ts b/src/cli/cliRun.ts index a97ab2aca..143b8ba8b 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:// and 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..622f6bd6d 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 (https:// or git@). + * This intentionally does NOT match shorthand (owner/repo) to avoid ambiguity with local directory paths. + */ +export const isExplicitRemoteUrl = (value: string): boolean => { + return value.startsWith('https://') || value.startsWith('git@'); +}; + 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..0b5918bdf 100644 --- a/tests/cli/cliRun.test.ts +++ b/tests/cli/cliRun.test.ts @@ -209,6 +209,35 @@ 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 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..19de7ce57 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,40 @@ 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 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); From aef7cc1f4a59154cf9a2e6ffc6e1e6174d496dc0 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Tue, 17 Feb 2026 22:54:03 +0900 Subject: [PATCH 2/3] feat(cli): Add ssh:// and git:// protocol support to remote URL auto-detection The existing --remote flag already supports ssh:// and git:// protocols via git-url-parse, so auto-detection should cover them as well. --- src/cli/cliRun.ts | 2 +- src/core/git/gitRemoteParse.ts | 4 ++-- tests/cli/cliRun.test.ts | 20 ++++++++++++++++++++ tests/core/git/gitRemoteParse.test.ts | 10 ++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/cli/cliRun.ts b/src/cli/cliRun.ts index 143b8ba8b..384af84d6 100644 --- a/src/cli/cliRun.ts +++ b/src/cli/cliRun.ts @@ -276,7 +276,7 @@ export const runCli = async (directories: string[], cwd: string, options: CliOpt return await runRemoteAction(options.remote, options); } - // Auto-detect explicit remote URLs (https:// and git@) in positional arguments + // 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); diff --git a/src/core/git/gitRemoteParse.ts b/src/core/git/gitRemoteParse.ts index 622f6bd6d..fc027d022 100644 --- a/src/core/git/gitRemoteParse.ts +++ b/src/core/git/gitRemoteParse.ts @@ -113,11 +113,11 @@ export const parseRemoteValue = ( }; /** - * Checks if a string is an explicit remote URL (https:// or git@). + * 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 value.startsWith('https://') || value.startsWith('git@'); + return ['https://', 'git@', 'ssh://', 'git://'].some((prefix) => value.startsWith(prefix)); }; export const isValidRemoteValue = (remoteValue: string, refs: string[] = []): boolean => { diff --git a/tests/cli/cliRun.test.ts b/tests/cli/cliRun.test.ts index 0b5918bdf..0fcd2d53f 100644 --- a/tests/cli/cliRun.test.ts +++ b/tests/cli/cliRun.test.ts @@ -224,6 +224,26 @@ describe('cliRun', () => { 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(), {}); diff --git a/tests/core/git/gitRemoteParse.test.ts b/tests/core/git/gitRemoteParse.test.ts index 19de7ce57..643ffd92a 100644 --- a/tests/core/git/gitRemoteParse.test.ts +++ b/tests/core/git/gitRemoteParse.test.ts @@ -282,6 +282,16 @@ describe('remoteAction functions', () => { 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); From 1d5297c9a6cdfafcf2380b0b5e26504773d01735 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:55:26 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- tests/cli/cliRun.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/cli/cliRun.test.ts b/tests/cli/cliRun.test.ts index 0fcd2d53f..aeab8f4c9 100644 --- a/tests/cli/cliRun.test.ts +++ b/tests/cli/cliRun.test.ts @@ -237,10 +237,7 @@ describe('cliRun', () => { 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(remoteAction.runRemoteAction).toHaveBeenCalledWith('git://github.com/user/repo.git', expect.any(Object)); expect(defaultAction.runDefaultAction).not.toHaveBeenCalled(); });