mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Merge fix/npx-bin-collision: restore correct npx behavior; robust global PATH collision handling\n\n- Runtime: always run bundled binary; show npx path; never delegate to VS Code\n- Postinstall: detect any PATH collisions, remove our 'code' shim, keep 'coder'\n- Logs: clear guidance for both npx and global installs
This commit is contained in:
+31
-51
@@ -3,8 +3,6 @@
|
||||
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { realpathSync, accessSync, constants } from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
// __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -12,55 +10,9 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
const { platform, arch } = process;
|
||||
|
||||
// If invoked as "code" and another "code" exists in PATH that is not
|
||||
// this package's launcher, delegate to it (e.g., VS Code's CLI).
|
||||
// This avoids hijacking users who already have VS Code installed.
|
||||
const maybeDelegateToOtherCode = () => {
|
||||
try {
|
||||
const invoked = process.env._ || "";
|
||||
const invokedBase = path.basename(invoked);
|
||||
if (invokedBase !== "code") return false;
|
||||
|
||||
const ourScriptReal = realpathSync(process.argv[1]);
|
||||
|
||||
const which = spawnSync("bash", ["-lc", "which -a code 2>/dev/null"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
if (which.status !== 0 || !which.stdout) return false;
|
||||
|
||||
const candidates = which.stdout
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const c of candidates) {
|
||||
// Skip the path that was used to invoke us
|
||||
if (c === invoked) continue;
|
||||
try {
|
||||
const real = realpathSync(c);
|
||||
// Skip if it resolves to our own script (same file inside our package)
|
||||
if (real === ourScriptReal) continue;
|
||||
|
||||
// Ensure it's executable; if so, exec and mirror exit code
|
||||
accessSync(c, constants.X_OK);
|
||||
const run = spawnSync(c, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
});
|
||||
// If it executed (even with non-zero), we consider delegation done.
|
||||
const code = run.status == null ? 1 : run.status;
|
||||
process.exit(code);
|
||||
} catch {
|
||||
// Try next candidate
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to our own binary
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
maybeDelegateToOtherCode();
|
||||
// Important: Never delegate to another system's `code` binary (e.g., VS Code).
|
||||
// When users run via `npx @just-every/code`, we must always execute our
|
||||
// packaged native binary by absolute path to avoid PATH collisions.
|
||||
|
||||
const isWSL = () => {
|
||||
if (platform !== "linux") return false;
|
||||
@@ -130,6 +82,7 @@ if (!existsSync(binaryPath)) {
|
||||
|
||||
// Check if binary exists and try to fix permissions if needed
|
||||
import { existsSync, chmodSync, statSync, openSync, readSync, closeSync } from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
if (existsSync(binaryPath)) {
|
||||
try {
|
||||
// Ensure binary is executable on Unix-like systems
|
||||
@@ -200,6 +153,33 @@ if (!validation.ok) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If running under npx/npm, emit a concise notice about which binary path is used
|
||||
try {
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
const isNpx = ua.includes("npx");
|
||||
if (isNpx && process.stderr && process.stderr.isTTY) {
|
||||
// Best-effort discovery of another 'code' on PATH for user clarity
|
||||
let otherCode = "";
|
||||
try {
|
||||
const cmd = process.platform === "win32" ? "where code" : "command -v code || which code || true";
|
||||
const out = spawnSync(process.platform === "win32" ? "cmd" : "bash", [
|
||||
process.platform === "win32" ? "/c" : "-lc",
|
||||
cmd,
|
||||
], { encoding: "utf8" });
|
||||
const line = (out.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
||||
if (line && !line.includes("@just-every/code")) {
|
||||
otherCode = line;
|
||||
}
|
||||
} catch {}
|
||||
if (otherCode) {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
console.error(`Note: a different 'code' exists at ${otherCode}; not delegating.`);
|
||||
} else {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Use an asynchronous spawn instead of spawnSync so that Node is able to
|
||||
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
|
||||
// executing. This allows us to forward those signals to the child process
|
||||
|
||||
+32
-25
@@ -364,20 +364,32 @@ async function main() {
|
||||
|
||||
const ourShim = join(globalBin || '', isWindows ? 'code.cmd' : 'code');
|
||||
|
||||
// Resolve which 'code' is currently on PATH
|
||||
const resolveOnPath = (cmd) => {
|
||||
// Resolve all 'code' candidates on PATH (so we detect collisions even if
|
||||
// our npm global bin currently appears first).
|
||||
const resolveAllOnPath = () => {
|
||||
try {
|
||||
if (isWindows) {
|
||||
const out = execSync(`where ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().split(/\r?\n/)[0]?.trim();
|
||||
return out || '';
|
||||
} else {
|
||||
return execSync(`command -v ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
||||
const out = execSync('where code', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
} catch { return ''; }
|
||||
// Prefer 'which -a' if available; fall back to 'command -v'
|
||||
let out = '';
|
||||
try {
|
||||
out = execSync('bash -lc "which -a code 2>/dev/null"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
} catch {
|
||||
try {
|
||||
out = execSync('command -v code || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
} catch { out = ''; }
|
||||
}
|
||||
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const codeResolved = resolveOnPath('code');
|
||||
const collision = codeResolved && ourShim && codeResolved !== ourShim;
|
||||
const candidates = resolveAllOnPath();
|
||||
const others = candidates.filter(p => p && ourShim && p !== ourShim);
|
||||
const collision = others.length > 0;
|
||||
|
||||
const ensureWrapper = (name, args) => {
|
||||
if (!globalBin) return;
|
||||
@@ -402,26 +414,22 @@ async function main() {
|
||||
ensureWrapper('code-exec', 'exec');
|
||||
|
||||
if (collision) {
|
||||
console.log('⚠ Detected an existing `code` command on your PATH (likely VS Code).');
|
||||
console.log('⚠ Detected existing `code` on PATH:');
|
||||
for (const p of others) console.log(` - ${p}`);
|
||||
if (globalBin) {
|
||||
// Create a 'coder' shim that forwards to our installed 'code' in the same dir
|
||||
// Remove our global `code` shim to avoid shadowing editors like VS Code
|
||||
try {
|
||||
const coderShim = join(globalBin, isWindows ? 'coder.cmd' : 'coder');
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"%~dp0code" %*\r\n`;
|
||||
writeFileSync(coderShim, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/code" "$@"\n`;
|
||||
writeFileSync(coderShim, content);
|
||||
chmodSync(coderShim, 0o755);
|
||||
if (existsSync(ourShim)) {
|
||||
unlinkSync(ourShim);
|
||||
console.log(`✓ Skipped global 'code' shim (removed ${ourShim})`);
|
||||
}
|
||||
console.log(`✓ Created fallback command \`coder\` -> our \`code\``);
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create 'coder' fallback: ${e.message}`);
|
||||
console.log(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
|
||||
}
|
||||
|
||||
// Offer to create a 'vscode' alias that points to the existing system VS Code
|
||||
if (isTTY && codeResolved) {
|
||||
const primaryOther = others[0];
|
||||
if (isTTY && primaryOther) {
|
||||
const prompt = (msg) => {
|
||||
process.stdout.write(msg);
|
||||
try {
|
||||
@@ -436,10 +444,10 @@ async function main() {
|
||||
try {
|
||||
const vscodeShim = join(globalBin, isWindows ? 'vscode.cmd' : 'vscode');
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"${codeResolved}" %*\r\n`;
|
||||
const content = `@echo off\r\n"${primaryOther}" %*\r\n`;
|
||||
writeFileSync(vscodeShim, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "${codeResolved}" "$@"\n`;
|
||||
const content = `#!/bin/sh\nexec "${primaryOther}" "$@"\n`;
|
||||
writeFileSync(vscodeShim, content);
|
||||
chmodSync(vscodeShim, 0o755);
|
||||
}
|
||||
@@ -451,7 +459,6 @@ async function main() {
|
||||
console.log('Skipping creation of `vscode` alias.');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('→ Use `coder` to run this tool, and `vscode` (if created) for your editor.');
|
||||
} else {
|
||||
console.log('Note: could not determine npm global bin; skipping alias creation.');
|
||||
|
||||
+6
-6
@@ -5,14 +5,14 @@
|
||||
"scripts": {
|
||||
"format": "prettier --check *.json *.md .github/workflows/*.yml **/*.js",
|
||||
"format:fix": "prettier --write *.json *.md .github/workflows/*.yml **/*.js",
|
||||
"build": "cd codex-rs && cargo build --release --bin code --bin code-tui --bin code-exec",
|
||||
"build:dev": "cd codex-rs && cargo build --bin code --bin code-tui --bin code-exec",
|
||||
"build:quick": "cd codex-rs && cargo build --release --bin code",
|
||||
"test:local": "cd codex-cli && npm link && code --version",
|
||||
"fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty"
|
||||
"build": "cd codex-rs && cargo build --release --bin code --bin code-tui --bin code-exec",
|
||||
"build:dev": "cd codex-rs && cargo build --bin code --bin code-tui --bin code-exec",
|
||||
"build:quick": "cd codex-rs && cargo build --release --bin code",
|
||||
"test:local": "cd codex-cli && npm link && code --version",
|
||||
"fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
"prettier": "^3.6.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"braces": "^3.0.3",
|
||||
|
||||
Generated
+5
-5
@@ -14,16 +14,16 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
|
||||
packages:
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
prettier@3.5.3: {}
|
||||
prettier@3.6.2: {}
|
||||
|
||||
Reference in New Issue
Block a user