From f10749923a34a38d827a803e06612cad005f4a66 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 10:08:49 +0200 Subject: [PATCH 01/11] Implement pnpm and pnpx support --- bin/aikido-pnpm.js | 8 + bin/aikido-pnpx.js | 8 + package-lock.json | 2 + package.json | 5 +- src/packagemanager/_shared/matchesCommand.js | 13 + src/packagemanager/currentPackageManager.js | 8 + .../pnpm/createPackageManager.js | 46 +++ .../commandArgumentScanner.js | 28 ++ .../parsing/parsePackagesFromArguments.js | 88 +++++ .../parsePackagesFromArguments.spec.js | 138 +++++++ src/packagemanager/pnpm/runPnpmCommand.js | 24 ++ src/shell-integration/helpers.js | 2 + src/shell-integration/setup.spec.js | 350 +++++++++++++----- src/shell-integration/teardown.spec.js | 179 ++++++--- 14 files changed, 754 insertions(+), 145 deletions(-) create mode 100755 bin/aikido-pnpm.js create mode 100755 bin/aikido-pnpx.js create mode 100644 src/packagemanager/_shared/matchesCommand.js create mode 100644 src/packagemanager/pnpm/createPackageManager.js create mode 100644 src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js create mode 100644 src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js create mode 100644 src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js create mode 100644 src/packagemanager/pnpm/runPnpmCommand.js diff --git a/bin/aikido-pnpm.js b/bin/aikido-pnpm.js new file mode 100755 index 0000000..e7bac47 --- /dev/null +++ b/bin/aikido-pnpm.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "pnpm"; +initializePackageManager(packageManagerName, process.versions.node); +await main(process.argv.slice(2)); diff --git a/bin/aikido-pnpx.js b/bin/aikido-pnpx.js new file mode 100755 index 0000000..25884ce --- /dev/null +++ b/bin/aikido-pnpx.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "pnpx"; +initializePackageManager(packageManagerName, process.versions.node); +await main(process.argv.slice(2)); diff --git a/package-lock.json b/package-lock.json index 4335bd2..260ee8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "bin": { "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", + "aikido-pnpm": "bin/aikido-pnpm.js", + "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" }, diff --git a/package.json b/package.json index 8a9241f..7bd518b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", "aikido-yarn": "bin/aikido-yarn.js", + "aikido-pnpm": "bin/aikido-pnpm.js", + "aikido-pnpx": "bin/aikido-pnpx.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", @@ -40,5 +42,6 @@ "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" }, - "homepage": "https://github.com/AikidoSec/safe-chain#readme" + "homepage": "https://github.com/AikidoSec/safe-chain#readme", + "packageManager": "npm@11.4.1+sha512.fcee43884166b6f9c5d04535fb95650e9708b6948a1f797eddf40e9778646778a518dfa32651b1c62ff36f4ac42becf177ca46ca27d53f24b539190c8d91802b" } diff --git a/src/packagemanager/_shared/matchesCommand.js b/src/packagemanager/_shared/matchesCommand.js new file mode 100644 index 0000000..d72caca --- /dev/null +++ b/src/packagemanager/_shared/matchesCommand.js @@ -0,0 +1,13 @@ +export function matchesCommand(args, ...commandArgs) { + if (args.length < commandArgs.length) { + return false; + } + + for (var i = 0; i < commandArgs.length; i++) { + if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) { + return false; + } + } + + return true; +} diff --git a/src/packagemanager/currentPackageManager.js b/src/packagemanager/currentPackageManager.js index 62e365a..9497a20 100644 --- a/src/packagemanager/currentPackageManager.js +++ b/src/packagemanager/currentPackageManager.js @@ -1,5 +1,9 @@ import { createNpmPackageManager } from "./npm/createPackageManager.js"; import { createNpxPackageManager } from "./npx/createPackageManager.js"; +import { + createPnpmPackageManager, + createPnpxPackageManager, +} from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; const state = { @@ -13,6 +17,10 @@ export function initializePackageManager(packageManagerName, version) { state.packageManagerName = createNpxPackageManager(); } else if (packageManagerName === "yarn") { state.packageManagerName = createYarnPackageManager(); + } else if (packageManagerName === "pnpm") { + state.packageManagerName = createPnpmPackageManager(); + } else if (packageManagerName === "pnpx") { + state.packageManagerName = createPnpxPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/src/packagemanager/pnpm/createPackageManager.js b/src/packagemanager/pnpm/createPackageManager.js new file mode 100644 index 0000000..763d920 --- /dev/null +++ b/src/packagemanager/pnpm/createPackageManager.js @@ -0,0 +1,46 @@ +import { matchesCommand } from "../_shared/matchesCommand.js"; +import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; +import { runPnpmCommand } from "./runPnpmCommand.js"; + +const scanner = commandArgumentScanner(); + +export function createPnpmPackageManager() { + return { + getWarningMessage: () => null, + runCommand: (args) => runPnpmCommand(args, "pnpm"), + isSupportedCommand: (args) => + matchesCommand(args, "add") || + matchesCommand(args, "update") || + matchesCommand(args, "upgrade") || + matchesCommand(args, "up") || + // dlx does not always come in the first position + // eg: pnpm --package=yo --package=generator-webapp dlx yo webapp + // documentation: https://pnpm.io/cli/dlx#--package-name + args.includes("dlx"), + getDependencyUpdatesForCommand: (args) => + getDependencyUpdatesForCommand(args, false), + }; +} + +export function createPnpxPackageManager() { + return { + getWarningMessage: () => null, + runCommand: (args) => runPnpmCommand(args, "pnpx"), + isSupportedCommand: () => true, + getDependencyUpdatesForCommand: (args) => + getDependencyUpdatesForCommand(args, true), + }; +} + +function getDependencyUpdatesForCommand(args, isPnpx) { + if (isPnpx) { + return scanner.scan(args); + } + if (args.includes("dlx")) { + // dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`) + // so we need to filter it out instead of slicing the array + // documentation: https://pnpm.io/cli/dlx#--package-name + return scanner.scan(args.filter((arg) => arg !== "dlx")); + } + return scanner.scan(args.slice(1)); +} diff --git a/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js b/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js new file mode 100644 index 0000000..c184b38 --- /dev/null +++ b/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js @@ -0,0 +1,28 @@ +import { resolvePackageVersion } from "../../../api/npmApi.js"; +import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; + +export function commandArgumentScanner() { + return { + scan: (args) => scanDependencies(args), + shouldScan: () => true, // There's no dry run for pnpm, so we always scan + }; +} + +async function scanDependencies(args) { + const changes = []; + const packageUpdates = parsePackagesFromArguments(args); + + for (const packageUpdate of packageUpdates) { + var exactVersion = await resolvePackageVersion( + packageUpdate.name, + packageUpdate.version + ); + if (exactVersion) { + packageUpdate.version = exactVersion; + } + + changes.push({ ...packageUpdate, type: "add" }); + } + + return changes; +} diff --git a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js new file mode 100644 index 0000000..d0383c2 --- /dev/null +++ b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js @@ -0,0 +1,88 @@ +export function parsePackagesFromArguments(args) { + const changes = []; + let defaultTag = "latest"; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + const option = getOption(arg); + + if (option) { + // If the option has a parameter, skip the next argument as well + i += option.numberOfParameters; + + continue; + } + + const packageDetails = parsePackagename(arg, defaultTag); + if (packageDetails) { + changes.push(packageDetails); + } + } + + return changes; +} + +function getOption(arg) { + if (isOptionWithParameter(arg)) { + return { + name: arg, + numberOfParameters: 1, + }; + } + + // Arguments starting with "-" or "--" are considered options + // except for "--package=" which contains the package name + if (arg.startsWith("-") && !arg.startsWith("--package=")) { + return { + name: arg, + numberOfParameters: 0, + }; + } + + return undefined; +} + +function isOptionWithParameter(arg) { + const optionsWithParameters = ["--C", "--dir"]; + + return optionsWithParameters.includes(arg); +} + +function parsePackagename(arg, defaultTag) { + // format can be --package=name@version + // in that case, we need to remove the --package= part + if (arg.startsWith("--package=")) { + arg = arg.slice(10); + } + + arg = removeAlias(arg); + + // Split at the last "@" to separate the package name and version + const lastAtIndex = arg.lastIndexOf("@"); + + let name, version; + // The index of the last "@" should be greater than 0 + // If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name") + if (lastAtIndex > 0) { + name = arg.slice(0, lastAtIndex); + version = arg.slice(lastAtIndex + 1); + } else { + name = arg; + version = defaultTag; // No tag specified (eg: "http-server"), use the default tag + } + + return { + name, + version, + }; +} + +function removeAlias(arg) { + // removes the alias. + // Eg.: server@npm:http-server@latest becomes http-server@latest + const aliasIndex = arg.indexOf("@npm:"); + if (aliasIndex !== -1) { + return arg.slice(aliasIndex + 5); + } + return arg; +} diff --git a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js new file mode 100644 index 0000000..be6812b --- /dev/null +++ b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js @@ -0,0 +1,138 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js"; + +describe("standardYarnArgumentParser", () => { + it("should return an empty array for no changes", () => { + const args = []; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, []); + }); + + it("should return an array of changes for one package", () => { + const args = ["axios@1.9.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]); + }); + + it("should return the package with latest tag if absent", () => { + const args = ["axios"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "latest" }]); + }); + + it("should return the package with latest tag if the version is absent and package starts with @", () => { + const args = ["@aikidosec/package-name"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "@aikidosec/package-name", version: "latest" }, + ]); + }); + + it("should return the package with the specified tag if the package starts with @ and includes the version", () => { + const args = ["@aikidosec/package-name@1.0.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "@aikidosec/package-name", version: "1.0.0" }, + ]); + }); + + it("should only return all packages", () => { + const args = ["axios", "jest"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "axios", version: "latest" }, + { name: "jest", version: "latest" }, + ]); + }); + + it("should ignore options with parameters and return an array of changes", () => { + const args = ["--C", "/Users/johnsmith/dev/project", "axios@1.9.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]); + }); + + it("should parse version even for aliased packages", () => { + const args = ["server@npm:axios@1.9.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]); + }); + + it("should parse scoped packages", () => { + const args = ["@scope/package@1.0.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]); + }); + + it("should parse packages with version ranges", () => { + const args = ["axios@^1.9.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]); + }); + + it("should parse package folders", () => { + const args = ["./local-package"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]); + }); + + it("should parse tarballs", () => { + const args = ["file:./local-package.tgz"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "file:./local-package.tgz", version: "latest" }, + ]); + }); + + it("should parse tarball URLs", () => { + const args = ["https://example.com/local-package.tgz"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "https://example.com/local-package.tgz", version: "latest" }, + ]); + }); + + it("should parse git URLs", () => { + const args = ["git://github.com/http-party/http-server"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [ + { name: "git://github.com/http-party/http-server", version: "latest" }, + ]); + }); + + it("should parse packages with --package={packageName}", () => { + const args = ["--package=axios@1.9.0"]; + + const result = parsePackagesFromArguments(args); + + assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]); + }); +}); diff --git a/src/packagemanager/pnpm/runPnpmCommand.js b/src/packagemanager/pnpm/runPnpmCommand.js new file mode 100644 index 0000000..37e7f3b --- /dev/null +++ b/src/packagemanager/pnpm/runPnpmCommand.js @@ -0,0 +1,24 @@ +import { spawnSync } from "child_process"; +import { ui } from "../../environment/userInteraction.js"; + +export function runPnpmCommand(args, toolName = "pnpm") { + try { + let result; + + if (toolName === "pnpm") { + result = spawnSync("pnpm", args, { stdio: "inherit" }); + } else if (toolName === "pnpx") { + result = spawnSync("pnpx", args, { stdio: "inherit" }); + } else { + throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`); + } + + if (result.status !== null) { + return { status: result.status }; + } + } catch (error) { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + return { status: 0 }; +} diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index c9b4c5e..76714ef 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -2,6 +2,8 @@ const knownAikidoTools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, { tool: "yarn", aikidoCommand: "aikido-yarn" }, + { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, + { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, // When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js) // and add the documentation for the new tool in the README.md ]; diff --git a/src/shell-integration/setup.spec.js b/src/shell-integration/setup.spec.js index cc5ae01..3c11136 100644 --- a/src/shell-integration/setup.spec.js +++ b/src/shell-integration/setup.spec.js @@ -6,45 +6,79 @@ import { getAliases } from "./helpers.js"; import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js"; describe("setupShell", () => { - function runSetupTestsForEnvironment(shell, startupExtension, expectedAliases) { + function runSetupTestsForEnvironment( + shell, + startupExtension, + expectedAliases + ) { describe(`${shell} shell setup`, () => { it(`should add aliases to ${shell} file`, () => { const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = appendAliasesToFile(aliases, fileContent, filePath); - - assert.strictEqual(result.addedCount, 3, "Should add 3 aliases"); - assert.strictEqual(result.existingCount, 0, "Should find no existing aliases"); - assert.strictEqual(result.failedCount, 0, "Should have no failed aliases"); - + + assert.strictEqual( + result.addedCount, + expectedAliases.length, + `Should add ${expectedAliases.length} aliases` + ); + assert.strictEqual( + result.existingCount, + 0, + "Should find no existing aliases" + ); + assert.strictEqual( + result.failedCount, + 0, + "Should have no failed aliases" + ); + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`); + assert.ok( + updatedContent.includes(alias), + `Alias "${alias}" should be added` + ); } - assert.ok(updatedContent.includes("alias cls='clear'"), "Original aliases should remain"); + assert.ok( + updatedContent.includes("alias cls='clear'"), + "Original aliases should remain" + ); }); it(`should not add aliases if they already exist in ${shell} file`, () => { const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = appendAliasesToFile(aliases, fileContent, filePath); - + assert.strictEqual(result.addedCount, 0, "Should add 0 aliases"); - assert.strictEqual(result.existingCount, 3, "Should find 3 existing aliases"); - assert.strictEqual(result.failedCount, 0, "Should have no failed aliases"); - + assert.strictEqual( + result.existingCount, + expectedAliases.length, + `Should find ${expectedAliases.length} existing aliases` + ); + assert.strictEqual( + result.failedCount, + 0, + "Should have no failed aliases" + ); + const updatedContent = readAndDeleteFile(filePath); // Count occurrences to ensure no duplicates were added for (const alias of expectedAliases) { - assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`); + assert.strictEqual( + countOccurrences(updatedContent, alias), + 1, + `Alias "${alias}" should appear exactly once` + ); } }); @@ -54,78 +88,144 @@ describe("setupShell", () => { if (fs.existsSync(filePath)) { fs.rmSync(filePath, { force: true }); } - + // Test readOrCreateStartupFile function const fileContent = readOrCreateStartupFile(filePath); - assert.strictEqual(fileContent, "", "Should return empty string for new file"); + assert.strictEqual( + fileContent, + "", + "Should return empty string for new file" + ); assert.ok(fs.existsSync(filePath), "File should be created"); - + // Test adding aliases to the newly created file const aliases = getAliases(filePath); const result = appendAliasesToFile(aliases, fileContent, filePath); - - assert.strictEqual(result.addedCount, 3, "Should add 3 aliases"); - assert.strictEqual(result.existingCount, 0, "Should find no existing aliases"); - assert.strictEqual(result.failedCount, 0, "Should have no failed aliases"); - + + assert.strictEqual( + result.addedCount, + expectedAliases.length, + `Should add ${expectedAliases.length} aliases` + ); + assert.strictEqual( + result.existingCount, + 0, + "Should find no existing aliases" + ); + assert.strictEqual( + result.failedCount, + 0, + "Should have no failed aliases" + ); + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`); + assert.ok( + updatedContent.includes(alias), + `Alias "${alias}" should be added` + ); } }); it(`should add aliases only once when called multiple times for ${shell}`, () => { const lines = [`#!/usr/bin/env ${shell}`, ""]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); - + // First call - should add aliases let fileContent = fs.readFileSync(filePath, "utf-8"); const result1 = appendAliasesToFile(aliases, fileContent, filePath); - assert.strictEqual(result1.addedCount, 3, "First call should add 3 aliases"); - + assert.strictEqual( + result1.addedCount, + expectedAliases.length, + `First call should add ${expectedAliases.length} aliases` + ); + // Second call - should detect existing aliases fileContent = fs.readFileSync(filePath, "utf-8"); const result2 = appendAliasesToFile(aliases, fileContent, filePath); - assert.strictEqual(result2.addedCount, 0, "Second call should add 0 aliases"); - assert.strictEqual(result2.existingCount, 3, "Second call should find 3 existing aliases"); - + assert.strictEqual( + result2.addedCount, + 0, + "Second call should add 0 aliases" + ); + assert.strictEqual( + result2.existingCount, + expectedAliases.length, + `Second call should find ${expectedAliases.length} existing aliases` + ); + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`); + assert.strictEqual( + countOccurrences(updatedContent, alias), + 1, + `Alias "${alias}" should appear exactly once` + ); } }); it(`should use real getAliases() for ${shell} file`, () => { const filePath = `${tmpdir()}/test${startupExtension}`; const aliases = getAliases(filePath); - + // Verify we get the expected aliases for this shell type - assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)"); + assert.strictEqual( + aliases.length, + expectedAliases.length, + "Should get all aliases (npm, npx, yarn)" + ); for (let i = 0; i < aliases.length; i++) { - assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`); + assert.strictEqual( + aliases[i], + expectedAliases[i], + `Alias ${i} should match expected format` + ); } }); it(`should handle mixed scenario - some existing, some new for ${shell}`, () => { - const lines = [`#!/usr/bin/env ${shell}`, "", expectedAliases[0], "alias other='command'"]; + const lines = [ + `#!/usr/bin/env ${shell}`, + "", + expectedAliases[0], + "alias other='command'", + ]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = appendAliasesToFile(aliases, fileContent, filePath); - - assert.strictEqual(result.addedCount, 2, "Should add 2 new aliases"); - assert.strictEqual(result.existingCount, 1, "Should find 1 existing alias"); - assert.strictEqual(result.failedCount, 0, "Should have no failed aliases"); - + + assert.strictEqual( + result.addedCount, + expectedAliases.length - 1, + `Should add ${expectedAliases.length - 1} aliases` + ); + assert.strictEqual( + result.existingCount, + 1, + "Should find 1 existing alias" + ); + assert.strictEqual( + result.failedCount, + 0, + "Should have no failed aliases" + ); + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be present`); + assert.ok( + updatedContent.includes(alias), + `Alias "${alias}" should be present` + ); } - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain"); + assert.ok( + updatedContent.includes("alias other='command'"), + "Other aliases should remain" + ); }); }); } @@ -134,65 +234,91 @@ describe("setupShell", () => { runSetupTestsForEnvironment("bash", ".bashrc", [ "alias npm='aikido-npm'", "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" + "alias yarn='aikido-yarn'", + "alias pnpm='aikido-pnpm'", + "alias pnpx='aikido-pnpx'", ]); runSetupTestsForEnvironment("zsh", ".zshrc", [ "alias npm='aikido-npm'", "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" + "alias yarn='aikido-yarn'", + "alias pnpm='aikido-pnpm'", + "alias pnpx='aikido-pnpx'", ]); runSetupTestsForEnvironment("fish", ".fish", [ 'alias npm "aikido-npm"', 'alias npx "aikido-npx"', - 'alias yarn "aikido-yarn"' + 'alias yarn "aikido-yarn"', + 'alias pnpm "aikido-pnpm"', + 'alias pnpx "aikido-pnpx"', ]); runSetupTestsForEnvironment("pwsh", ".ps1", [ "Set-Alias npm aikido-npm", "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn" + "Set-Alias yarn aikido-yarn", + "Set-Alias pnpm aikido-pnpm", + "Set-Alias pnpx aikido-pnpx", ]); describe("readOrCreateStartupFile", () => { it("should read existing file content", () => { const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"]; const filePath = createShellStartupScript(lines, ".bashrc"); - + const content = readOrCreateStartupFile(filePath); - - assert.ok(content.includes("#!/usr/bin/env bash"), "Should contain shebang"); - assert.ok(content.includes("alias test='echo test'"), "Should contain existing aliases"); - + + assert.ok( + content.includes("#!/usr/bin/env bash"), + "Should contain shebang" + ); + assert.ok( + content.includes("alias test='echo test'"), + "Should contain existing aliases" + ); + // Cleanup fs.rmSync(filePath, { force: true }); }); it("should create file if it doesn't exist", () => { - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; if (fs.existsSync(filePath)) { fs.rmSync(filePath, { force: true }); } - + const content = readOrCreateStartupFile(filePath); - - assert.strictEqual(content, "", "Should return empty string for new file"); + + assert.strictEqual( + content, + "", + "Should return empty string for new file" + ); assert.ok(fs.existsSync(filePath), "File should be created"); - + // Cleanup fs.rmSync(filePath, { force: true }); }); it("should handle empty existing file", () => { - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; fs.writeFileSync(filePath, "", "utf-8"); - + const content = readOrCreateStartupFile(filePath); - - assert.strictEqual(content, "", "Should return empty string for empty file"); + + assert.strictEqual( + content, + "", + "Should return empty string for empty file" + ); assert.ok(fs.existsSync(filePath), "File should still exist"); - + // Cleanup fs.rmSync(filePath, { force: true }); }); @@ -203,15 +329,22 @@ describe("setupShell", () => { const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"]; const filePath = createShellStartupScript(lines, ".bashrc"); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = appendAliasesToFile([], fileContent, filePath); - + assert.strictEqual(result.addedCount, 0, "Should add 0 aliases"); - assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases"); + assert.strictEqual( + result.existingCount, + 0, + "Should find 0 existing aliases" + ); assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases"); - + const updatedContent = readAndDeleteFile(filePath); - assert.ok(updatedContent.includes("alias test='echo test'"), "Original content should remain"); + assert.ok( + updatedContent.includes("alias test='echo test'"), + "Original content should remain" + ); }); it("should handle partial substring matches correctly", () => { @@ -219,38 +352,57 @@ describe("setupShell", () => { "#!/usr/bin/env bash", "", "alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm=' - "alias test='echo test'" + "alias test='echo test'", ]; const filePath = createShellStartupScript(lines, ".bashrc"); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const aliases = ["alias npm='aikido-npm'"]; const result = appendAliasesToFile(aliases, fileContent, filePath); - + assert.strictEqual(result.addedCount, 1, "Should add 1 alias (npm)"); - assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases"); + assert.strictEqual( + result.existingCount, + 0, + "Should find 0 existing aliases" + ); assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases"); - + const updatedContent = readAndDeleteFile(filePath); - assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "npm alias should be added"); - assert.ok(updatedContent.includes("alias npmx='some-other-command'"), "npmx alias should remain"); + assert.ok( + updatedContent.includes("alias npm='aikido-npm'"), + "npm alias should be added" + ); + assert.ok( + updatedContent.includes("alias npmx='some-other-command'"), + "npmx alias should remain" + ); }); it("should handle file with only whitespace", () => { - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; const fileContent = `${EOL}${EOL} ${EOL}`; fs.writeFileSync(filePath, fileContent, "utf-8"); - + const aliases = ["alias npm='aikido-npm'"]; const result = appendAliasesToFile(aliases, fileContent, filePath); - + assert.strictEqual(result.addedCount, 1, "Should add 1 alias"); - assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases"); + assert.strictEqual( + result.existingCount, + 0, + "Should find 0 existing aliases" + ); assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases"); - + const updatedContent = fs.readFileSync(filePath, "utf-8"); - assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "Alias should be added"); - + assert.ok( + updatedContent.includes("alias npm='aikido-npm'"), + "Alias should be added" + ); + // Cleanup fs.rmSync(filePath, { force: true }); }); @@ -258,21 +410,31 @@ describe("setupShell", () => { describe("appendAliasesToFile error handling", () => { it("should handle file permission errors gracefully", () => { - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; fs.writeFileSync(filePath, "#!/usr/bin/env bash", "utf-8"); - + // Make file read-only to simulate permission error fs.chmodSync(filePath, 0o444); - + const aliases = ["alias npm='aikido-npm'"]; const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = appendAliasesToFile(aliases, fileContent, filePath); - - assert.strictEqual(result.addedCount, 0, "Should add 0 aliases due to permission error"); - assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases"); + + assert.strictEqual( + result.addedCount, + 0, + "Should add 0 aliases due to permission error" + ); + assert.strictEqual( + result.existingCount, + 0, + "Should find 0 existing aliases" + ); assert.strictEqual(result.failedCount, 1, "Should have 1 failed alias"); - + // Restore permissions and cleanup fs.chmodSync(filePath, 0o644); fs.rmSync(filePath, { force: true }); @@ -301,4 +463,4 @@ function countOccurrences(lines, searchString) { } } return count; -} \ No newline at end of file +} diff --git a/src/shell-integration/teardown.spec.js b/src/shell-integration/teardown.spec.js index 3f301c2..5077a99 100644 --- a/src/shell-integration/teardown.spec.js +++ b/src/shell-integration/teardown.spec.js @@ -6,41 +6,64 @@ import { getAliases } from "./helpers.js"; import { removeAliasesFromFile } from "./teardown.js"; describe("teardown", () => { - function runRemovalTestsForEnvironment(shell, startupExtension, expectedAliases) { + function runRemovalTestsForEnvironment( + shell, + startupExtension, + expectedAliases + ) { describe(`${shell} shell removal`, () => { it(`should remove aliases from ${shell} file`, () => { const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""]; const filePath = createShellStartupScript(lines, startupExtension); - + // Test the removeAliasesFromFile function directly const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - - assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases"); + + assert.strictEqual( + result.removedCount, + expectedAliases.length, + "Should remove all aliases" + ); assert.strictEqual(result.notFoundCount, 0, "Should find all aliases"); - + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.ok(!updatedContent.includes(alias), `Alias "${alias}" should be removed`); + assert.ok( + !updatedContent.includes(alias), + `Alias "${alias}" should be removed` + ); } }); it(`should handle file with no aliases for ${shell}`, () => { - const lines = [`#!/usr/bin/env ${shell}`, "", "alias other='command'", ""]; + const lines = [ + `#!/usr/bin/env ${shell}`, + "", + "alias other='command'", + "", + ]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - + assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases"); - assert.strictEqual(result.notFoundCount, 3, "Should report 3 aliases not found"); - + assert.strictEqual( + result.notFoundCount, + expectedAliases.length, + "Should report all aliases not found" + ); + const updatedContent = readAndDeleteFile(filePath); - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain unchanged"); + assert.ok( + updatedContent.includes("alias other='command'"), + "Other aliases should remain unchanged" + ); }); it(`should remove duplicate aliases from ${shell} file`, () => { @@ -50,33 +73,51 @@ describe("teardown", () => { ...expectedAliases, "alias other='command'", ...expectedAliases, // duplicates - "" + "", ]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - - assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases (counting duplicates as single removal)"); + + assert.strictEqual( + result.removedCount, + expectedAliases.length, + "Should remove all aliases (counting duplicates as single removal)" + ); assert.strictEqual(result.notFoundCount, 0, "Should find all aliases"); - + const updatedContent = readAndDeleteFile(filePath); for (const alias of expectedAliases) { - assert.ok(!updatedContent.includes(alias), `Alias "${alias}" should be completely removed`); + assert.ok( + !updatedContent.includes(alias), + `Alias "${alias}" should be completely removed` + ); } - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain"); + assert.ok( + updatedContent.includes("alias other='command'"), + "Other aliases should remain" + ); }); it(`should use real getAliases() for ${shell} file`, () => { const filePath = `${tmpdir()}/test${startupExtension}`; const aliases = getAliases(filePath); - + // Verify we get the expected aliases for this shell type - assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)"); + assert.strictEqual( + aliases.length, + expectedAliases.length, + "Should get all aliases (npm, npx, yarn)" + ); for (let i = 0; i < aliases.length; i++) { - assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`); + assert.strictEqual( + aliases[i], + expectedAliases[i], + `Alias ${i} should match expected format` + ); } }); @@ -86,21 +127,31 @@ describe("teardown", () => { "", expectedAliases[0], // Only first alias "alias other='command'", - "" + "", ]; const filePath = createShellStartupScript(lines, startupExtension); - + const aliases = getAliases(filePath); const fileContent = fs.readFileSync(filePath, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - + assert.strictEqual(result.removedCount, 1, "Should remove 1 alias"); - assert.strictEqual(result.notFoundCount, 2, "Should report 2 aliases not found"); - + assert.strictEqual( + result.notFoundCount, + expectedAliases.length - 1, + "Should report all aliases not found" + ); + const updatedContent = readAndDeleteFile(filePath); - assert.ok(!updatedContent.includes(expectedAliases[0]), "First alias should be removed"); - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain"); + assert.ok( + !updatedContent.includes(expectedAliases[0]), + "First alias should be removed" + ); + assert.ok( + updatedContent.includes("alias other='command'"), + "Other aliases should remain" + ); }); }); } @@ -109,39 +160,57 @@ describe("teardown", () => { runRemovalTestsForEnvironment("bash", ".bashrc", [ "alias npm='aikido-npm'", "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" + "alias yarn='aikido-yarn'", + "alias pnpm='aikido-pnpm'", + "alias pnpx='aikido-pnpx'", ]); runRemovalTestsForEnvironment("zsh", ".zshrc", [ "alias npm='aikido-npm'", "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" + "alias yarn='aikido-yarn'", + "alias pnpm='aikido-pnpm'", + "alias pnpx='aikido-pnpx'", ]); runRemovalTestsForEnvironment("fish", ".fish", [ 'alias npm "aikido-npm"', 'alias npx "aikido-npx"', - 'alias yarn "aikido-yarn"' + 'alias yarn "aikido-yarn"', + 'alias pnpm "aikido-pnpm"', + 'alias pnpx "aikido-pnpx"', ]); runRemovalTestsForEnvironment("pwsh", ".ps1", [ "Set-Alias npm aikido-npm", "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn" + "Set-Alias yarn aikido-yarn", + "Set-Alias pnpm aikido-pnpm", + "Set-Alias pnpx aikido-pnpx", ]); describe("removeAliasesFromFile edge cases", () => { it("should handle empty file", () => { const aliases = ["alias npm='aikido-npm'"]; const fileContent = ""; - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; fs.writeFileSync(filePath, fileContent, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - - assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from empty file"); - assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found"); - + + assert.strictEqual( + result.removedCount, + 0, + "Should remove 0 aliases from empty file" + ); + assert.strictEqual( + result.notFoundCount, + 1, + "Should report 1 alias not found" + ); + // Cleanup fs.rmSync(filePath, { force: true }); }); @@ -149,14 +218,24 @@ describe("teardown", () => { it("should handle file with only whitespace", () => { const aliases = ["alias npm='aikido-npm'"]; const fileContent = `${EOL}${EOL} ${EOL}`; - const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`; + const filePath = `${tmpdir()}/test-${Math.random() + .toString(36) + .substring(2, 15)}.bashrc`; fs.writeFileSync(filePath, fileContent, "utf-8"); - + const result = removeAliasesFromFile(aliases, fileContent, filePath); - - assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from whitespace-only file"); - assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found"); - + + assert.strictEqual( + result.removedCount, + 0, + "Should remove 0 aliases from whitespace-only file" + ); + assert.strictEqual( + result.notFoundCount, + 1, + "Should report 1 alias not found" + ); + // Cleanup fs.rmSync(filePath, { force: true }); }); @@ -174,4 +253,4 @@ function readAndDeleteFile(filePath) { const fileContent = fs.readFileSync(filePath, "utf-8"); fs.rmSync(filePath, { force: true }); return fileContent.split(EOL); -} \ No newline at end of file +} From 9dbd860661b86ed8070ad0dcc97ed1051eb17a5f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 10:26:45 +0200 Subject: [PATCH 02/11] Update documentation to reflext pnpm/pnpx has been added --- README.md | 15 ++++++++------- docs/shell-integration.md | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 844b637..a7b30db 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Aikido Safe Chain -The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, or yarn. +The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. -The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), and [yarn](https://yarnpkg.com/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, or yarn from downloading or running the malware. +The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware. ![demo](https://aikido-production-staticfiles-public.s3.eu-west-1.amazonaws.com/safe-pkg.gif) @@ -11,7 +11,8 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - βœ… **npm** - βœ… **npx** - βœ… **yarn** -- 🚧 **pnpm** Coming soon +- βœ… **pnpm** +- βœ… **pnpx** # Usage @@ -28,20 +29,20 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` 3. **❗Restart your terminal** to start using the Aikido Safe Chain. - - This step is crucial as it ensures that the shell aliases for npm, npx, and yarn are loaded correctly. If you do not restart your terminal, the aliases will not be available. + - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm and pnpx are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running: ```shell npm install eslint-js ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, or `yarn` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm` or `pnpx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. ## How it works -The Aikido Safe Chain works by intercepting the npm, npx, and yarn commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. +The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm and pnpx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. -The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, and yarn commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support: +The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm and pnpx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support: - βœ… **Bash** - βœ… **Zsh** diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 715344c..3243c20 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -2,7 +2,7 @@ ## Overview -The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents. +The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents. ## Supported Shells @@ -27,7 +27,7 @@ safe-chain setup This command: - Detects all supported shells on your system -- Adds aliases for `npm`, `npx`, and `yarn` to each shell's startup file +- Adds aliases for `npm`, `npx`, `yarn`, `pnpm` and `pnpx` to each shell's startup file ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the aliases are loaded correctly. @@ -75,7 +75,7 @@ The system modifies the following files based on your shell configuration: This means the aliases are working but the Aikido commands aren't installed or available in your PATH: - Make sure Aikido Safe Chain is properly installed on your system -- Verify the `aikido-npm`, `aikido-npx`, and `aikido-yarn` commands exist +- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist - Check that these commands are in your system's PATH ### Manual Verification @@ -105,4 +105,4 @@ To verify the integration is working, follow these steps: 3. **If you need to remove aliases manually:** - Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, or `aikido-yarn`. + Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` or `aikido-pnpx`. From 62a82cf57eea0f03aaf45e447439db0803b810cc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 10:28:16 +0200 Subject: [PATCH 03/11] Remove explicit packagemanager --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 7bd518b..4ab4c95 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,5 @@ "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" }, - "homepage": "https://github.com/AikidoSec/safe-chain#readme", - "packageManager": "npm@11.4.1+sha512.fcee43884166b6f9c5d04535fb95650e9708b6948a1f797eddf40e9778646778a518dfa32651b1c62ff36f4ac42becf177ca46ca27d53f24b539190c8d91802b" + "homepage": "https://github.com/AikidoSec/safe-chain#readme" } From 2166bcc234bffbda63eeb3bb9ad5575371656722 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 11:08:43 +0200 Subject: [PATCH 04/11] Fix typo --- .../pnpm/parsing/parsePackagesFromArguments.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js index be6812b..cc9b792 100644 --- a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +++ b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js @@ -2,7 +2,7 @@ import { describe, it } from "node:test"; import assert from "node:assert"; import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js"; -describe("standardYarnArgumentParser", () => { +describe("standardPnpmArgumentParser", () => { it("should return an empty array for no changes", () => { const args = []; From 6b84d449a16b016864150abc091d775f31d68339 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 11:46:23 +0200 Subject: [PATCH 05/11] Add bun as "coming soon" --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a7b30db..33dcc26 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - βœ… **yarn** - βœ… **pnpm** - βœ… **pnpx** +- 🚧 **bun** Coming soon # Usage From 64bb04a61e206da03a5b2b2b65512ddfd130d059 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 12:35:02 +0200 Subject: [PATCH 06/11] Document correct tests to modify in source --- src/shell-integration/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index 76714ef..5b7c4d6 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -4,7 +4,7 @@ const knownAikidoTools = [ { tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, - // When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js) + // When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js) // and add the documentation for the new tool in the README.md ]; From 41bf3252d925d2e5d521bb8916dd2b661b60e852 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 11:15:21 +0200 Subject: [PATCH 07/11] Fix command injection --- src/shell-integration/helpers.js | 10 +--------- .../supported-shells/bash.js | 19 ++++++++++++++++--- .../supported-shells/bash.spec.js | 8 +++++++- .../supported-shells/fish.js | 19 ++++++++++++++++--- .../supported-shells/fish.spec.js | 8 +++++++- .../supported-shells/powershell.js | 19 ++++++++++++++++--- .../supported-shells/powershell.spec.js | 8 +++++++- .../supported-shells/windowsPowershell.js | 19 ++++++++++++++++--- .../windowsPowershell.spec.js | 8 +++++++- src/shell-integration/supported-shells/zsh.js | 19 ++++++++++++++++--- .../supported-shells/zsh.spec.js | 8 +++++++- 11 files changed, 116 insertions(+), 29 deletions(-) diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index 4226801..9075e66 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -1,4 +1,4 @@ -import { execSync, spawnSync } from "child_process"; +import { spawnSync } from "child_process"; import * as os from "os"; import fs from "fs"; @@ -26,14 +26,6 @@ export function doesExecutableExistOnSystem(executableName) { } } -export function execAndGetOutput(command, shell) { - try { - return execSync(command, { encoding: "utf8", shell }).trim(); - } catch (error) { - throw new Error(`Command failed: ${command}. Error: ${error.message}`); - } -} - export function removeLinesMatchingPattern(filePath, pattern) { if (!fs.existsSync(filePath)) { return; diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js index 35d23c1..fd18903 100644 --- a/src/shell-integration/supported-shells/bash.js +++ b/src/shell-integration/supported-shells/bash.js @@ -1,9 +1,9 @@ import { addLineToFile, doesExecutableExistOnSystem, - execAndGetOutput, removeLinesMatchingPattern, } from "../helpers.js"; +import { execSync } from "child_process"; const shellName = "Bash"; const executableName = "bash"; @@ -14,7 +14,7 @@ function isInstalled() { } function teardown() { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" // This will remove the safe-chain aliases for npm, npx, and yarn commands. @@ -24,7 +24,7 @@ function teardown() { } function setup(tools) { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); teardown(); for (const { tool, aikidoCommand } of tools) { @@ -37,6 +37,19 @@ function setup(tools) { return true; } +function getStartupFile() { + try { + return execSync(startupFileCommand, { + encoding: "utf8", + shell: executableName, + }).trim(); + } catch (error) { + throw new Error( + `Command failed: ${startupFileCommand}. Error: ${error.message}` + ); + } +} + export default { name: shellName, isInstalled, diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js index 21be84e..c852a85 100644 --- a/src/shell-integration/supported-shells/bash.spec.js +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -15,7 +15,6 @@ describe("Bash shell integration", () => { // Mock the helpers module mock.module("../helpers.js", { namedExports: { - execAndGetOutput: () => mockStartupFile, doesExecutableExistOnSystem: () => true, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { @@ -33,6 +32,13 @@ describe("Bash shell integration", () => { }, }); + // Mock child_process execSync + mock.module("child_process", { + namedExports: { + execSync: () => mockStartupFile, + }, + }); + // Import bash module after mocking bash = (await import("./bash.js")).default; }); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index 429d351..9269262 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -1,9 +1,9 @@ import { addLineToFile, doesExecutableExistOnSystem, - execAndGetOutput, removeLinesMatchingPattern, } from "../helpers.js"; +import { execSync } from "child_process"; const shellName = "Fish"; const executableName = "fish"; @@ -14,7 +14,7 @@ function isInstalled() { } function teardown() { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" // This will remove the safe-chain aliases for npm, npx, and yarn commands. @@ -24,7 +24,7 @@ function teardown() { } function setup(tools) { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); teardown(); for (const { tool, aikidoCommand } of tools) { @@ -37,6 +37,19 @@ function setup(tools) { return true; } +function getStartupFile() { + try { + return execSync(startupFileCommand, { + encoding: "utf8", + shell: executableName, + }).trim(); + } catch (error) { + throw new Error( + `Command failed: ${startupFileCommand}. Error: ${error.message}` + ); + } +} + export default { name: shellName, isInstalled, diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index 15344a3..988b24f 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -15,7 +15,6 @@ describe("Fish shell integration", () => { // Mock the helpers module mock.module("../helpers.js", { namedExports: { - execAndGetOutput: () => mockStartupFile, doesExecutableExistOnSystem: () => true, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { @@ -33,6 +32,13 @@ describe("Fish shell integration", () => { } }); + // Mock child_process execSync + mock.module("child_process", { + namedExports: { + execSync: () => mockStartupFile, + }, + }); + // Import fish module after mocking fish = (await import("./fish.js")).default; }); diff --git a/src/shell-integration/supported-shells/powershell.js b/src/shell-integration/supported-shells/powershell.js index f07efbf..f83093e 100644 --- a/src/shell-integration/supported-shells/powershell.js +++ b/src/shell-integration/supported-shells/powershell.js @@ -1,9 +1,9 @@ import { addLineToFile, doesExecutableExistOnSystem, - execAndGetOutput, removeLinesMatchingPattern, } from "../helpers.js"; +import { execSync } from "child_process"; const shellName = "PowerShell Core"; const executableName = "pwsh"; @@ -14,7 +14,7 @@ function isInstalled() { } function teardown() { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); // Removes all aliases starting with "Set-Alias npm=", "Set-Alias npx=", or "Set-Alias yarn=" // This will remove the safe-chain aliases for npm, npx, and yarn commands. @@ -24,7 +24,7 @@ function teardown() { } function setup(tools) { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); teardown(); for (const { tool, aikidoCommand } of tools) { @@ -37,6 +37,19 @@ function setup(tools) { return true; } +function getStartupFile() { + try { + return execSync(startupFileCommand, { + encoding: "utf8", + shell: executableName, + }).trim(); + } catch (error) { + throw new Error( + `Command failed: ${startupFileCommand}. Error: ${error.message}` + ); + } +} + export default { name: shellName, isInstalled, diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 9d71d94..4006b13 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -15,7 +15,6 @@ describe("PowerShell Core shell integration", () => { // Mock the helpers module mock.module("../helpers.js", { namedExports: { - execAndGetOutput: () => mockStartupFile, doesExecutableExistOnSystem: () => true, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { @@ -33,6 +32,13 @@ describe("PowerShell Core shell integration", () => { } }); + // Mock child_process execSync + mock.module("child_process", { + namedExports: { + execSync: () => mockStartupFile, + }, + }); + // Import powershell module after mocking powershell = (await import("./powershell.js")).default; }); diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js index 381b987..584a447 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.js +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -1,9 +1,9 @@ import { addLineToFile, doesExecutableExistOnSystem, - execAndGetOutput, removeLinesMatchingPattern, } from "../helpers.js"; +import { execSync } from "child_process"; const shellName = "Windows PowerShell"; const executableName = "powershell"; @@ -14,7 +14,7 @@ function isInstalled() { } function teardown() { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); // Removes all aliases starting with "Set-Alias npm=", "Set-Alias npx=", or "Set-Alias yarn=" // This will remove the safe-chain aliases for npm, npx, and yarn commands. @@ -24,7 +24,7 @@ function teardown() { } function setup(tools) { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); teardown(); for (const { tool, aikidoCommand } of tools) { @@ -37,6 +37,19 @@ function setup(tools) { return true; } +function getStartupFile() { + try { + return execSync(startupFileCommand, { + encoding: "utf8", + shell: executableName, + }).trim(); + } catch (error) { + throw new Error( + `Command failed: ${startupFileCommand}. Error: ${error.message}` + ); + } +} + export default { name: shellName, isInstalled, diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js index fe8b64f..f8dd182 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -15,7 +15,6 @@ describe("Windows PowerShell shell integration", () => { // Mock the helpers module mock.module("../helpers.js", { namedExports: { - execAndGetOutput: () => mockStartupFile, doesExecutableExistOnSystem: () => true, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { @@ -33,6 +32,13 @@ describe("Windows PowerShell shell integration", () => { } }); + // Mock child_process execSync + mock.module("child_process", { + namedExports: { + execSync: () => mockStartupFile, + }, + }); + // Import windowsPowershell module after mocking windowsPowershell = (await import("./windowsPowershell.js")).default; }); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index d0b179b..dbcf072 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -1,9 +1,9 @@ import { addLineToFile, doesExecutableExistOnSystem, - execAndGetOutput, removeLinesMatchingPattern, } from "../helpers.js"; +import { execSync } from "child_process"; const shellName = "Zsh"; const executableName = "zsh"; @@ -14,7 +14,7 @@ function isInstalled() { } function teardown() { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" // This will remove the safe-chain aliases for npm, npx, and yarn commands. @@ -24,7 +24,7 @@ function teardown() { } function setup(tools) { - const startupFile = execAndGetOutput(startupFileCommand, executableName); + const startupFile = getStartupFile(); teardown(); for (const { tool, aikidoCommand } of tools) { @@ -37,6 +37,19 @@ function setup(tools) { return true; } +function getStartupFile() { + try { + return execSync(startupFileCommand, { + encoding: "utf8", + shell: executableName, + }).trim(); + } catch (error) { + throw new Error( + `Command failed: ${startupFileCommand}. Error: ${error.message}` + ); + } +} + export default { name: shellName, isInstalled, diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index 37cc20f..8dabe87 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -15,7 +15,6 @@ describe("Zsh shell integration", () => { // Mock the helpers module mock.module("../helpers.js", { namedExports: { - execAndGetOutput: () => mockStartupFile, doesExecutableExistOnSystem: () => true, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { @@ -33,6 +32,13 @@ describe("Zsh shell integration", () => { }, }); + // Mock child_process execSync + mock.module("child_process", { + namedExports: { + execSync: () => mockStartupFile, + }, + }); + // Import zsh module after mocking zsh = (await import("./zsh.js")).default; }); From 6c269a1bb52ea3f6a9eb1280fdb307d3ea0d2879 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 18 Jul 2025 09:53:58 +0000 Subject: [PATCH 08/11] Update src/shell-integration/supported-shells/powershell.spec.js --- src/shell-integration/supported-shells/powershell.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 4006b13..6ebe732 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -200,4 +200,4 @@ describe("PowerShell Core shell integration", () => { assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); }); }); -}); \ No newline at end of file +}); From 36c195f5a90857c27a8f30b7d9f0bd89087adbf9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 14:23:51 +0200 Subject: [PATCH 09/11] Handle comments from the PR --- package.json | 3 +- src/shell-integration/helpers.js | 16 ++--- src/shell-integration/setup.js | 1 + src/shell-integration/shellDetection.js | 14 +++- .../supported-shells/bash.js | 10 +-- .../supported-shells/bash.spec.js | 30 +++----- .../supported-shells/fish.js | 13 ++-- .../supported-shells/fish.spec.js | 58 ++++++++-------- .../supported-shells/powershell.js | 13 ++-- .../supported-shells/powershell.spec.js | 67 +++++++++--------- .../supported-shells/windowsPowershell.js | 13 ++-- .../windowsPowershell.spec.js | 69 ++++++++++--------- src/shell-integration/supported-shells/zsh.js | 10 +-- .../supported-shells/zsh.spec.js | 30 +++----- 14 files changed, 166 insertions(+), 181 deletions(-) diff --git a/package.json b/package.json index 4ab4c95..7bd518b 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,6 @@ "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" }, - "homepage": "https://github.com/AikidoSec/safe-chain#readme" + "homepage": "https://github.com/AikidoSec/safe-chain#readme", + "packageManager": "npm@11.4.1+sha512.fcee43884166b6f9c5d04535fb95650e9708b6948a1f797eddf40e9778646778a518dfa32651b1c62ff36f4ac42becf177ca46ca27d53f24b539190c8d91802b" } diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index 9075e66..7808c7e 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -13,16 +13,12 @@ export const knownAikidoTools = [ ]; export function doesExecutableExistOnSystem(executableName) { - try { - if (os.platform() === "win32") { - const result = spawnSync("where", [executableName], { stdio: "ignore" }); - return result.status === 0; - } else { - const result = spawnSync("which", [executableName], { stdio: "ignore" }); - return result.status === 0; - } - } catch { - return false; + if (os.platform() === "win32") { + const result = spawnSync("where", [executableName], { stdio: "ignore" }); + return result.status === 0; + } else { + const result = spawnSync("which", [executableName], { stdio: "ignore" }); + return result.status === 0; } } diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 85d319b..e5be973 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -51,6 +51,7 @@ export async function setup() { function setupShell(shell) { let success = false; try { + shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases success = shell.setup(knownAikidoTools); } catch { success = false; diff --git a/src/shell-integration/shellDetection.js b/src/shell-integration/shellDetection.js index e1bb52c..d868f6f 100644 --- a/src/shell-integration/shellDetection.js +++ b/src/shell-integration/shellDetection.js @@ -3,15 +3,23 @@ import bash from "./supported-shells/bash.js"; import powershell from "./supported-shells/powershell.js"; import windowsPowershell from "./supported-shells/windowsPowershell.js"; import fish from "./supported-shells/fish.js"; +import { ui } from "../environment/userInteraction.js"; export function detectShells() { let possibleShells = [zsh, bash, powershell, windowsPowershell, fish]; let availableShells = []; - for (const shell of possibleShells) { - if (shell.isInstalled()) { - availableShells.push(shell); + try { + for (const shell of possibleShells) { + if (shell.isInstalled()) { + availableShells.push(shell); + } } + } catch (error) { + ui.writeError( + `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}` + ); + return []; } return availableShells; diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js index fd18903..66b844d 100644 --- a/src/shell-integration/supported-shells/bash.js +++ b/src/shell-integration/supported-shells/bash.js @@ -13,19 +13,19 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } -function teardown() { +function teardown(tools) { const startupFile = getStartupFile(); - // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" - // This will remove the safe-chain aliases for npm, npx, and yarn commands. - removeLinesMatchingPattern(startupFile, /^alias\s+(npm|npx|yarn)=/); + for (const { tool } of tools) { + // Remove any existing alias for the tool + removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); + } return true; } function setup(tools) { const startupFile = getStartupFile(); - teardown(); for (const { tool, aikidoCommand } of tools) { addLineToFile( diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js index c852a85..ce666e5 100644 --- a/src/shell-integration/supported-shells/bash.spec.js +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -3,6 +3,7 @@ import assert from "node:assert"; import { tmpdir } from "node:os"; import fs from "node:fs"; import path from "path"; +import { knownAikidoTools } from "../helpers.js"; describe("Bash shell integration", () => { let mockStartupFile; @@ -69,7 +70,7 @@ describe("Bash shell integration", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; const result = bash.setup(tools); @@ -87,22 +88,6 @@ describe("Bash shell integration", () => { ); }); - it("should call teardown before setup", () => { - // Pre-populate file with existing aliases - fs.writeFileSync( - mockStartupFile, - 'alias npm="old-npm"\nalias npx="old-npx"\n', - "utf-8" - ); - - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - bash.setup(tools); - - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('alias npm="old-npm"')); - assert.ok(content.includes('alias npm="aikido-npm"')); - }); - it("should handle empty tools array", () => { const result = bash.setup([]); assert.strictEqual(result, true); @@ -128,7 +113,7 @@ describe("Bash shell integration", () => { fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = bash.teardown(); + const result = bash.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -144,7 +129,7 @@ describe("Bash shell integration", () => { fs.unlinkSync(mockStartupFile); } - const result = bash.teardown(); + const result = bash.teardown(knownAikidoTools); assert.strictEqual(result, true); }); @@ -157,7 +142,7 @@ describe("Bash shell integration", () => { fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = bash.teardown(); + const result = bash.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -183,7 +168,7 @@ describe("Bash shell integration", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; // Setup @@ -193,7 +178,7 @@ describe("Bash shell integration", () => { assert.ok(content.includes('alias yarn="aikido-yarn"')); // Teardown - bash.teardown(); + bash.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); assert.ok(!content.includes("alias yarn=")); @@ -203,6 +188,7 @@ describe("Bash shell integration", () => { const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; bash.setup(tools); + bash.teardown(tools); bash.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index 9269262..fc6fc85 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -13,19 +13,22 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } -function teardown() { +function teardown(tools) { const startupFile = getStartupFile(); - // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" - // This will remove the safe-chain aliases for npm, npx, and yarn commands. - removeLinesMatchingPattern(startupFile, /^alias\s+(npm|npx|yarn)\s+/); + for (const { tool } of tools) { + // Remove any existing alias for the tool + removeLinesMatchingPattern( + startupFile, + new RegExp(`^alias\\s+${tool}\\s+`) + ); + } return true; } function setup(tools) { const startupFile = getStartupFile(); - teardown(); for (const { tool, aikidoCommand } of tools) { addLineToFile( diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index 988b24f..5f1ab64 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -3,6 +3,7 @@ import assert from "node:assert"; import { tmpdir } from "node:os"; import fs from "node:fs"; import path from "path"; +import { knownAikidoTools } from "../helpers.js"; describe("Fish shell integration", () => { let mockStartupFile; @@ -11,7 +12,7 @@ describe("Fish shell integration", () => { beforeEach(async () => { // Create temporary startup file for testing mockStartupFile = path.join(tmpdir(), `test-fish-config-${Date.now()}`); - + // Mock the helpers module mock.module("../helpers.js", { namedExports: { @@ -26,10 +27,10 @@ describe("Fish shell integration", () => { if (!fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); - const filteredLines = lines.filter(line => !pattern.test(line)); + const filteredLines = lines.filter((line) => !pattern.test(line)); fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); - } - } + }, + }, }); // Mock child_process execSync @@ -48,7 +49,7 @@ describe("Fish shell integration", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); } - + // Reset mocks mock.reset(); }); @@ -69,34 +70,28 @@ describe("Fish shell integration", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; const result = fish.setup(tools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm "aikido-npm" # Safe-chain alias for npm')); - assert.ok(content.includes('alias npx "aikido-npx" # Safe-chain alias for npx')); - assert.ok(content.includes('alias yarn "aikido-yarn" # Safe-chain alias for yarn')); - }); - - it("should call teardown before setup", () => { - // Pre-populate file with existing aliases - fs.writeFileSync(mockStartupFile, 'alias npm "old-npm"\nalias npx "old-npx"\n', "utf-8"); - - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - fish.setup(tools); - - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('alias npm "old-npm"')); - assert.ok(content.includes('alias npm "aikido-npm"')); + assert.ok( + content.includes('alias npm "aikido-npm" # Safe-chain alias for npm') + ); + assert.ok( + content.includes('alias npx "aikido-npx" # Safe-chain alias for npx') + ); + assert.ok( + content.includes('alias yarn "aikido-yarn" # Safe-chain alias for yarn') + ); }); it("should handle empty tools array", () => { const result = fish.setup([]); assert.strictEqual(result, true); - + // File should be created during teardown call even if no tools are provided if (fs.existsSync(mockStartupFile)) { const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -113,12 +108,12 @@ describe("Fish shell integration", () => { "alias npx 'aikido-npx'", "alias yarn 'aikido-yarn'", "alias ls 'ls --color=auto'", - "alias grep 'grep --color=auto'" + "alias grep 'grep --color=auto'", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = fish.teardown(); + const result = fish.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -134,7 +129,7 @@ describe("Fish shell integration", () => { fs.unlinkSync(mockStartupFile); } - const result = fish.teardown(); + const result = fish.teardown(knownAikidoTools); assert.strictEqual(result, true); }); @@ -142,12 +137,12 @@ describe("Fish shell integration", () => { const initialContent = [ "#!/usr/bin/env fish", "alias ls 'ls --color=auto'", - "set PATH $PATH ~/bin" + "set PATH $PATH ~/bin", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = fish.teardown(); + const result = fish.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -173,7 +168,7 @@ describe("Fish shell integration", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; // Setup @@ -183,7 +178,7 @@ describe("Fish shell integration", () => { assert.ok(content.includes('alias yarn "aikido-yarn"')); // Teardown - fish.teardown(); + fish.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm ")); assert.ok(!content.includes("alias yarn ")); @@ -193,11 +188,12 @@ describe("Fish shell integration", () => { const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; fish.setup(tools); + fish.teardown(tools); fish.setup(tools); - + const content = fs.readFileSync(mockStartupFile, "utf-8"); const npmMatches = (content.match(/alias npm "/g) || []).length; assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); }); }); -}); \ No newline at end of file +}); diff --git a/src/shell-integration/supported-shells/powershell.js b/src/shell-integration/supported-shells/powershell.js index f83093e..4690bb6 100644 --- a/src/shell-integration/supported-shells/powershell.js +++ b/src/shell-integration/supported-shells/powershell.js @@ -13,19 +13,22 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } -function teardown() { +function teardown(tools) { const startupFile = getStartupFile(); - // Removes all aliases starting with "Set-Alias npm=", "Set-Alias npx=", or "Set-Alias yarn=" - // This will remove the safe-chain aliases for npm, npx, and yarn commands. - removeLinesMatchingPattern(startupFile, /^Set-Alias\s+(npm|npx|yarn)\s+/); + for (const { tool } of tools) { + // Remove any existing alias for the tool + removeLinesMatchingPattern( + startupFile, + new RegExp(`^Set-Alias\\s+${tool}\\s+`) + ); + } return true; } function setup(tools) { const startupFile = getStartupFile(); - teardown(); for (const { tool, aikidoCommand } of tools) { addLineToFile( diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 6ebe732..9afade7 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -3,6 +3,7 @@ import assert from "node:assert"; import { tmpdir } from "node:os"; import fs from "node:fs"; import path from "path"; +import { knownAikidoTools } from "../helpers.js"; describe("PowerShell Core shell integration", () => { let mockStartupFile; @@ -10,8 +11,11 @@ describe("PowerShell Core shell integration", () => { beforeEach(async () => { // Create temporary startup file for testing - mockStartupFile = path.join(tmpdir(), `test-powershell-profile-${Date.now()}.ps1`); - + mockStartupFile = path.join( + tmpdir(), + `test-powershell-profile-${Date.now()}.ps1` + ); + // Mock the helpers module mock.module("../helpers.js", { namedExports: { @@ -26,10 +30,10 @@ describe("PowerShell Core shell integration", () => { if (!fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); - const filteredLines = lines.filter(line => !pattern.test(line)); + const filteredLines = lines.filter((line) => !pattern.test(line)); fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); - } - } + }, + }, }); // Mock child_process execSync @@ -48,7 +52,7 @@ describe("PowerShell Core shell integration", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); } - + // Reset mocks mock.reset(); }); @@ -69,34 +73,30 @@ describe("PowerShell Core shell integration", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; const result = powershell.setup(tools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('Set-Alias npm aikido-npm # Safe-chain alias for npm')); - assert.ok(content.includes('Set-Alias npx aikido-npx # Safe-chain alias for npx')); - assert.ok(content.includes('Set-Alias yarn aikido-yarn # Safe-chain alias for yarn')); - }); - - it("should call teardown before setup", () => { - // Pre-populate file with existing aliases - fs.writeFileSync(mockStartupFile, 'Set-Alias npm old-npm\nSet-Alias npx old-npx\n', "utf-8"); - - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - powershell.setup(tools); - - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('Set-Alias npm old-npm')); - assert.ok(content.includes('Set-Alias npm aikido-npm')); + assert.ok( + content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm") + ); + assert.ok( + content.includes("Set-Alias npx aikido-npx # Safe-chain alias for npx") + ); + assert.ok( + content.includes( + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn" + ) + ); }); it("should handle empty tools array", () => { const result = powershell.setup([]); assert.strictEqual(result, true); - + // File should be created during teardown call even if no tools are provided if (fs.existsSync(mockStartupFile)) { const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -113,12 +113,12 @@ describe("PowerShell Core shell integration", () => { "Set-Alias npx aikido-npx", "Set-Alias yarn aikido-yarn", "Set-Alias ls Get-ChildItem", - "Set-Alias grep Select-String" + "Set-Alias grep Select-String", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = powershell.teardown(); + const result = powershell.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -134,7 +134,7 @@ describe("PowerShell Core shell integration", () => { fs.unlinkSync(mockStartupFile); } - const result = powershell.teardown(); + const result = powershell.teardown(knownAikidoTools); assert.strictEqual(result, true); }); @@ -142,12 +142,12 @@ describe("PowerShell Core shell integration", () => { const initialContent = [ "# PowerShell profile", "Set-Alias ls Get-ChildItem", - "$env:PATH += ';C:\\Tools'" + "$env:PATH += ';C:\\Tools'", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = powershell.teardown(); + const result = powershell.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -173,17 +173,17 @@ describe("PowerShell Core shell integration", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; // Setup powershell.setup(tools); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('Set-Alias npm aikido-npm')); - assert.ok(content.includes('Set-Alias yarn aikido-yarn')); + assert.ok(content.includes("Set-Alias npm aikido-npm")); + assert.ok(content.includes("Set-Alias yarn aikido-yarn")); // Teardown - powershell.teardown(); + powershell.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("Set-Alias npm ")); assert.ok(!content.includes("Set-Alias yarn ")); @@ -193,8 +193,9 @@ describe("PowerShell Core shell integration", () => { const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; powershell.setup(tools); + powershell.teardown(tools); powershell.setup(tools); - + const content = fs.readFileSync(mockStartupFile, "utf-8"); const npmMatches = (content.match(/Set-Alias npm /g) || []).length; assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js index 584a447..118a0b9 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.js +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -13,19 +13,22 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } -function teardown() { +function teardown(tools) { const startupFile = getStartupFile(); - // Removes all aliases starting with "Set-Alias npm=", "Set-Alias npx=", or "Set-Alias yarn=" - // This will remove the safe-chain aliases for npm, npx, and yarn commands. - removeLinesMatchingPattern(startupFile, /^Set-Alias\s+(npm|npx|yarn)\s+/); + for (const { tool } of tools) { + // Remove any existing alias for the tool + removeLinesMatchingPattern( + startupFile, + new RegExp(`^Set-Alias\\s+${tool}\\s+`) + ); + } return true; } function setup(tools) { const startupFile = getStartupFile(); - teardown(); for (const { tool, aikidoCommand } of tools) { addLineToFile( diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js index f8dd182..85da9f1 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -3,6 +3,7 @@ import assert from "node:assert"; import { tmpdir } from "node:os"; import fs from "node:fs"; import path from "path"; +import { knownAikidoTools } from "../helpers.js"; describe("Windows PowerShell shell integration", () => { let mockStartupFile; @@ -10,8 +11,11 @@ describe("Windows PowerShell shell integration", () => { beforeEach(async () => { // Create temporary startup file for testing - mockStartupFile = path.join(tmpdir(), `test-windows-powershell-profile-${Date.now()}.ps1`); - + mockStartupFile = path.join( + tmpdir(), + `test-windows-powershell-profile-${Date.now()}.ps1` + ); + // Mock the helpers module mock.module("../helpers.js", { namedExports: { @@ -26,10 +30,10 @@ describe("Windows PowerShell shell integration", () => { if (!fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); - const filteredLines = lines.filter(line => !pattern.test(line)); + const filteredLines = lines.filter((line) => !pattern.test(line)); fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); - } - } + }, + }, }); // Mock child_process execSync @@ -48,7 +52,7 @@ describe("Windows PowerShell shell integration", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); } - + // Reset mocks mock.reset(); }); @@ -69,34 +73,30 @@ describe("Windows PowerShell shell integration", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; const result = windowsPowershell.setup(tools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('Set-Alias npm aikido-npm # Safe-chain alias for npm')); - assert.ok(content.includes('Set-Alias npx aikido-npx # Safe-chain alias for npx')); - assert.ok(content.includes('Set-Alias yarn aikido-yarn # Safe-chain alias for yarn')); - }); - - it("should call teardown before setup", () => { - // Pre-populate file with existing aliases - fs.writeFileSync(mockStartupFile, 'Set-Alias npm old-npm\nSet-Alias npx old-npx\n', "utf-8"); - - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - windowsPowershell.setup(tools); - - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('Set-Alias npm old-npm')); - assert.ok(content.includes('Set-Alias npm aikido-npm')); + assert.ok( + content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm") + ); + assert.ok( + content.includes("Set-Alias npx aikido-npx # Safe-chain alias for npx") + ); + assert.ok( + content.includes( + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn" + ) + ); }); it("should handle empty tools array", () => { const result = windowsPowershell.setup([]); assert.strictEqual(result, true); - + // File should be created during teardown call even if no tools are provided if (fs.existsSync(mockStartupFile)) { const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -113,12 +113,12 @@ describe("Windows PowerShell shell integration", () => { "Set-Alias npx aikido-npx", "Set-Alias yarn aikido-yarn", "Set-Alias ls Get-ChildItem", - "Set-Alias grep Select-String" + "Set-Alias grep Select-String", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = windowsPowershell.teardown(); + const result = windowsPowershell.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -134,7 +134,7 @@ describe("Windows PowerShell shell integration", () => { fs.unlinkSync(mockStartupFile); } - const result = windowsPowershell.teardown(); + const result = windowsPowershell.teardown(knownAikidoTools); assert.strictEqual(result, true); }); @@ -142,12 +142,12 @@ describe("Windows PowerShell shell integration", () => { const initialContent = [ "# Windows PowerShell profile", "Set-Alias ls Get-ChildItem", - "$env:PATH += ';C:\\Tools'" + "$env:PATH += ';C:\\Tools'", ].join("\n"); fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = windowsPowershell.teardown(); + const result = windowsPowershell.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -173,17 +173,17 @@ describe("Windows PowerShell shell integration", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; // Setup windowsPowershell.setup(tools); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('Set-Alias npm aikido-npm')); - assert.ok(content.includes('Set-Alias yarn aikido-yarn')); + assert.ok(content.includes("Set-Alias npm aikido-npm")); + assert.ok(content.includes("Set-Alias yarn aikido-yarn")); // Teardown - windowsPowershell.teardown(); + windowsPowershell.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("Set-Alias npm ")); assert.ok(!content.includes("Set-Alias yarn ")); @@ -193,11 +193,12 @@ describe("Windows PowerShell shell integration", () => { const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; windowsPowershell.setup(tools); + windowsPowershell.teardown(tools); windowsPowershell.setup(tools); - + const content = fs.readFileSync(mockStartupFile, "utf-8"); const npmMatches = (content.match(/Set-Alias npm /g) || []).length; assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); }); }); -}); \ No newline at end of file +}); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index dbcf072..965c814 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -13,19 +13,19 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } -function teardown() { +function teardown(tools) { const startupFile = getStartupFile(); - // Removes all aliases starting with "alias npm=", "alias npx=", or "alias yarn=" - // This will remove the safe-chain aliases for npm, npx, and yarn commands. - removeLinesMatchingPattern(startupFile, /^alias\s+(npm|npx|yarn)=/); + for (const { tool } of tools) { + // Remove any existing alias for the tool + removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); + } return true; } function setup(tools) { const startupFile = getStartupFile(); - teardown(); for (const { tool, aikidoCommand } of tools) { addLineToFile( diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index 8dabe87..e284c50 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -3,6 +3,7 @@ import assert from "node:assert"; import { tmpdir } from "node:os"; import fs from "node:fs"; import path from "path"; +import { knownAikidoTools } from "../helpers.js"; describe("Zsh shell integration", () => { let mockStartupFile; @@ -69,7 +70,7 @@ describe("Zsh shell integration", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; const result = zsh.setup(tools); @@ -87,22 +88,6 @@ describe("Zsh shell integration", () => { ); }); - it("should call teardown before setup", () => { - // Pre-populate file with existing aliases - fs.writeFileSync( - mockStartupFile, - 'alias npm="old-npm"\nalias npx="old-npx"\n', - "utf-8" - ); - - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - zsh.setup(tools); - - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('alias npm="old-npm"')); - assert.ok(content.includes('alias npm="aikido-npm"')); - }); - it("should handle empty tools array", () => { const result = zsh.setup([]); assert.strictEqual(result, true); @@ -128,7 +113,7 @@ describe("Zsh shell integration", () => { fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = zsh.teardown(); + const result = zsh.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -144,7 +129,7 @@ describe("Zsh shell integration", () => { fs.unlinkSync(mockStartupFile); } - const result = zsh.teardown(); + const result = zsh.teardown(knownAikidoTools); assert.strictEqual(result, true); }); @@ -157,7 +142,7 @@ describe("Zsh shell integration", () => { fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); - const result = zsh.teardown(); + const result = zsh.teardown(knownAikidoTools); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -183,7 +168,7 @@ describe("Zsh shell integration", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" } + { tool: "yarn", aikidoCommand: "aikido-yarn" }, ]; // Setup @@ -193,7 +178,7 @@ describe("Zsh shell integration", () => { assert.ok(content.includes('alias yarn="aikido-yarn"')); // Teardown - zsh.teardown(); + zsh.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); assert.ok(!content.includes("alias yarn=")); @@ -203,6 +188,7 @@ describe("Zsh shell integration", () => { const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; zsh.setup(tools); + zsh.teardown(tools); zsh.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); From 0f354ccbb8ace9b23fa60132ef275074cb2ec0f7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 14:49:21 +0200 Subject: [PATCH 10/11] Remove packagemanager from package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 7bd518b..4ab4c95 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,5 @@ "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" }, - "homepage": "https://github.com/AikidoSec/safe-chain#readme", - "packageManager": "npm@11.4.1+sha512.fcee43884166b6f9c5d04535fb95650e9708b6948a1f797eddf40e9778646778a518dfa32651b1c62ff36f4ac42becf177ca46ca27d53f24b539190c8d91802b" + "homepage": "https://github.com/AikidoSec/safe-chain#readme" } From 2aa8830b25bcfc11cbd52a95d4b7fda6997490e1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 15:05:09 +0200 Subject: [PATCH 11/11] Fix bug in teardown: missing argument --- src/shell-integration/teardown.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shell-integration/teardown.js b/src/shell-integration/teardown.js index 00a933f..9f74300 100644 --- a/src/shell-integration/teardown.js +++ b/src/shell-integration/teardown.js @@ -1,6 +1,7 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; +import { knownAikidoTools } from "./helpers.js"; export async function teardown() { ui.writeInformation( @@ -26,7 +27,7 @@ export async function teardown() { for (const shell of shells) { let success = false; try { - success = shell.teardown(); + success = shell.teardown(knownAikidoTools); } catch { success = false; }