Files
hyperframes/packages/cli/src/commands/skills.ts
T
Miguel Ángel 37370e1e7d fix(cli): set GIT_CLONE_PROTECTION_ACTIVE=0 for skills (GH #316) (#328)
## Summary

Fixes #316 — `hyperframes skills` (and `npx skills add heygen-com/hyperframes`) fails with:

```
■  Failed to clone repository
fatal: active \`post-checkout\` hook found during \`git clone\`
└  Installation failed
```

## Root cause

Two layers stacked:

1. **Git 2.45+ refuses to execute hooks during `git clone` by default.** The opt-in is `GIT_CLONE_PROTECTION_ACTIVE=0` — the env-var name is intentionally explicit about the trade-off.
2. **Users who ran `git lfs install` globally have a post-checkout hook registered at `core.hooksPath`.** When the upstream `skills` CLI shells out to `git clone` to fetch a repo's `skills/` directory, git detects the user's LFS hook and aborts.

The check fires for **any repo**, regardless of whether the cloned repo uses LFS itself — it's protection against the user's own hooks, not the repo's content. Users who have git-lfs installed (very common) hit this for every clone the `skills` CLI does.

## The fix

`hyperframes skills` wraps `npx skills add`. The wrapper now sets `GIT_CLONE_PROTECTION_ACTIVE=0` on the spawned child's env via a single helper (`gitCloneFriendlyEnv`) with a docstring at the call site explaining exactly why. The rest of `process.env` is preserved — proxy settings, extra CA certs, locale, etc. stay untouched.

## What this fix doesn't do (deliberately)

This is the **code-path-we-own** fix. The deeper root cause is that the upstream `skills` CLI (vercel-labs/skills) should set this env var when it shells out to `git clone`. That would fix the bug for every user invoking `skills` directly — not just those who route through our wrapper. An upstream issue should be opened separately; not landing it as part of this PR.

## Users who call `npx skills add` directly

Documented in the new troubleshooting subsection: set the env var manually.

```bash
GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes
```

## Tests

`packages/cli/src/commands/skills.test.ts` — 2 cases:
- Every spawned child has `GIT_CLONE_PROTECTION_ACTIVE=0`
- The rest of `process.env` is preserved (not a wiped env)

Uses `vi.mock` on `node:child_process` because ESM doesn't allow live-module `vi.spyOn` on re-exported bindings.

## Docs

`docs/packages/cli.mdx` — new **Troubleshooting** subsection under the `skills` command. Explains both the automatic fix (`hyperframes skills` users are already covered) and the manual workaround (`npx skills add …` users).

## Closes

- #316
2026-04-18 21:17:26 +02:00

64 lines
2.0 KiB
TypeScript

import { defineCommand } from "citty";
import { execFileSync, spawn } from "node:child_process";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
function hasNpx(): boolean {
try {
execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5000 });
return true;
} catch {
return false;
}
}
function runSkillsAdd(repo: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn("npx", ["skills", "add", repo, "--all"], {
stdio: "inherit",
timeout: 120_000,
// GH #316 — the upstream `skills` CLI shells out to `git clone`.
// When Git's clone-hook protection is active (shipped on by
// default in 2.45.1, reverted in 2.45.2, still present on many
// corporate and CI setups), any globally-registered
// `git lfs install` post-checkout hook aborts the clone. The
// `repo` reaching this function is hardcoded in SOURCES below
// — no user input reaches the spawn — so opting out here is safe.
env: { ...process.env, GIT_CLONE_PROTECTION_ACTIVE: "0" },
});
child.on("close", (code, signal) => {
if (code === 0) resolve();
else if (signal === "SIGINT" || code === 130) process.exit(0);
else reject(new Error(`npx skills add exited with code ${code}`));
});
child.on("error", reject);
});
}
const SOURCES = [{ name: "HyperFrames", repo: "heygen-com/hyperframes" }];
export default defineCommand({
meta: {
name: "skills",
description: "Install HyperFrames skills for AI coding tools",
},
args: {},
async run() {
if (!hasNpx()) {
clack.log.error(c.error("npx not found. Install Node.js and retry."));
return;
}
for (const source of SOURCES) {
console.log();
console.log(c.bold(`Installing ${source.name} skills...`));
console.log();
try {
await runSkillsAdd(source.repo);
} catch {
console.log(c.dim(`${source.name} skills skipped`));
}
}
},
});