From 1b82aeb6b0a365c27bb834c5af864c14fc8d4511 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 15:28:27 -0700 Subject: [PATCH] Adapt the structure to parse the initial pip commands --- .../pip/createPipPackageManager.js | 61 ++++++++++------- .../commandArgumentScanner.js | 27 ++++++++ .../pip/dependencyScanner/nullScanner.js | 10 +++ .../parsing/parsePackagesFromInstallArgs.js | 62 +++++++++++++++++ .../parsePackagesFromInstallArgs.spec.js | 40 +++++++++++ .../src/packagemanager/pip/runPipCommand.js | 66 +++++++++++++++++++ .../packagemanager/pip/utils/pipCommands.js | 29 ++++++++ 7 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js create mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js create mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js create mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js create mode 100644 packages/safe-chain/src/packagemanager/pip/runPipCommand.js create mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js index 93d0fcc..7861d16 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js @@ -1,6 +1,11 @@ -import { ui } from "../../environment/userInteraction.js"; -import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; +import { nullScanner } from "./dependencyScanner/nullScanner.js"; +import { runPip } from "./runPipCommand.js"; +import { + getPipCommandForArgs, + pipInstallCommand, + pipUninstallCommand, +} from "./utils/pipCommands.js"; /** * Creates a package manager interface for Python's pip package installer @@ -8,28 +13,40 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg * @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip" */ export function createPipPackageManager(command = "pip") { - return { - runCommand: (args) => runPipCommand(command, args), + function isSupportedCommand(args) { + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); + return scanner.shouldScan(args); + } - // For pip, set proxy server - isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + function getDependencyUpdatesForCommand(args) { + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); + return scanner.scan(args); + } + + return { + runCommand: (args) => runPip(command, args), + isSupportedCommand, + getDependencyUpdatesForCommand, }; } -async function runPipCommand(command, args) { - try { - const result = await safeSpawn(command, args, { - stdio: "inherit", - env: mergeSafeChainProxyEnvironmentVariables(process.env), - }); - return { status: result.status }; - } catch (error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } +const commandScannerMapping = { + [pipInstallCommand]: commandArgumentScanner(), + [pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning +}; + +function findDependencyScannerForCommand(scanners, args) { + const command = getPipCommandForArgs(args); + if (!command) { + return nullScanner(); } + + const scanner = scanners[command]; + return scanner ? scanner : nullScanner(); } diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js new file mode 100644 index 0000000..26429f9 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -0,0 +1,27 @@ +/** + * Scanner for pip command arguments to detect package installations + * + * @param {Object} options - Scanner options + * @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag + * @returns {Object} Scanner interface + */ +export function commandArgumentScanner(options = {}) { + const { ignoreDryRun = false } = options; + + function shouldScan(args) { + // For now, pip scanning is not yet implemented + // This would need to detect 'install' commands and package arguments + return false; + } + + function scan(args) { + // Future implementation would parse pip install arguments + // and return array of {name, version, type} objects + return []; + } + + return { + shouldScan, + scan, + }; +} diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js new file mode 100644 index 0000000..ec3ba12 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js @@ -0,0 +1,10 @@ +/** + * Null scanner that returns no dependencies + * Used when a command is not supported for scanning + */ +export function nullScanner() { + return { + shouldScan: () => false, + scan: () => [], + }; +} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js new file mode 100644 index 0000000..109f994 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -0,0 +1,62 @@ +/** + * Parses package specifications from pip install arguments + * + * Pip supports various package specification formats: + * - package_name + * - package_name==version + * - package_name>=version + * - package_name~=version + * - git+https://... + * - -r requirements.txt + * - . (local directory) + * + * @param {string[]} args - pip install command arguments + * @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications + */ +export function parsePackagesFromInstallArgs(args) { + const packages = []; + let skipNext = false; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (skipNext) { + skipNext = false; + continue; + } + + // Skip the command itself (install, uninstall, etc.) + if (i === 0 && !arg.startsWith("-")) { + continue; + } + + // Skip flags and their values + if (arg.startsWith("-")) { + // Some flags take a value, skip the next arg for those + if (arg === "-r" || arg === "--requirement" || + arg === "-c" || arg === "--constraint" || + arg === "-e" || arg === "--editable" || + arg === "-t" || arg === "--target" || + arg === "-i" || arg === "--index-url" || + arg === "--extra-index-url") { + skipNext = true; + } + continue; + } + + // TODO: Implement full parsing logic + // For now, this is a placeholder that would need to handle: + // - Version specifiers (==, >=, <=, ~=, !=, <, >) + // - VCS urls (git+, hg+, svn+, bzr+) + // - Local file paths + // - Requirements files (-r, --requirement) + // - Extras (package[extra1,extra2]) + + packages.push({ + name: arg, + type: "add", + }); + } + + return packages; +} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js new file mode 100644 index 0000000..1e3782c --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js @@ -0,0 +1,40 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { parsePackagesFromInstallArgs } from "./parsePackagesFromInstallArgs.js"; + +describe("parsePackagesFromInstallArgs", () => { + it("should parse simple package name", () => { + const result = parsePackagesFromInstallArgs(["install", "requests"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + ]); + }); + + it("should parse package with version specifier", () => { + const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]); + assert.deepEqual(result, [ + { name: "requests==2.28.0", type: "add" }, + ]); + }); + + it("should skip flags", () => { + const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + ]); + }); + + it("should parse multiple packages", () => { + const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + { name: "flask", type: "add" }, + { name: "django==4.0", type: "add" }, + ]); + }); + + it("should return empty array for no packages", () => { + const result = parsePackagesFromInstallArgs(["install", "--help"]); + assert.deepEqual(result, []); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js new file mode 100644 index 0000000..859326a --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -0,0 +1,66 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +/** + * Runs a pip command with the specified arguments + * + * @param {string} command - The pip command to use (e.g., "pip", "pip3") + * @param {string[]} args - Command arguments + * @returns {Promise<{status: number}>} Result object with status code + */ +export async function runPip(command, args) { + try { + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} + +/** + * Runs a pip command in dry-run mode and captures output + * Note: pip doesn't have a native --dry-run flag, so this may need adjustment + * + * @param {string} command - The pip command to use + * @param {string[]} args - Command arguments + * @returns {Promise<{status: number, output: string}>} Result with status and output + */ +export async function dryRunPipCommandAndOutput(command, args) { + try { + // Note: pip doesn't have a --dry-run flag like npm + // This would need to be implemented differently if dry-run functionality is needed + const result = await safeSpawn( + command, + args, + { + stdio: "pipe", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + } + ); + return { + status: result.status, + output: result.status === 0 ? result.stdout : result.stderr, + }; + } catch (error) { + if (error.status) { + const output = + error.stdout?.toString() ?? + error.stderr?.toString() ?? + error.message ?? + ""; + return { status: error.status, output }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js new file mode 100644 index 0000000..99ce14b --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -0,0 +1,29 @@ +/** + * Pip command constants + */ +export const pipInstallCommand = "install"; +export const pipUninstallCommand = "uninstall"; +export const pipListCommand = "list"; +export const pipShowCommand = "show"; +export const pipFreeze = "freeze"; + +/** + * Gets the pip command from the arguments array + * + * @param {string[]} args - Command line arguments + * @returns {string|null} The pip command or null if not found + */ +export function getPipCommandForArgs(args) { + if (!args || args.length === 0) { + return null; + } + + // The first non-flag argument is typically the command + for (const arg of args) { + if (!arg.startsWith("-")) { + return arg; + } + } + + return null; +}