mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(skills): bootstrap npm safely on Windows (#3705)
* fix(skills): bootstrap npm safely on Windows * test(skills): verify npm bootstrap on Windows Node 24
This commit is contained in:
@@ -364,6 +364,23 @@ jobs:
|
||||
printf ' * %s\n' "${SKILLS_TESTS[@]}"
|
||||
node --test "${SKILLS_TESTS[@]}"
|
||||
|
||||
test-skill-bootstrap-windows:
|
||||
name: "Test: skill bootstrap (Windows, Node 24)"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.skills == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Exercise npm-cli bootstrap command
|
||||
run: >-
|
||||
node --test
|
||||
skills/hyperframes-animation/scripts/package-loader.test.mjs
|
||||
skills/hyperframes-creative/scripts/package-loader.test.mjs
|
||||
|
||||
# Guards that skills-manifest.json (the published freshness fingerprint read
|
||||
# by `hyperframes skills check`) was regenerated when a skill changed. Runs
|
||||
# `gen:skills-manifest --check`, which compares per-skill content hashes; the
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"files": 17
|
||||
},
|
||||
"hyperframes-animation": {
|
||||
"hash": "a5be218c0b0fe377",
|
||||
"hash": "4252b08a4905b5ba",
|
||||
"files": 121
|
||||
},
|
||||
"hyperframes-audio": {
|
||||
@@ -38,7 +38,7 @@
|
||||
"files": 20
|
||||
},
|
||||
"hyperframes-creative": {
|
||||
"hash": "68d6b293b011b5e8",
|
||||
"hash": "f3b201ca29eae92a",
|
||||
"files": 78
|
||||
},
|
||||
"hyperframes-keyframes": {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
|
||||
import { basename, delimiter, dirname, join, parse, resolve, win32 as win32Path } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
@@ -339,23 +339,54 @@ function ancestors(start) {
|
||||
return dirs;
|
||||
}
|
||||
|
||||
export function resolveNpmSpawnCommand(
|
||||
args,
|
||||
platform = process.platform,
|
||||
env = process.env,
|
||||
nodeExecPath = process.execPath,
|
||||
pathExists = existsSync,
|
||||
) {
|
||||
if (platform !== "win32") {
|
||||
return { cmd: "npm", args, opts: { stdio: "inherit" } };
|
||||
}
|
||||
|
||||
const bundledNpmCli = win32Path.join(
|
||||
win32Path.dirname(nodeExecPath),
|
||||
"node_modules",
|
||||
"npm",
|
||||
"bin",
|
||||
"npm-cli.js",
|
||||
);
|
||||
const npmCli = [env.npm_execpath, bundledNpmCli].find(
|
||||
(candidate) => candidate && pathExists(candidate),
|
||||
);
|
||||
if (!npmCli) return null;
|
||||
return {
|
||||
cmd: env.npm_node_execpath || nodeExecPath,
|
||||
args: [npmCli, ...args],
|
||||
opts: { stdio: "inherit", windowsHide: true },
|
||||
};
|
||||
}
|
||||
|
||||
function bootstrapWithNpmInstall(packageNames) {
|
||||
const installRoot = mkdtempSync(join(tmpdir(), "hyperframes-skill-deps-"));
|
||||
const installResult = spawnSync(
|
||||
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
[
|
||||
"install",
|
||||
"--silent",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--ignore-scripts",
|
||||
"--no-save",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
...packageNames,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const npmArgs = [
|
||||
"install",
|
||||
"--silent",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--ignore-scripts",
|
||||
"--no-save",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
...packageNames,
|
||||
];
|
||||
const npmCommand = resolveNpmSpawnCommand(npmArgs);
|
||||
if (!npmCommand) {
|
||||
rmSync(installRoot, { recursive: true, force: true });
|
||||
throw new Error("Could not locate npm-cli.js for dependency bootstrap on Windows.");
|
||||
}
|
||||
const installResult = spawnSync(npmCommand.cmd, npmCommand.args, npmCommand.opts);
|
||||
|
||||
if (installResult.error) throw installResult.error;
|
||||
if (installResult.status !== 0) {
|
||||
|
||||
@@ -5,10 +5,62 @@ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveNpmSpawnCommand } from "./package-loader.mjs";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ENV = "HYPERFRAMES_SKILL_PKG_VERSION";
|
||||
|
||||
test("resolveNpmSpawnCommand routes Windows npm through node and npm-cli.js", () => {
|
||||
const npmCli = "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
||||
const node = "C:\\Program Files\\nodejs\\node.exe";
|
||||
const resolved = resolveNpmSpawnCommand(
|
||||
["install", "@hyperframes/producer@0.7.55", "value & calc"],
|
||||
"win32",
|
||||
{ npm_execpath: npmCli, npm_node_execpath: node },
|
||||
node,
|
||||
(path) => path === npmCli,
|
||||
);
|
||||
|
||||
assert.deepEqual(resolved, {
|
||||
cmd: node,
|
||||
args: [npmCli, "install", "@hyperframes/producer@0.7.55", "value & calc"],
|
||||
opts: { stdio: "inherit", windowsHide: true },
|
||||
});
|
||||
assert.equal(resolved.opts.shell, undefined);
|
||||
});
|
||||
|
||||
test("resolveNpmSpawnCommand finds npm-cli.js beside node for direct Windows runs", () => {
|
||||
const node = "C:\\Program Files\\nodejs\\node.exe";
|
||||
const npmCli = "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
||||
const resolved = resolveNpmSpawnCommand(
|
||||
["install", "@hyperframes/producer@0.7.55"],
|
||||
"win32",
|
||||
{},
|
||||
node,
|
||||
(path) => path === npmCli,
|
||||
);
|
||||
|
||||
assert.equal(resolved?.cmd, node);
|
||||
assert.deepEqual(resolved?.args, [npmCli, "install", "@hyperframes/producer@0.7.55"]);
|
||||
});
|
||||
|
||||
test(
|
||||
"resolveNpmSpawnCommand launches the installed npm CLI on Windows",
|
||||
{ skip: process.platform !== "win32" },
|
||||
() => {
|
||||
const resolved = resolveNpmSpawnCommand(["--version"]);
|
||||
assert.ok(resolved);
|
||||
|
||||
const result = spawnSync(resolved.cmd, resolved.args, {
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout.trim(), /^\d+\./);
|
||||
},
|
||||
);
|
||||
|
||||
// (a) env override wins — no ancestor lookup, exact version echoed back.
|
||||
test("hyperframesPackageSpec: env override wins", async () => {
|
||||
const prev = process.env[ENV];
|
||||
|
||||
@@ -12,7 +12,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
|
||||
import { basename, delimiter, dirname, join, parse, resolve, win32 as win32Path } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
@@ -339,23 +339,54 @@ function ancestors(start) {
|
||||
return dirs;
|
||||
}
|
||||
|
||||
export function resolveNpmSpawnCommand(
|
||||
args,
|
||||
platform = process.platform,
|
||||
env = process.env,
|
||||
nodeExecPath = process.execPath,
|
||||
pathExists = existsSync,
|
||||
) {
|
||||
if (platform !== "win32") {
|
||||
return { cmd: "npm", args, opts: { stdio: "inherit" } };
|
||||
}
|
||||
|
||||
const bundledNpmCli = win32Path.join(
|
||||
win32Path.dirname(nodeExecPath),
|
||||
"node_modules",
|
||||
"npm",
|
||||
"bin",
|
||||
"npm-cli.js",
|
||||
);
|
||||
const npmCli = [env.npm_execpath, bundledNpmCli].find(
|
||||
(candidate) => candidate && pathExists(candidate),
|
||||
);
|
||||
if (!npmCli) return null;
|
||||
return {
|
||||
cmd: env.npm_node_execpath || nodeExecPath,
|
||||
args: [npmCli, ...args],
|
||||
opts: { stdio: "inherit", windowsHide: true },
|
||||
};
|
||||
}
|
||||
|
||||
function bootstrapWithNpmInstall(packageNames) {
|
||||
const installRoot = mkdtempSync(join(tmpdir(), "hyperframes-skill-deps-"));
|
||||
const installResult = spawnSync(
|
||||
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
[
|
||||
"install",
|
||||
"--silent",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--ignore-scripts",
|
||||
"--no-save",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
...packageNames,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const npmArgs = [
|
||||
"install",
|
||||
"--silent",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--ignore-scripts",
|
||||
"--no-save",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
...packageNames,
|
||||
];
|
||||
const npmCommand = resolveNpmSpawnCommand(npmArgs);
|
||||
if (!npmCommand) {
|
||||
rmSync(installRoot, { recursive: true, force: true });
|
||||
throw new Error("Could not locate npm-cli.js for dependency bootstrap on Windows.");
|
||||
}
|
||||
const installResult = spawnSync(npmCommand.cmd, npmCommand.args, npmCommand.opts);
|
||||
|
||||
if (installResult.error) throw installResult.error;
|
||||
if (installResult.status !== 0) {
|
||||
|
||||
@@ -5,10 +5,62 @@ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveNpmSpawnCommand } from "./package-loader.mjs";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ENV = "HYPERFRAMES_SKILL_PKG_VERSION";
|
||||
|
||||
test("resolveNpmSpawnCommand routes Windows npm through node and npm-cli.js", () => {
|
||||
const npmCli = "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
||||
const node = "C:\\Program Files\\nodejs\\node.exe";
|
||||
const resolved = resolveNpmSpawnCommand(
|
||||
["install", "@hyperframes/producer@0.7.55", "value & calc"],
|
||||
"win32",
|
||||
{ npm_execpath: npmCli, npm_node_execpath: node },
|
||||
node,
|
||||
(path) => path === npmCli,
|
||||
);
|
||||
|
||||
assert.deepEqual(resolved, {
|
||||
cmd: node,
|
||||
args: [npmCli, "install", "@hyperframes/producer@0.7.55", "value & calc"],
|
||||
opts: { stdio: "inherit", windowsHide: true },
|
||||
});
|
||||
assert.equal(resolved.opts.shell, undefined);
|
||||
});
|
||||
|
||||
test("resolveNpmSpawnCommand finds npm-cli.js beside node for direct Windows runs", () => {
|
||||
const node = "C:\\Program Files\\nodejs\\node.exe";
|
||||
const npmCli = "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
||||
const resolved = resolveNpmSpawnCommand(
|
||||
["install", "@hyperframes/producer@0.7.55"],
|
||||
"win32",
|
||||
{},
|
||||
node,
|
||||
(path) => path === npmCli,
|
||||
);
|
||||
|
||||
assert.equal(resolved?.cmd, node);
|
||||
assert.deepEqual(resolved?.args, [npmCli, "install", "@hyperframes/producer@0.7.55"]);
|
||||
});
|
||||
|
||||
test(
|
||||
"resolveNpmSpawnCommand launches the installed npm CLI on Windows",
|
||||
{ skip: process.platform !== "win32" },
|
||||
() => {
|
||||
const resolved = resolveNpmSpawnCommand(["--version"]);
|
||||
assert.ok(resolved);
|
||||
|
||||
const result = spawnSync(resolved.cmd, resolved.args, {
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout.trim(), /^\d+\./);
|
||||
},
|
||||
);
|
||||
|
||||
// (a) env override wins — no ancestor lookup, exact version echoed back.
|
||||
test("hyperframesPackageSpec: env override wins", async () => {
|
||||
const prev = process.env[ENV];
|
||||
|
||||
Reference in New Issue
Block a user