MCPcopy Create free account
hub / github.com/dyoshikawa/rulesync / fetchSkillFiles

Function fetchSkillFiles

src/lib/git-client.ts:127–196  ·  view source on GitHub ↗
(params: {
  url: string;
  ref: string;
  skillsPath: string;
  logger?: Logger;
})

Source from the content-addressed store, hash-verified

125 * `git clone --branch` does not accept raw SHAs.
126 */
127export 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 });

Callers 2

fetchSourceViaGitFunction · 0.85
git-client.test.tsFile · 0.85

Calls 8

validateGitUrlFunction · 0.85
validateRefFunction · 0.85
findControlCharacterFunction · 0.85
checkGitAvailableFunction · 0.85
createTempDirectoryFunction · 0.85
directoryExistsFunction · 0.85
walkDirectoryFunction · 0.85
removeTempDirectoryFunction · 0.85

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…