Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on Keep a Changelog and the project follows Semantic Version

## [Unreleased]

### Bug Fixes

- **create-rezi/cli**: Fixed Windows nested installs by switching `create-rezi` to the standard `cross-spawn` process launcher and by resolving npm installs through the active npm entrypoint instead of relying on Git Bash shell resolution.
- **create-rezi/minimal**: Replaced the invalid bare `+` keybinding in the minimal template with Windows-safe `=` / `shift+=` bindings while keeping `+` as an accepted command alias.

## [0.1.0-alpha.68] - 2026-04-15

### CI / Tooling
Expand Down
92 changes: 80 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/create-rezi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"bun": ">=1.3.0"
},
"devDependencies": {
"@rezi-ui/testkit": "0.1.0-alpha.61"
"@rezi-ui/testkit": "0.1.0-alpha.61",
"@types/cross-spawn": "^6.0.6"
},
"dependencies": {
"cross-spawn": "^7.0.6"
}
}
45 changes: 44 additions & 1 deletion packages/create-rezi/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolve } from "node:path";
import { assert, test } from "@rezi-ui/testkit";
import { createInstallEnv, resolveInstallCwd } from "../index.js";
import { createInstallEnv, resolveInstallCwd, resolveInstallInvocation } from "../index.js";

test("resolveInstallCwd resolves targetDir against the current base directory", () => {
assert.equal(
Expand Down Expand Up @@ -53,3 +53,46 @@ test("createInstallEnv strips parent npm lifecycle metadata but preserves useful
assert.equal(childEnv.npm_package_name, undefined);
assert.equal(childEnv.npm_package_json, undefined);
});

test("resolveInstallInvocation prefers npm_execpath and falls back to node-adjacent npm.cmd on Windows", () => {
assert.deepEqual(
resolveInstallInvocation("npm", {
env: {
npm_execpath: "C:\\Users\\k3nig\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js",
},
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
}),
{
command: "C:\\Program Files\\nodejs\\node.exe",
args: [
"C:\\Users\\k3nig\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js",
"install",
],
},
);

assert.deepEqual(
resolveInstallInvocation("npm", {
env: {},
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
}),
{
command: "C:\\Program Files\\nodejs\\npm.cmd",
args: ["install"],
},
);

assert.deepEqual(
resolveInstallInvocation("pnpm", {
env: {},
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
}),
{
command: "pnpm",
args: ["install"],
},
);
});
33 changes: 30 additions & 3 deletions packages/create-rezi/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { relative, resolve } from "node:path";
import { dirname, join, relative, resolve } from "node:path";
import { cwd, exit, stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import * as crossSpawn from "cross-spawn";
import { isMainModuleEntry } from "./mainEntry.js";
import {
TEMPLATE_DEFINITIONS,
Expand Down Expand Up @@ -165,6 +165,32 @@ export function resolveInstallCwd(targetDir: string, baseDir: string = cwd()): s
return resolve(baseDir, targetDir);
}

export function resolveInstallInvocation(
packageManager: PackageManager,
{
env = process.env,
platform = process.platform,
nodeExecPath = process.execPath,
}: {
env?: Readonly<Record<string, string | undefined>>;
platform?: NodeJS.Platform;
nodeExecPath?: string;
} = {},
): { command: string; args: string[] } {
if (packageManager === "npm") {
// biome-ignore lint/complexity/useLiteralKeys: process.env-compatible maps use index signatures in TS.
const npmExecPath = env["npm_execpath"];
if (npmExecPath) {
return { command: nodeExecPath, args: [npmExecPath, "install"] };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect --pm npm instead of always trusting npm_execpath

In resolveInstallInvocation, any truthy npm_execpath is executed when packageManager === "npm", but npm_execpath is often set by non-npm launchers (for example pnpm/yarn script contexts). In that case, create-rezi --pm npm can run the wrong installer (or fail) because it invokes node <pnpm-or-yarn-cli> install instead of npm, which breaks the explicit --pm contract and regresses prior behavior that always executed npm install.

Useful? React with 👍 / 👎.

}
if (platform === "win32") {
return { command: join(dirname(nodeExecPath), "npm.cmd"), args: ["install"] };
}
}

return { command: packageManager, args: ["install"] };
}

async function promptText(
rl: ReturnType<typeof createInterface>,
prompt: string,
Expand Down Expand Up @@ -200,7 +226,8 @@ async function promptTemplate(rl: ReturnType<typeof createInterface>): Promise<s

function runInstall(pm: PackageManager, targetDir: string): void {
const installCwd = resolveInstallCwd(targetDir);
const res = spawnSync(pm, ["install"], {
const installInvocation = resolveInstallInvocation(pm);
const res = crossSpawn.sync(installInvocation.command, installInvocation.args, {
cwd: installCwd,
stdio: "inherit",
env: createInstallEnv(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resolveMinimalCommand } from "../helpers/keybindings.js";

test("minimal keybinding map resolves expected commands", () => {
assert.equal(resolveMinimalCommand("q"), "quit");
assert.equal(resolveMinimalCommand("="), "increment");
assert.equal(resolveMinimalCommand("+"), "increment");
assert.equal(resolveMinimalCommand("-"), "decrement");
assert.equal(resolveMinimalCommand("t"), "cycle-theme");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const COMMAND_BY_KEY: Readonly<Record<string, MinimalCommand>> = Object.freeze({
"ctrl+c": "quit",
h: "toggle-help",
"shift+/": "toggle-help",
"=": "increment",
"+": "increment",
"shift+=": "increment",
"-": "decrement",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-rezi/templates/minimal/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ app.keys({
"ctrl+c": () => applyCommand(resolveMinimalCommand("ctrl+c")),
h: () => applyCommand(resolveMinimalCommand("h")),
"shift+/": () => applyCommand(resolveMinimalCommand("shift+/")),
"+": () => applyCommand(resolveMinimalCommand("+")),
"=": () => applyCommand(resolveMinimalCommand("=")),
"shift+=": () => applyCommand(resolveMinimalCommand("shift+=")),
"-": () => applyCommand(resolveMinimalCommand("-")),
t: () => applyCommand(resolveMinimalCommand("t")),
Expand Down
Loading