Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 11 additions & 6 deletions src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}`);
}

Expand Down
20 changes: 16 additions & 4 deletions test/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});