(params: {
url: string;
ref: string;
skillsPath: string;
logger?: Logger;
})
| 125 | * `git clone --branch` does not accept raw SHAs. |
| 126 | */ |
| 127 | export async function fetchSkillFiles(params: { |
| 128 | url: string; |
| 129 | ref: string; |
| 130 | skillsPath: string; |
| 131 | logger?: Logger; |
| 132 | }): Promise<Array<{ relativePath: string; content: string; size: number }>> { |
| 133 | const { url, ref, skillsPath, logger } = params; |
| 134 | validateGitUrl(url, { logger }); |
| 135 | validateRef(ref); |
| 136 | if (skillsPath.split(/[/\\]/).includes("..") || isAbsolute(skillsPath)) { |
| 137 | throw new GitClientError( |
| 138 | `Invalid skillsPath "${skillsPath}": must be a relative path without ".."`, |
| 139 | ); |
| 140 | } |
| 141 | const ctrl = findControlCharacter(skillsPath); |
| 142 | if (ctrl) { |
| 143 | throw new GitClientError( |
| 144 | `skillsPath contains control character ${ctrl.hex} at position ${ctrl.position}`, |
| 145 | ); |
| 146 | } |
| 147 | await checkGitAvailable(); |
| 148 | const tmpDir = await createTempDirectory("rulesync-git-"); |
| 149 | // Treat empty/"." paths as the repository root. Cone-mode sparse-checkout |
| 150 | // with such patterns only restores the top-level files (it intentionally |
| 151 | // excludes any subdirectory), so we must check out the entire working tree |
| 152 | // instead. Otherwise repositories whose skills live directly at the root |
| 153 | // (e.g. `<repo>/<skill-name>/SKILL.md` without a `skills/` container) |
| 154 | // would only yield root-level files like README.md. |
| 155 | // Normalize first so variants like "./.", ".//", or Windows ".\\" are also |
| 156 | // recognized as the root and don't fall back to the (buggy) sparse-checkout path. |
| 157 | const normalizedSkillsPath = posix.normalize(skillsPath.replace(/\\/g, "/")).replace(/\/+$/, ""); |
| 158 | const isRootPath = normalizedSkillsPath === "" || normalizedSkillsPath === "."; |
| 159 | try { |
| 160 | await execFileAsync( |
| 161 | "git", |
| 162 | [ |
| 163 | "clone", |
| 164 | "--depth", |
| 165 | "1", |
| 166 | "--branch", |
| 167 | ref, |
| 168 | "--no-checkout", |
| 169 | "--filter=blob:none", |
| 170 | "--", |
| 171 | url, |
| 172 | tmpDir, |
| 173 | ], |
| 174 | { timeout: GIT_TIMEOUT_MS }, |
| 175 | ); |
| 176 | if (isRootPath) { |
| 177 | // Disable sparse-checkout and restore the full tree. |
| 178 | await execFileAsync("git", ["-C", tmpDir, "sparse-checkout", "disable"], { |
| 179 | timeout: GIT_TIMEOUT_MS, |
| 180 | }); |
| 181 | } else { |
| 182 | await execFileAsync("git", ["-C", tmpDir, "sparse-checkout", "set", "--", skillsPath], { |
| 183 | timeout: GIT_TIMEOUT_MS, |
| 184 | }); |
no test coverage detected
searching dependent graphs…