mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(cli): resolve npx shims on Windows (#1626)
This commit is contained in:
@@ -32,6 +32,7 @@ jobs:
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
cli: ${{ steps.filter.outputs.cli }}
|
||||
steps:
|
||||
# Force git-based change detection instead of the pull_request REST API.
|
||||
# The API path can fail the whole workflow on transient listFiles
|
||||
@@ -52,6 +53,11 @@ jobs:
|
||||
- "tsconfig*.json"
|
||||
- "Dockerfile*"
|
||||
- ".github/workflows/**"
|
||||
cli:
|
||||
- "packages/cli/**"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- ".github/workflows/ci.yml"
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -218,6 +224,30 @@ jobs:
|
||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||
- run: bun run --filter '!@hyperframes/producer' test
|
||||
|
||||
cli-npx-shim:
|
||||
name: "CLI: npx shim (${{ matrix.os }})"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.cli == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
if: runner.os != 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
- name: Install dependencies
|
||||
if: runner.os == 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts --linker=hoisted
|
||||
- run: bun run --cwd packages/cli test src/utils/npxCommand.test.ts src/commands/skills.test.ts
|
||||
|
||||
sdk-tests:
|
||||
name: "SDK: unit + contract + smoke"
|
||||
needs: changes
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createRequire } from "node:module";
|
||||
import * as clack from "@clack/prompts";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { isDevMode } from "../utils/env.js";
|
||||
import { buildNpxCommand } from "../utils/npxCommand.js";
|
||||
import {
|
||||
openBrowser,
|
||||
parseRemoteDebuggingPort,
|
||||
@@ -377,7 +378,8 @@ async function runLocalStudioMode(
|
||||
const s = clack.spinner();
|
||||
s.start("Starting studio...");
|
||||
|
||||
const child = spawn("npx", ["vite"], {
|
||||
const viteCommand = buildNpxCommand(["vite"]);
|
||||
const child = spawn(viteCommand.command, viteCommand.args, {
|
||||
cwd: studioPkgPath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
@@ -11,13 +11,25 @@ type SpawnCall = {
|
||||
env: NodeJS.ProcessEnv | undefined;
|
||||
};
|
||||
|
||||
const state: { calls: SpawnCall[] } = { calls: [] };
|
||||
type ExecCall = {
|
||||
command: string;
|
||||
args: ReadonlyArray<string>;
|
||||
};
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
const state: { execCalls: ExecCall[]; spawnCalls: SpawnCall[] } = {
|
||||
execCalls: [],
|
||||
spawnCalls: [],
|
||||
};
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execFileSync: vi.fn(() => Buffer.from("11.0.0")),
|
||||
execFileSync: vi.fn((command: string, args: ReadonlyArray<string>) => {
|
||||
state.execCalls.push({ command, args });
|
||||
return Buffer.from("11.0.0");
|
||||
}),
|
||||
spawn: vi.fn(
|
||||
(command: string, args: ReadonlyArray<string>, opts?: { env?: NodeJS.ProcessEnv }) => {
|
||||
state.calls.push({ command, args, env: opts?.env });
|
||||
state.spawnCalls.push({ command, args, env: opts?.env });
|
||||
const fake = new EventEmitter();
|
||||
setImmediate(() => fake.emit("close", 0, null));
|
||||
return fake;
|
||||
@@ -25,25 +37,66 @@ vi.mock("node:child_process", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@clack/prompts", () => ({
|
||||
log: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function setPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: platform,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("hyperframes skills", () => {
|
||||
beforeEach(() => {
|
||||
state.calls = [];
|
||||
state.execCalls = [];
|
||||
state.spawnCalls = [];
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setPlatform(originalPlatform);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("sets GIT_CLONE_PROTECTION_ACTIVE=0 on the spawned skills CLI child (GH #316)", async () => {
|
||||
setPlatform("linux");
|
||||
|
||||
const { default: skillsCmd } = await import("./skills.js");
|
||||
await skillsCmd.run?.({ args: {}, rawArgs: [], cmd: skillsCmd } as never);
|
||||
|
||||
const first = state.calls[0];
|
||||
const first = state.spawnCalls[0];
|
||||
expect(first).toBeDefined();
|
||||
expect(first!.command).toBe("npx");
|
||||
expect(first!.args).toContain("skills");
|
||||
expect(first!.args).toContain("add");
|
||||
expect(first!.env?.GIT_CLONE_PROTECTION_ACTIVE).toBe("0");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["linux", "npx", ["--version"], ["skills", "add", "heygen-com/hyperframes", "--all"]],
|
||||
["darwin", "npx", ["--version"], ["skills", "add", "heygen-com/hyperframes", "--all"]],
|
||||
[
|
||||
"win32",
|
||||
"cmd.exe",
|
||||
["/d", "/s", "/c", "npx.cmd", "--version"],
|
||||
["/d", "/s", "/c", "npx.cmd", "skills", "add", "heygen-com/hyperframes", "--all"],
|
||||
],
|
||||
] as const)(
|
||||
"uses %s-compatible npx command for preflight and skills install",
|
||||
async (platform, expectedCommand, expectedPreflightArgs, expectedInstallArgs) => {
|
||||
setPlatform(platform);
|
||||
|
||||
const { default: skillsCmd } = await import("./skills.js");
|
||||
await skillsCmd.run?.({ args: {}, rawArgs: [], cmd: skillsCmd } as never);
|
||||
|
||||
expect(state.execCalls[0]?.command).toBe(expectedCommand);
|
||||
expect(state.execCalls[0]?.args).toEqual(expectedPreflightArgs);
|
||||
expect(state.spawnCalls[0]?.command).toBe(expectedCommand);
|
||||
expect(state.spawnCalls[0]?.args).toEqual(expectedInstallArgs);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,10 +2,12 @@ import { defineCommand } from "citty";
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import * as clack from "@clack/prompts";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { buildNpxCommand } from "../utils/npxCommand.js";
|
||||
|
||||
function hasNpx(): boolean {
|
||||
const npx = buildNpxCommand(["--version"]);
|
||||
try {
|
||||
execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5000 });
|
||||
execFileSync(npx.command, npx.args, { stdio: "ignore", timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -13,8 +15,9 @@ function hasNpx(): boolean {
|
||||
}
|
||||
|
||||
function runSkillsAdd(repo: string): Promise<void> {
|
||||
const npx = buildNpxCommand(["skills", "add", repo, "--all"]);
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("npx", ["skills", "add", repo, "--all"], {
|
||||
const child = spawn(npx.command, npx.args, {
|
||||
stdio: "inherit",
|
||||
timeout: 120_000,
|
||||
// GH #316 — the upstream `skills` CLI shells out to `git clone`.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildNpxCommand } from "./npxCommand.js";
|
||||
|
||||
describe("buildNpxCommand", () => {
|
||||
it.each([
|
||||
["linux", "npx", ["--version"]],
|
||||
["darwin", "npx", ["--version"]],
|
||||
["win32", "cmd.exe", ["/d", "/s", "/c", "npx.cmd", "--version"]],
|
||||
] as const)("builds the %s npx invocation", (platform, expectedCommand, expectedArgs) => {
|
||||
expect(buildNpxCommand(["--version"], platform)).toEqual({
|
||||
command: expectedCommand,
|
||||
args: expectedArgs,
|
||||
});
|
||||
});
|
||||
|
||||
it("executes the host npx version check through the resolved command", () => {
|
||||
const npx = buildNpxCommand(["--version"]);
|
||||
const version = execFileSync(npx.command, npx.args, {
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
}).trim();
|
||||
|
||||
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export type NpxCommand = {
|
||||
command: string;
|
||||
args: string[];
|
||||
};
|
||||
|
||||
export function buildNpxCommand(
|
||||
args: readonly string[],
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): NpxCommand {
|
||||
if (platform === "win32") {
|
||||
// npm installs npx as a .cmd shim on Windows; invoke it through cmd.exe
|
||||
// instead of relying on child_process to resolve or execute the shim.
|
||||
return { command: "cmd.exe", args: ["/d", "/s", "/c", "npx.cmd", ...args] };
|
||||
}
|
||||
|
||||
return { command: "npx", args: [...args] };
|
||||
}
|
||||
Reference in New Issue
Block a user