mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
fix(workspaces): resolve Windows runtime commands
This commit is contained in:
@@ -21,10 +21,10 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "node ../../scripts/clean-package-dist.mjs",
|
||||
"build": "tsc -p tsconfig.json --incremental false && node -e \"const fs=require('node:fs');fs.copyFileSync('src/executable.mjs','dist/executable.mjs');fs.chmodSync('dist/executable.mjs',0o755)\"",
|
||||
"build": "tsc -p tsconfig.json --incremental false && node -e \"const fs=require('node:fs');fs.copyFileSync('src/executable.mjs','dist/executable.mjs');fs.copyFileSync('src/command-resolution.mjs','dist/command-resolution.mjs');fs.chmodSync('dist/executable.mjs',0o755)\"",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
"prepack": "npm run build:clean",
|
||||
"test": "npm run build && vitest run test/workspace-helper.posix.test.ts --bail=1",
|
||||
"test": "npm run build && vitest run test/command-resolution.test.mjs test/workspace-helper.posix.test.ts --bail=1",
|
||||
"test:standalone": "node --test test/standalone.test.mjs",
|
||||
"typecheck": "tsgo -p tsconfig.json --noEmit"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export async function resolveCommand(
|
||||
workspaceRoot,
|
||||
name,
|
||||
{
|
||||
environment = process.env,
|
||||
executablePath = process.execPath,
|
||||
platform = process.platform,
|
||||
} = {},
|
||||
) {
|
||||
if (!name) throw new Error("--name is required");
|
||||
if (name.includes("/") || (platform === "win32" && name.includes("\\"))) {
|
||||
const candidate = path.isAbsolute(name) ? name : path.resolve(workspaceRoot, name);
|
||||
return resolveFirstExecutable(executableCandidates(candidate, platform, environment));
|
||||
}
|
||||
const searchPath = environment.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
||||
const delimiter = platform === "win32" ? ";" : path.delimiter;
|
||||
for (const directory of new Set([path.dirname(executablePath), ...searchPath.split(delimiter)])) {
|
||||
const resolved = await resolveFirstExecutable(
|
||||
executableCandidates(path.join(directory, name), platform, environment),
|
||||
);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function executableCandidates(candidate, platform, environment) {
|
||||
if (platform !== "win32") return [candidate];
|
||||
const extensions = (environment.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
||||
const candidateExtension = path.extname(candidate).toLowerCase();
|
||||
if (extensions.some((extension) => extension.toLowerCase() === candidateExtension)) {
|
||||
return [candidate];
|
||||
}
|
||||
return [candidate, ...extensions.map((extension) => `${candidate}${extension}`)];
|
||||
}
|
||||
|
||||
async function resolveFirstExecutable(candidates) {
|
||||
for (const candidate of candidates) {
|
||||
const resolved = await resolveExecutable(candidate);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveExecutable(candidate) {
|
||||
try {
|
||||
await access(candidate, constants.X_OK);
|
||||
return await realpath(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { constants, watch } from "node:fs";
|
||||
import { access, open, realpath, readdir, rename, stat, unlink } from "node:fs/promises";
|
||||
import { open, realpath, readdir, rename, stat, unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
|
||||
import { resolveCommand } from "./command-resolution.mjs";
|
||||
|
||||
const outsideMessage = "Access outside of workspace is not allowed";
|
||||
const imageTypes = new Map([
|
||||
[".png", "image/png"],
|
||||
@@ -62,33 +64,6 @@ function allowedArguments(command) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function resolveCommand(workspaceRoot, name) {
|
||||
if (!name) throw new Error("--name is required");
|
||||
if (name.includes("/") || (process.platform === "win32" && name.includes("\\"))) {
|
||||
const candidate = path.isAbsolute(name) ? name : path.resolve(workspaceRoot, name);
|
||||
try {
|
||||
await access(candidate, constants.X_OK);
|
||||
return await realpath(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const searchPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
||||
for (const directory of new Set([
|
||||
path.dirname(process.execPath),
|
||||
...searchPath.split(path.delimiter),
|
||||
])) {
|
||||
const candidate = path.join(directory, name);
|
||||
try {
|
||||
await access(candidate, constants.X_OK);
|
||||
return await realpath(candidate);
|
||||
} catch {
|
||||
// Continue through the runtime's own search path.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fileStat(workspaceRoot, relativePath) {
|
||||
try {
|
||||
const scoped = await resolveScoped(workspaceRoot, relativePath);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
|
||||
import { resolveCommand } from "../src/command-resolution.mjs";
|
||||
|
||||
const cleanupRoots = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
cleanupRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
test("Windows command resolution honors PATHEXT", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "paseo-helper-windows-command-"));
|
||||
cleanupRoots.push(root);
|
||||
const executable = path.join(root, "git.EXE");
|
||||
await writeFile(executable, "");
|
||||
await chmod(executable, 0o755);
|
||||
|
||||
await expect(
|
||||
resolveCommand(root, "git", {
|
||||
environment: { PATH: root, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
|
||||
executablePath: path.join(root, "node.EXE"),
|
||||
platform: "win32",
|
||||
}),
|
||||
).resolves.toBe(await realpath(executable));
|
||||
});
|
||||
Reference in New Issue
Block a user