Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"files": 17
},
"hyperframes-animation": {
"hash": "a5be218c0b0fe377",
"hash": "4252b08a4905b5ba",
"files": 121
},
"hyperframes-audio": {
Expand All @@ -38,7 +38,7 @@
"files": 20
},
"hyperframes-creative": {
"hash": "68d6b293b011b5e8",
"hash": "f3b201ca29eae92a",
"files": 78
},
"hyperframes-keyframes": {
Expand Down
63 changes: 47 additions & 16 deletions skills/hyperframes-animation/scripts/package-loader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions skills/hyperframes-animation/scripts/package-loader.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
63 changes: 47 additions & 16 deletions skills/hyperframes-creative/scripts/package-loader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions skills/hyperframes-creative/scripts/package-loader.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading