mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
fix(cli): resolve npx shims on Windows (#1626)
This commit is contained in:
@@ -32,6 +32,7 @@ jobs:
|
|||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
outputs:
|
outputs:
|
||||||
code: ${{ steps.filter.outputs.code }}
|
code: ${{ steps.filter.outputs.code }}
|
||||||
|
cli: ${{ steps.filter.outputs.cli }}
|
||||||
steps:
|
steps:
|
||||||
# Force git-based change detection instead of the pull_request REST API.
|
# Force git-based change detection instead of the pull_request REST API.
|
||||||
# The API path can fail the whole workflow on transient listFiles
|
# The API path can fail the whole workflow on transient listFiles
|
||||||
@@ -52,6 +53,11 @@ jobs:
|
|||||||
- "tsconfig*.json"
|
- "tsconfig*.json"
|
||||||
- "Dockerfile*"
|
- "Dockerfile*"
|
||||||
- ".github/workflows/**"
|
- ".github/workflows/**"
|
||||||
|
cli:
|
||||||
|
- "packages/cli/**"
|
||||||
|
- "package.json"
|
||||||
|
- "bun.lock"
|
||||||
|
- ".github/workflows/ci.yml"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@@ -218,6 +224,30 @@ jobs:
|
|||||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||||
- run: bun run --filter '!@hyperframes/producer' test
|
- 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:
|
sdk-tests:
|
||||||
name: "SDK: unit + contract + smoke"
|
name: "SDK: unit + contract + smoke"
|
||||||
needs: changes
|
needs: changes
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { createRequire } from "node:module";
|
|||||||
import * as clack from "@clack/prompts";
|
import * as clack from "@clack/prompts";
|
||||||
import { c } from "../ui/colors.js";
|
import { c } from "../ui/colors.js";
|
||||||
import { isDevMode } from "../utils/env.js";
|
import { isDevMode } from "../utils/env.js";
|
||||||
|
import { buildNpxCommand } from "../utils/npxCommand.js";
|
||||||
import {
|
import {
|
||||||
openBrowser,
|
openBrowser,
|
||||||
parseRemoteDebuggingPort,
|
parseRemoteDebuggingPort,
|
||||||
@@ -377,7 +378,8 @@ async function runLocalStudioMode(
|
|||||||
const s = clack.spinner();
|
const s = clack.spinner();
|
||||||
s.start("Starting studio...");
|
s.start("Starting studio...");
|
||||||
|
|
||||||
const child = spawn("npx", ["vite"], {
|
const viteCommand = buildNpxCommand(["vite"]);
|
||||||
|
const child = spawn(viteCommand.command, viteCommand.args, {
|
||||||
cwd: studioPkgPath,
|
cwd: studioPkgPath,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,13 +11,25 @@ type SpawnCall = {
|
|||||||
env: NodeJS.ProcessEnv | undefined;
|
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", () => ({
|
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(
|
spawn: vi.fn(
|
||||||
(command: string, args: ReadonlyArray<string>, opts?: { env?: NodeJS.ProcessEnv }) => {
|
(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();
|
const fake = new EventEmitter();
|
||||||
setImmediate(() => fake.emit("close", 0, null));
|
setImmediate(() => fake.emit("close", 0, null));
|
||||||
return fake;
|
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", () => {
|
describe("hyperframes skills", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.calls = [];
|
state.execCalls = [];
|
||||||
|
state.spawnCalls = [];
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
setPlatform(originalPlatform);
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets GIT_CLONE_PROTECTION_ACTIVE=0 on the spawned skills CLI child (GH #316)", async () => {
|
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");
|
const { default: skillsCmd } = await import("./skills.js");
|
||||||
await skillsCmd.run?.({ args: {}, rawArgs: [], cmd: skillsCmd } as never);
|
await skillsCmd.run?.({ args: {}, rawArgs: [], cmd: skillsCmd } as never);
|
||||||
|
|
||||||
const first = state.calls[0];
|
const first = state.spawnCalls[0];
|
||||||
expect(first).toBeDefined();
|
expect(first).toBeDefined();
|
||||||
expect(first!.command).toBe("npx");
|
expect(first!.command).toBe("npx");
|
||||||
expect(first!.args).toContain("skills");
|
expect(first!.args).toContain("skills");
|
||||||
expect(first!.args).toContain("add");
|
expect(first!.args).toContain("add");
|
||||||
expect(first!.env?.GIT_CLONE_PROTECTION_ACTIVE).toBe("0");
|
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 { execFileSync, spawn } from "node:child_process";
|
||||||
import * as clack from "@clack/prompts";
|
import * as clack from "@clack/prompts";
|
||||||
import { c } from "../ui/colors.js";
|
import { c } from "../ui/colors.js";
|
||||||
|
import { buildNpxCommand } from "../utils/npxCommand.js";
|
||||||
|
|
||||||
function hasNpx(): boolean {
|
function hasNpx(): boolean {
|
||||||
|
const npx = buildNpxCommand(["--version"]);
|
||||||
try {
|
try {
|
||||||
execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5000 });
|
execFileSync(npx.command, npx.args, { stdio: "ignore", timeout: 5000 });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -13,8 +15,9 @@ function hasNpx(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runSkillsAdd(repo: string): Promise<void> {
|
function runSkillsAdd(repo: string): Promise<void> {
|
||||||
|
const npx = buildNpxCommand(["skills", "add", repo, "--all"]);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn("npx", ["skills", "add", repo, "--all"], {
|
const child = spawn(npx.command, npx.args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
timeout: 120_000,
|
timeout: 120_000,
|
||||||
// GH #316 — the upstream `skills` CLI shells out to `git clone`.
|
// 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