Skip to content
Open
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
77 changes: 74 additions & 3 deletions packages/cli/src/utils/skillsManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ import {
// `canonical: true` network path (see resolveLatestManifest) instead of an
// explicit local `source` — that's the whole point (it must NOT read a stale
// local repo manifest). Stub the two network boundaries it can reach so those
// tests stay fast and offline: `git ls-remote` (remoteHeadSha) always "fails"
// tests stay fast and offline: `git ls-remote` (remoteHeadSha) "fails" by default
// so it falls back to the branch URL, and `fetch` is stubbed per-test. `vi.mock`
// is hoisted above these imports regardless of source position. No existing
// test in this file omits `source`, so nothing else touches this mock.
// is hoisted above these imports regardless of source position. Tests that need
// ls-remote to answer queue their own `mockImplementationOnce`.
vi.mock("node:child_process", () => ({
execFile: vi.fn(
(
Expand Down Expand Up @@ -677,3 +677,74 @@ describe("checkSkills canonical bypass of the in-repo manifest shortcut", () =>
expect(res.skills.map((s) => s.name)).toEqual(["from-explicit-source"]);
});
});

describe("checkSkills remote HEAD lookup", () => {
beforeEach(() => {
// mockClear would keep a queued mockImplementationOnce; mockReset drops it.
vi.mocked(execFile).mockReset();
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
({
ok: true,
json: async () => ({ source: "owner/repo", skills: {} }),
}) as unknown as Response,
),
);
});
afterEach(() => {
vi.unstubAllGlobals();
});

it("asks git for main without any credential helper or askpass prompt", async () => {
await checkSkills({ source: "owner/repo", cwd: root, home: root });
expect(vi.mocked(execFile)).toHaveBeenCalledWith(
"git",
[
"-c",
"credential.helper=",
"ls-remote",
"https://github.com/owner/repo.git",
"refs/heads/main",
],
expect.objectContaining({
env: expect.objectContaining({
GIT_TERMINAL_PROMPT: "0",
GIT_ASKPASS: "",
GCM_INTERACTIVE: "never",
}),
}),
expect.any(Function),
);
});

it("pins a valid slug's manifest to the commit ls-remote reports", async () => {
const sha = "a".repeat(40);
// execFile is overloaded; this stub models only the callback form promisify calls.
const lsRemote = ((
_cmd: string,
_args: readonly string[],
_opts: unknown,
callback: (err: Error | null, out: { stdout: string; stderr: string }) => void,
) =>
callback(null, {
stdout: `${sha}\trefs/heads/main\n`,
stderr: "",
})) as unknown as typeof execFile;
vi.mocked(execFile).mockImplementationOnce(lsRemote);
await checkSkills({ source: "owner/repo", cwd: root, home: root });
expect(vi.mocked(fetch)).toHaveBeenCalledWith(
`https://raw.githubusercontent.com/owner/repo/${sha}/${MANIFEST_FILE}`,
expect.anything(),
);
});

it.each(["--source owner/repo", "owner", "owner/repo/extra", "owner/repo?x=1"])(
"never runs git for the malformed slug %j",
async (source) => {
await checkSkills({ source, cwd: root, home: root });
expect(vi.mocked(execFile)).not.toHaveBeenCalled();
},
);
});
29 changes: 23 additions & 6 deletions packages/cli/src/utils/skillsManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface SkillsCheckResult {
}

const DEFAULT_REPO_SLUG = "heygen-com/hyperframes";
const REPO_SLUG_PATTERN = /^[\w.-]+\/[\w.-]+$/;
/** Manifest filename, published at the repo root. */
export const MANIFEST_FILE = "skills-manifest.json";
const FETCH_TIMEOUT_MS = 4000;
Expand Down Expand Up @@ -588,19 +589,35 @@ async function fetchManifest(url: string): Promise<SkillsManifest> {
}

/**
* Resolve main's live HEAD sha via `git ls-remote`. GitHub's branch-raw CDN
* (raw.githubusercontent.com/<owner>/<repo>/main/...) can serve stale content
* for minutes after a push; a SHA-pinned raw URL is immediately consistent.
* Returns null when git/network is unavailable so callers fall back to main.
* Resolve main's live HEAD sha via `git ls-remote`: the branch-raw CDN can serve
* stale content for minutes after a push, a SHA-pinned raw URL cannot. Returns
* null for a slug that is not `owner/repo`, or when git/network is unavailable.
*
* GIT_TERMINAL_PROMPT=0 does not stop credential helpers or askpass, and Git
* Credential Manager (the Git for Windows default) answers a private or missing
* repo's auth challenge with a blocking sign-in window. An empty credential.helper
* clears every helper; an empty GIT_ASKPASS also skips core.askPass and SSH_ASKPASS.
*/
async function remoteHeadSha(repoSlug: string): Promise<string | null> {
if (!REPO_SLUG_PATTERN.test(repoSlug)) return null;
try {
const { stdout } = await execFileAsync(
"git",
["ls-remote", `https://github.com/${repoSlug}.git`, "refs/heads/main"],
[
"-c",
"credential.helper=",
"ls-remote",
`https://github.com/${repoSlug}.git`,
"refs/heads/main",
],
{
timeout: FETCH_TIMEOUT_MS,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
env: {
...process.env,
GIT_TERMINAL_PROMPT: "0",
GIT_ASKPASS: "",
GCM_INTERACTIVE: "never",
},
windowsHide: true,
},
);
Expand Down
Loading