mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
fix(workspaces): keep helper executable standalone
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"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.copyFileSync('src/command-resolution.mjs','dist/command-resolution.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.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/command-resolution.test.mjs test/workspace-helper.posix.test.ts --bail=1",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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,12 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { constants, watch } from "node:fs";
|
||||
import { open, realpath, readdir, rename, stat, unlink } from "node:fs/promises";
|
||||
import { access, 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"],
|
||||
@@ -64,6 +62,49 @@ function allowedArguments(command) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function resolveCommand(workspaceRoot, name) {
|
||||
if (!name) throw new Error("--name is required");
|
||||
const candidates = commandCandidates(name);
|
||||
if (name.includes("/") || (process.platform === "win32" && name.includes("\\"))) {
|
||||
for (const candidateName of candidates) {
|
||||
const candidate = path.isAbsolute(candidateName)
|
||||
? candidateName
|
||||
: path.resolve(workspaceRoot, candidateName);
|
||||
const resolved = await resolveExecutable(candidate);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
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),
|
||||
])) {
|
||||
for (const candidateName of candidates) {
|
||||
const resolved = await resolveExecutable(path.join(directory, candidateName));
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function commandCandidates(name) {
|
||||
if (process.platform !== "win32") return [name];
|
||||
const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
||||
const nameExtension = path.extname(name).toLowerCase();
|
||||
if (extensions.some((extension) => extension.toLowerCase() === nameExtension)) return [name];
|
||||
return [name, ...extensions.map((extension) => `${name}${extension}`)];
|
||||
}
|
||||
|
||||
async function resolveExecutable(candidate) {
|
||||
try {
|
||||
await access(candidate, constants.X_OK);
|
||||
return await realpath(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fileStat(workspaceRoot, relativePath) {
|
||||
try {
|
||||
const scoped = await resolveScoped(workspaceRoot, relativePath);
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { resolveCommand } from "../src/command-resolution.mjs";
|
||||
const run = promisify(execFile);
|
||||
const workspaceHelper = fileURLToPath(new URL("../src/executable.mjs", import.meta.url));
|
||||
|
||||
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));
|
||||
});
|
||||
test.runIf(process.platform === "win32")(
|
||||
"Windows command resolution honors PATHEXT",
|
||||
async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "paseo-helper-windows-command-"));
|
||||
try {
|
||||
const executable = path.join(root, "paseo-pathext-fixture.CMD");
|
||||
await writeFile(executable, "@exit /b 0\r\n");
|
||||
const result = await run(
|
||||
process.execPath,
|
||||
[workspaceHelper, "resolve-command", "--name", "paseo-pathext-fixture"],
|
||||
{
|
||||
cwd: root,
|
||||
env: { ...process.env, PATH: root, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
|
||||
},
|
||||
);
|
||||
expect(JSON.parse(result.stdout)).toEqual({ path: await realpath(executable) });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user