From c662cbcf62e48cf3c3f035443c6e88615a51b73f Mon Sep 17 00:00:00 2001 From: ganesh47 <22994026+ganesh47@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:18:58 +0530 Subject: [PATCH] fix: shell-quote inferred workspace validation paths --- src/validation.ts | 17 +++++++++++------ test/validation.test.ts | 20 ++++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/validation.ts b/src/validation.ts index 8ceda22..80f08b1 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -647,17 +647,22 @@ function buildPackageScriptCommand(packageManager: "npm" | "pnpm" | "yarn", scri return packageManager === "npm" ? `npm run ${scriptName}` : `${packageManager} ${scriptName}`; } +function shellQuote(value: string): string { + return `'${value.replace(/'/g, "'\"'\"'")}'`; +} + function buildWorkspacePackageScriptCommand(packageManager: "npm" | "pnpm" | "yarn", targetPath: string, scriptName: string): string { if (targetPath === ".") { return buildPackageScriptCommand(packageManager, scriptName); } + const quotedPath = shellQuote(targetPath); if (packageManager === "npm") { - return `npm --prefix ${targetPath} run ${scriptName}`; + return `npm --prefix ${quotedPath} run ${scriptName}`; } if (packageManager === "yarn") { - return `yarn --cwd ${targetPath} ${scriptName}`; + return `yarn --cwd ${quotedPath} ${scriptName}`; } - return `pnpm --dir ${targetPath} ${scriptName}`; + return `pnpm --dir ${quotedPath} ${scriptName}`; } function parsePyprojectRequiresPython(raw: string): string | null { @@ -703,15 +708,15 @@ async function inferWorkspaceTargetDetails(options: { addPrerequisite(`uv available on PATH for ${options.targetPath}`); } if (/\[tool\.ruff\]/i.test(pyproject) || /\bruff\b/i.test(pyproject)) { - addCommand(`cd ${options.targetPath} && uv run ruff check .`); + addCommand(`cd ${shellQuote(options.targetPath)} && uv run ruff check .`); } if (/\[tool\.pytest\.ini_options\]/i.test(pyproject) || /\bpytest\b/i.test(pyproject)) { - addCommand(`cd ${options.targetPath} && uv run pytest`); + addCommand(`cd ${shellQuote(options.targetPath)} && uv run pytest`); } } if (options.manifests.includes("pom.xml")) { - addCommand(`mvn --batch-mode -f ${options.targetPath}/pom.xml verify`); + addCommand(`mvn --batch-mode -f ${shellQuote(`${options.targetPath}/pom.xml`)} verify`); addPrerequisite(`Java required for ${options.targetPath}`); } diff --git a/test/validation.test.ts b/test/validation.test.ts index 4acdedb..e14ad79 100644 --- a/test/validation.test.ts +++ b/test/validation.test.ts @@ -320,12 +320,24 @@ describe("validation intelligence", () => { const commands = selectDefaultLocalCommands(profile, { status: "passed", requestedCommands: [], results: [] }); expect(commands).toContain("pnpm check"); - expect(commands).toContain("pnpm --dir packages/api lint"); - expect(commands).toContain("pnpm --dir packages/api test"); - expect(commands).toContain("cd packages/cli && uv run ruff check ."); - expect(commands).toContain("cd packages/cli && uv run pytest"); + expect(commands).toContain("pnpm --dir 'packages/api' lint"); + expect(commands).toContain("pnpm --dir 'packages/api' test"); + expect(commands).toContain("cd 'packages/cli' && uv run ruff check ."); + expect(commands).toContain("cd 'packages/cli' && uv run pytest"); expect(profile.prerequisites).toContain("Node 20.17.0 from .nvmrc"); expect(profile.prerequisites).toContain("Python >=3.12 for packages/cli"); expect(profile.prerequisites).toContain("uv available on PATH for packages/cli"); }); + + it("shell-quotes inferred nested workspace paths", async () => { + const nestedPath = path.join(repoDir, "packages", "evil;touch /tmp/cstack_pwned"); + await fs.mkdir(nestedPath, { recursive: true }); + await fs.writeFile(path.join(repoDir, "package.json"), JSON.stringify({ name: "fixture", private: true, packageManager: "pnpm@9.12.0" }), "utf8"); + await fs.writeFile(path.join(nestedPath, "package.json"), JSON.stringify({ name: "@fixture/evil", private: true, scripts: { test: "vitest run" } }), "utf8"); + + const profile = await profileValidationRepository(repoDir); + const commands = selectDefaultLocalCommands(profile, { status: "passed", requestedCommands: [], results: [] }); + + expect(commands).toContain("pnpm --dir 'packages/evil;touch /tmp/cstack_pwned' test"); + }); });