From f10749923a34a38d827a803e06612cad005f4a66 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 10:08:49 +0200 Subject: [PATCH 01/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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 fe1ca396b437e0cc70ae173ba9d7fac13cd6c854 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:35:03 +0200 Subject: [PATCH 07/57] Refactor to customize shell integration per shell --- src/shell-integration/helpers.js | 70 ++-- src/shell-integration/setup.js | 129 ++------ src/shell-integration/setup.spec.js | 304 ------------------ src/shell-integration/shellDetection.js | 75 +---- .../supported-shells/bash.js | 45 +++ .../supported-shells/bash.spec.js | 200 ++++++++++++ .../supported-shells/fish.js | 45 +++ .../supported-shells/fish.spec.js | 190 +++++++++++ .../supported-shells/powershell.js | 45 +++ .../supported-shells/powershell.spec.js | 197 ++++++++++++ .../supported-shells/windowsPowershell.js | 45 +++ .../windowsPowershell.spec.js | 197 ++++++++++++ src/shell-integration/supported-shells/zsh.js | 45 +++ .../supported-shells/zsh.spec.js | 200 ++++++++++++ src/shell-integration/teardown.js | 118 ++----- src/shell-integration/teardown.spec.js | 177 ---------- 16 files changed, 1302 insertions(+), 780 deletions(-) delete mode 100644 src/shell-integration/setup.spec.js create mode 100644 src/shell-integration/supported-shells/bash.js create mode 100644 src/shell-integration/supported-shells/bash.spec.js create mode 100644 src/shell-integration/supported-shells/fish.js create mode 100644 src/shell-integration/supported-shells/fish.spec.js create mode 100644 src/shell-integration/supported-shells/powershell.js create mode 100644 src/shell-integration/supported-shells/powershell.spec.js create mode 100644 src/shell-integration/supported-shells/windowsPowershell.js create mode 100644 src/shell-integration/supported-shells/windowsPowershell.spec.js create mode 100644 src/shell-integration/supported-shells/zsh.js create mode 100644 src/shell-integration/supported-shells/zsh.spec.js delete mode 100644 src/shell-integration/teardown.spec.js diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index c9b4c5e..eeae002 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -1,4 +1,8 @@ -const knownAikidoTools = [ +import { execSync } from "child_process"; +import * as os from "os"; +import fs from "fs"; + +export const knownAikidoTools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, { tool: "yarn", aikidoCommand: "aikido-yarn" }, @@ -6,39 +10,43 @@ const knownAikidoTools = [ // and add the documentation for the new tool in the README.md ]; -export function getAliases(fileName) { - const fileExtension = fileName.split(".").pop().toLowerCase(); - - let createAlias = pickCreateAliasFunction(fileExtension); - - const aliases = knownAikidoTools.map(({ tool, aikidoCommand }) => - createAlias(tool, aikidoCommand) - ); - - return aliases; -} - -function pickCreateAliasFunction(fileExtension) { - let createAlias; - switch (fileExtension) { - case "ps1": - createAlias = createGeneralPowershellAlias; - break; - case "fish": - createAlias = createGeneralFishAlias; - break; - default: - createAlias = createGeneralPosixAlias; +export function doesExecutableExistOnSystem(executableName) { + try { + if (os.platform() === "win32") { + execSync(`where ${executableName}`, { stdio: "ignore" }); + } else { + execSync(`which ${executableName}`, { stdio: "ignore" }); + } + return true; + } catch { + return false; } - return createAlias; } -function createGeneralPosixAlias(tool, aikidoCommand) { - return `alias ${tool}='${aikidoCommand}'`; +export function execAndGetOutput(command, shell) { + try { + return execSync(command, { encoding: "utf8", shell }).trim(); + } catch (error) { + throw new Error(`Command failed: ${command}. Error: ${error.message}`); + } } -function createGeneralPowershellAlias(tool, aikidoCommand) { - return `Set-Alias ${tool} ${aikidoCommand}`; + +export function removeLinesMatchingPattern(filePath, pattern) { + if (!fs.existsSync(filePath)) { + return; + } + + const fileContent = fs.readFileSync(filePath, "utf-8"); + const lines = fileContent.split(os.EOL); + const updatedLines = lines.filter((line) => !pattern.test(line)); + fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8"); } -function createGeneralFishAlias(tool, aikidoCommand) { - return `alias ${tool} "${aikidoCommand}"`; + +export function addLineToFile(filePath, line) { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + const fileContent = fs.readFileSync(filePath, "utf-8"); + const updatedContent = fileContent + os.EOL + line; + fs.writeFileSync(filePath, updatedContent, "utf-8"); } diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index d6a2566..85d319b 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -1,10 +1,11 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { getAliases } from "./helpers.js"; -import fs from "fs"; -import { EOL } from "os"; +import { knownAikidoTools } from "./helpers.js"; +/** + * Loops over the detected shells and calls the setup function for each. + */ export async function setup() { ui.writeInformation( chalk.bold("Setting up shell aliases.") + @@ -27,7 +28,7 @@ export async function setup() { let updatedCount = 0; for (const shell of shells) { - if (setupAliasesForShell(shell)) { + if (setupShell(shell)) { updatedCount++; } } @@ -45,107 +46,29 @@ export async function setup() { } /** - * This function sets up aliases for the given shell. - * It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.), - * and then appends the aliases for npm, npx, and yarn commands. - * If the aliases already exist, it will not add them again. - * If the startup file does not exist, it will create it. - * - * The shell startup script is loaded by the respective shell when it starts. - * This means that the aliases will be available in the shell after it is restarted. + * Calls the setup function for the given shell and reports the result. */ -function setupAliasesForShell(shell) { - if (!shell.startupFile) { - ui.writeError( - `- ${chalk.bold( - shell.name - )}: no startup file found. Cannot set up aliases.` +function setupShell(shell) { + let success = false; + try { + success = shell.setup(knownAikidoTools); + } catch { + success = false; + } + + if (success) { + ui.writeInformation( + `${chalk.bold("- " + shell.name + ":")} ${chalk.green( + "Setup successful" + )}` + ); + } else { + ui.writeError( + `${chalk.bold("- " + shell.name + ":")} ${chalk.red( + "Setup failed" + )}. Please check your ${shell.name} configuration.` ); - return false; } - const aliases = getAliases(shell.startupFile); - - if (aliases.length === 0) { - ui.writeError(`- ${chalk.bold(shell.name)}: could not generate aliases.`); - return false; - } - - const fileContent = readOrCreateStartupFile(shell.startupFile); - const { addedCount, existingCount, failedCount } = appendAliasesToFile( - aliases, - fileContent, - shell.startupFile - ); - - let summary = "- " + chalk.bold(shell.name) + ": "; - - if (addedCount > 0) { - summary += chalk.green(`${addedCount} aliases were added`); - } - if (existingCount > 0) { - if (addedCount > 0) { - summary += ", "; - } - summary += chalk.yellow(`${existingCount} aliases were already present`); - } - if (failedCount > 0) { - if (addedCount > 0 || existingCount > 0) { - summary += ", "; - } - summary += chalk.red(`${failedCount} aliases failed to add`); - } - - // write summary in a single line - ui.writeInformation(summary); - - return true; -} - -/** - * This reads the content of the startup file. - * If the file does not exist, it creates an empty file and returns an empty string. - * The startup file is the shell's startup script (eg: ~/.bashrc, ~/.zshrc, etc.). - * It is used to set up the shell environment when it starts. - * Some shells may not have a startup file, in which case this function will create one. - */ -export function readOrCreateStartupFile(filePath) { - if (!fs.existsSync(filePath)) { - fs.writeFileSync(filePath, "", "utf-8"); - ui.writeInformation(`File ${filePath} created.`); - } - return fs.readFileSync(filePath, "utf-8"); -} - -/** - * This function appends the aliases to the startup file. - * eg: for bash it will append 'alias npm="aikido-npm"' for npm to ~/.bashrc - * @returns an object with the counts of added, existing, and failed aliases. - */ -export function appendAliasesToFile(aliases, fileContent, startupFilePath) { - let addedCount = 0; - let existingCount = 0; - let failedCount = 0; - - for (const alias of aliases) { - try { - if (fileContent.includes(alias)) { - existingCount++; - continue; - } - - fs.appendFileSync(startupFilePath, `${EOL}${alias}`, "utf-8"); - - addedCount++; - } catch { - failedCount++; - continue; - } - } - - return { - addedCount, - existingCount, - failedCount, - }; + return success; } diff --git a/src/shell-integration/setup.spec.js b/src/shell-integration/setup.spec.js deleted file mode 100644 index cc5ae01..0000000 --- a/src/shell-integration/setup.spec.js +++ /dev/null @@ -1,304 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { EOL, tmpdir } from "node:os"; -import fs from "node:fs"; -import { getAliases } from "./helpers.js"; -import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js"; - -describe("setupShell", () => { - 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"); - - const updatedContent = readAndDeleteFile(filePath); - for (const alias of expectedAliases) { - assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`); - } - 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"); - - 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`); - } - }); - - it(`should create file and add aliases if file does not exist for ${shell}`, () => { - const randomName = Math.random().toString(36).substring(2, 15); - const filePath = `${tmpdir()}/nonexistent-${randomName}${startupExtension}`; - 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.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"); - - const updatedContent = readAndDeleteFile(filePath); - for (const alias of expectedAliases) { - 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"); - - // 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"); - - const updatedContent = readAndDeleteFile(filePath); - for (const alias of expectedAliases) { - 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)"); - for (let i = 0; i < aliases.length; i++) { - 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 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"); - - const updatedContent = readAndDeleteFile(filePath); - for (const alias of expectedAliases) { - assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be present`); - } - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain"); - }); - }); - } - - // Test for each shell type using real getAliases() output - runSetupTestsForEnvironment("bash", ".bashrc", [ - "alias npm='aikido-npm'", - "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" - ]); - - runSetupTestsForEnvironment("zsh", ".zshrc", [ - "alias npm='aikido-npm'", - "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" - ]); - - runSetupTestsForEnvironment("fish", ".fish", [ - 'alias npm "aikido-npm"', - 'alias npx "aikido-npx"', - 'alias yarn "aikido-yarn"' - ]); - - runSetupTestsForEnvironment("pwsh", ".ps1", [ - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn" - ]); - - 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"); - - // 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`; - if (fs.existsSync(filePath)) { - fs.rmSync(filePath, { force: true }); - } - - const content = readOrCreateStartupFile(filePath); - - 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`; - fs.writeFileSync(filePath, "", "utf-8"); - - const content = readOrCreateStartupFile(filePath); - - 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 }); - }); - }); - - describe("appendAliasesToFile edge cases", () => { - it("should handle empty aliases array", () => { - 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.failedCount, 0, "Should have 0 failed aliases"); - - const updatedContent = readAndDeleteFile(filePath); - assert.ok(updatedContent.includes("alias test='echo test'"), "Original content should remain"); - }); - - it("should handle partial substring matches correctly", () => { - const lines = [ - "#!/usr/bin/env bash", - "", - "alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm=' - "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.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"); - }); - - it("should handle file with only whitespace", () => { - 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.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"); - - // Cleanup - fs.rmSync(filePath, { force: true }); - }); - }); - - describe("appendAliasesToFile error handling", () => { - it("should handle file permission errors gracefully", () => { - 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.failedCount, 1, "Should have 1 failed alias"); - - // Restore permissions and cleanup - fs.chmodSync(filePath, 0o644); - fs.rmSync(filePath, { force: true }); - }); - }); -}); - -function createShellStartupScript(lines, fileExtension) { - const randomFileName = Math.random().toString(36).substring(2, 15); - const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`; - fs.writeFileSync(filePath, lines.join(EOL), "utf-8"); - return filePath; -} - -function readAndDeleteFile(filePath) { - const fileContent = fs.readFileSync(filePath, "utf-8"); - fs.rmSync(filePath, { force: true }); - return fileContent.split(EOL); -} - -function countOccurrences(lines, searchString) { - let count = 0; - for (const line of lines) { - if (line.includes(searchString)) { - count++; - } - } - return count; -} \ No newline at end of file diff --git a/src/shell-integration/shellDetection.js b/src/shell-integration/shellDetection.js index de38680..e1bb52c 100644 --- a/src/shell-integration/shellDetection.js +++ b/src/shell-integration/shellDetection.js @@ -1,75 +1,18 @@ -import * as os from "os"; -import { execSync } from "child_process"; - -const shellList = { - bash: { - name: "Bash", - executable: "bash", - getStartupFileCommand: "echo ~/.bashrc", - }, - zsh: { - name: "Zsh", - executable: "zsh", - getStartupFileCommand: "echo ${ZDOTDIR:-$HOME}/.zshrc", - }, - fish: { - name: "Fish", - executable: "fish", - getStartupFileCommand: "echo ~/.config/fish/config.fish", - }, - powershell: { - name: "PowerShell Core", - executable: "pwsh", - getStartupFileCommand: "echo $PROFILE", - }, - windowsPowerShell: { - name: "Windows PowerShell", - executable: "powershell", - getStartupFileCommand: "echo $PROFILE", - }, -}; +import zsh from "./supported-shells/zsh.js"; +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"; export function detectShells() { + let possibleShells = [zsh, bash, powershell, windowsPowershell, fish]; let availableShells = []; - for (const shellName of Object.keys(shellList)) { - const shell = shellList[shellName]; - - if (isShellAvailable(shell)) { - const startupFile = getShellStartupFile(shell); - availableShells.push({ - name: shell.name, - executable: shell.executable, - startupFile: startupFile || null, - }); + for (const shell of possibleShells) { + if (shell.isInstalled()) { + availableShells.push(shell); } } return availableShells; } - -function isShellAvailable(shell) { - try { - if (os.platform() === "win32") { - execSync(`where ${shell.executable}`, { stdio: "ignore" }); - } else { - execSync(`which ${shell.executable}`, { stdio: "ignore" }); - } - return true; - } catch { - return false; - } -} - -function getShellStartupFile(shell) { - try { - const command = shell.getStartupFileCommand; - const output = execSync(command, { - encoding: "utf8", - shell: shell.executable, - }).trim(); - return output; - } catch { - return null; - } -} diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js new file mode 100644 index 0000000..b53d401 --- /dev/null +++ b/src/shell-integration/supported-shells/bash.js @@ -0,0 +1,45 @@ +import { + addLineToFile, + doesExecutableExistOnSystem, + execAndGetOutput, + removeLinesMatchingPattern, +} from "../helpers.js"; + +const shellName = "Bash"; +const executableName = "bash"; +const startupFileCommand = "echo ~/.bashrc"; + +function isInstalled() { + return doesExecutableExistOnSystem(executableName); +} + +function teardown() { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + + // 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)=/); + + return true; +} + +function setup(tools) { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + teardown(); + + for (const tool of tools) { + addLineToFile( + startupFile, + `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` + ); + } + + return true; +} + +export default { + name: shellName, + isInstalled, + setup, + teardown, +}; diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js new file mode 100644 index 0000000..a7cc1bd --- /dev/null +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -0,0 +1,200 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import fs from "node:fs"; +import path from "path"; + +describe("Bash shell integration", () => { + let mockStartupFile; + let bash; + + beforeEach(async () => { + // Create temporary startup file for testing + mockStartupFile = path.join(tmpdir(), `test-bashrc-${Date.now()}`); + + // Mock the helpers module + mock.module("../helpers.js", { + namedExports: { + execAndGetOutput: () => mockStartupFile, + doesExecutableExistOnSystem: () => true, + addLineToFile: (filePath, line) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + fs.appendFileSync(filePath, line + "\n", "utf-8"); + }, + removeLinesMatchingPattern: (filePath, pattern) => { + 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)); + fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); + }, + }, + }); + + // Import bash module after mocking + bash = (await import("./bash.js")).default; + }); + + afterEach(() => { + // Clean up test files + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + // Reset mocks + mock.reset(); + }); + + describe("isInstalled", () => { + it("should return true when bash is installed", () => { + assert.strictEqual(bash.isInstalled(), true); + }); + + it("should call doesExecutableExistOnSystem with correct parameter", () => { + // Test that the method calls the helper with the right executable name + assert.strictEqual(bash.isInstalled(), true); + }); + }); + + describe("setup", () => { + it("should add aliases for all provided tools", () => { + const tools = ["npm", "npx", "yarn"]; + + const result = bash.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 = ["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); + + // File should be created during teardown call even if no tools are provided + if (fs.existsSync(mockStartupFile)) { + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.strictEqual(content.trim(), ""); + } + }); + }); + + describe("teardown", () => { + it("should remove npm, npx, and yarn aliases", () => { + const initialContent = [ + "#!/bin/bash", + "alias npm='aikido-npm'", + "alias npx='aikido-npx'", + "alias yarn='aikido-yarn'", + "alias ls='ls --color=auto'", + "alias grep='grep --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = bash.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("alias npx=")); + assert.ok(!content.includes("alias yarn=")); + assert.ok(content.includes("alias ls=")); + assert.ok(content.includes("alias grep=")); + }); + + it("should handle file that doesn't exist", () => { + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + const result = bash.teardown(); + assert.strictEqual(result, true); + }); + + it("should handle file with no relevant aliases", () => { + const initialContent = [ + "#!/bin/bash", + "alias ls='ls --color=auto'", + "export PATH=$PATH:~/bin", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = bash.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("alias ls=")); + assert.ok(content.includes("export PATH=")); + }); + }); + + describe("shell properties", () => { + it("should have correct name", () => { + assert.strictEqual(bash.name, "Bash"); + }); + + it("should expose all required methods", () => { + assert.ok(typeof bash.isInstalled === "function"); + assert.ok(typeof bash.setup === "function"); + assert.ok(typeof bash.teardown === "function"); + assert.ok(typeof bash.name === "string"); + }); + }); + + describe("integration tests", () => { + it("should handle complete setup and teardown cycle", () => { + const tools = ["npm", "yarn"]; + + // Setup + bash.setup(tools); + let content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes('alias npm="aikido-npm"')); + assert.ok(content.includes('alias yarn="aikido-yarn"')); + + // Teardown + bash.teardown(); + content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("alias yarn=")); + }); + + it("should handle multiple setup calls", () => { + const tools = ["npm"]; + + bash.setup(tools); + bash.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"); + }); + }); +}); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js new file mode 100644 index 0000000..b8c3bb3 --- /dev/null +++ b/src/shell-integration/supported-shells/fish.js @@ -0,0 +1,45 @@ +import { + addLineToFile, + doesExecutableExistOnSystem, + execAndGetOutput, + removeLinesMatchingPattern, +} from "../helpers.js"; + +const shellName = "Fish"; +const executableName = "fish"; +const startupFileCommand = "echo ~/.config/fish/config.fish"; + +function isInstalled() { + return doesExecutableExistOnSystem(executableName); +} + +function teardown() { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + + // 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+/); + + return true; +} + +function setup(tools) { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + teardown(); + + for (const tool of tools) { + addLineToFile( + startupFile, + `alias ${tool} "aikido-${tool}" # Safe-chain alias for ${tool}` + ); + } + + return true; +} + +export default { + name: shellName, + isInstalled, + setup, + teardown, +}; diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js new file mode 100644 index 0000000..af4834d --- /dev/null +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -0,0 +1,190 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import fs from "node:fs"; +import path from "path"; + +describe("Fish shell integration", () => { + let mockStartupFile; + let fish; + + 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: { + execAndGetOutput: () => mockStartupFile, + doesExecutableExistOnSystem: () => true, + addLineToFile: (filePath, line) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + fs.appendFileSync(filePath, line + "\n", "utf-8"); + }, + removeLinesMatchingPattern: (filePath, pattern) => { + 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)); + fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); + } + } + }); + + // Import fish module after mocking + fish = (await import("./fish.js")).default; + }); + + afterEach(() => { + // Clean up test files + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + // Reset mocks + mock.reset(); + }); + + describe("isInstalled", () => { + it("should return true when fish is installed", () => { + assert.strictEqual(fish.isInstalled(), true); + }); + + it("should call doesExecutableExistOnSystem with correct parameter", () => { + // Test that the method calls the helper with the right executable name + assert.strictEqual(fish.isInstalled(), true); + }); + }); + + describe("setup", () => { + it("should add aliases for all provided tools", () => { + const tools = ["npm", "npx", "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 = ["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"')); + }); + + 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"); + assert.strictEqual(content.trim(), ""); + } + }); + }); + + describe("teardown", () => { + it("should remove npm, npx, and yarn aliases", () => { + const initialContent = [ + "#!/usr/bin/env fish", + "alias npm 'aikido-npm'", + "alias npx 'aikido-npx'", + "alias yarn 'aikido-yarn'", + "alias ls 'ls --color=auto'", + "alias grep 'grep --color=auto'" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = fish.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm ")); + assert.ok(!content.includes("alias npx ")); + assert.ok(!content.includes("alias yarn ")); + assert.ok(content.includes("alias ls ")); + assert.ok(content.includes("alias grep ")); + }); + + it("should handle file that doesn't exist", () => { + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + const result = fish.teardown(); + assert.strictEqual(result, true); + }); + + it("should handle file with no relevant aliases", () => { + const initialContent = [ + "#!/usr/bin/env fish", + "alias ls 'ls --color=auto'", + "set PATH $PATH ~/bin" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = fish.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("alias ls ")); + assert.ok(content.includes("set PATH ")); + }); + }); + + describe("shell properties", () => { + it("should have correct name", () => { + assert.strictEqual(fish.name, "Fish"); + }); + + it("should expose all required methods", () => { + assert.ok(typeof fish.isInstalled === "function"); + assert.ok(typeof fish.setup === "function"); + assert.ok(typeof fish.teardown === "function"); + assert.ok(typeof fish.name === "string"); + }); + }); + + describe("integration tests", () => { + it("should handle complete setup and teardown cycle", () => { + const tools = ["npm", "yarn"]; + + // Setup + fish.setup(tools); + let content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes('alias npm "aikido-npm"')); + assert.ok(content.includes('alias yarn "aikido-yarn"')); + + // Teardown + fish.teardown(); + content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm ")); + assert.ok(!content.includes("alias yarn ")); + }); + + it("should handle multiple setup calls", () => { + const tools = ["npm"]; + + fish.setup(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 new file mode 100644 index 0000000..f07efbf --- /dev/null +++ b/src/shell-integration/supported-shells/powershell.js @@ -0,0 +1,45 @@ +import { + addLineToFile, + doesExecutableExistOnSystem, + execAndGetOutput, + removeLinesMatchingPattern, +} from "../helpers.js"; + +const shellName = "PowerShell Core"; +const executableName = "pwsh"; +const startupFileCommand = "echo $PROFILE"; + +function isInstalled() { + return doesExecutableExistOnSystem(executableName); +} + +function teardown() { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + + // 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+/); + + return true; +} + +function setup(tools) { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + teardown(); + + for (const { tool, aikidoCommand } of tools) { + addLineToFile( + startupFile, + `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` + ); + } + + return true; +} + +export default { + name: shellName, + isInstalled, + setup, + teardown, +}; diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js new file mode 100644 index 0000000..9d71d94 --- /dev/null +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -0,0 +1,197 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import fs from "node:fs"; +import path from "path"; + +describe("PowerShell Core shell integration", () => { + let mockStartupFile; + let powershell; + + beforeEach(async () => { + // Create temporary startup file for testing + mockStartupFile = path.join(tmpdir(), `test-powershell-profile-${Date.now()}.ps1`); + + // Mock the helpers module + mock.module("../helpers.js", { + namedExports: { + execAndGetOutput: () => mockStartupFile, + doesExecutableExistOnSystem: () => true, + addLineToFile: (filePath, line) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + fs.appendFileSync(filePath, line + "\n", "utf-8"); + }, + removeLinesMatchingPattern: (filePath, pattern) => { + 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)); + fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); + } + } + }); + + // Import powershell module after mocking + powershell = (await import("./powershell.js")).default; + }); + + afterEach(() => { + // Clean up test files + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + // Reset mocks + mock.reset(); + }); + + describe("isInstalled", () => { + it("should return true when powershell is installed", () => { + assert.strictEqual(powershell.isInstalled(), true); + }); + + it("should call doesExecutableExistOnSystem with correct parameter", () => { + // Test that the method calls the helper with the right executable name + assert.strictEqual(powershell.isInstalled(), true); + }); + }); + + describe("setup", () => { + it("should add aliases for all provided tools", () => { + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "npx", aikidoCommand: "aikido-npx" }, + { 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')); + }); + + 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"); + assert.strictEqual(content.trim(), ""); + } + }); + }); + + describe("teardown", () => { + it("should remove npm, npx, and yarn aliases", () => { + const initialContent = [ + "# PowerShell profile", + "Set-Alias npm aikido-npm", + "Set-Alias npx aikido-npx", + "Set-Alias yarn aikido-yarn", + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = powershell.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("Set-Alias npm ")); + assert.ok(!content.includes("Set-Alias npx ")); + assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should handle file that doesn't exist", () => { + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + const result = powershell.teardown(); + assert.strictEqual(result, true); + }); + + it("should handle file with no relevant aliases", () => { + const initialContent = [ + "# PowerShell profile", + "Set-Alias ls Get-ChildItem", + "$env:PATH += ';C:\\Tools'" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = powershell.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("$env:PATH ")); + }); + }); + + describe("shell properties", () => { + it("should have correct name", () => { + assert.strictEqual(powershell.name, "PowerShell Core"); + }); + + it("should expose all required methods", () => { + assert.ok(typeof powershell.isInstalled === "function"); + assert.ok(typeof powershell.setup === "function"); + assert.ok(typeof powershell.teardown === "function"); + assert.ok(typeof powershell.name === "string"); + }); + }); + + describe("integration tests", () => { + it("should handle complete setup and teardown cycle", () => { + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { 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')); + + // Teardown + powershell.teardown(); + content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("Set-Alias npm ")); + assert.ok(!content.includes("Set-Alias yarn ")); + }); + + it("should handle multiple setup calls", () => { + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; + + powershell.setup(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"); + }); + }); +}); \ No newline at end of file diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js new file mode 100644 index 0000000..381b987 --- /dev/null +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -0,0 +1,45 @@ +import { + addLineToFile, + doesExecutableExistOnSystem, + execAndGetOutput, + removeLinesMatchingPattern, +} from "../helpers.js"; + +const shellName = "Windows PowerShell"; +const executableName = "powershell"; +const startupFileCommand = "echo $PROFILE"; + +function isInstalled() { + return doesExecutableExistOnSystem(executableName); +} + +function teardown() { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + + // 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+/); + + return true; +} + +function setup(tools) { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + teardown(); + + for (const { tool, aikidoCommand } of tools) { + addLineToFile( + startupFile, + `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` + ); + } + + return true; +} + +export default { + name: shellName, + isInstalled, + setup, + teardown, +}; diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js new file mode 100644 index 0000000..fe8b64f --- /dev/null +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -0,0 +1,197 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import fs from "node:fs"; +import path from "path"; + +describe("Windows PowerShell shell integration", () => { + let mockStartupFile; + let windowsPowershell; + + beforeEach(async () => { + // Create temporary startup file for testing + mockStartupFile = path.join(tmpdir(), `test-windows-powershell-profile-${Date.now()}.ps1`); + + // Mock the helpers module + mock.module("../helpers.js", { + namedExports: { + execAndGetOutput: () => mockStartupFile, + doesExecutableExistOnSystem: () => true, + addLineToFile: (filePath, line) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + fs.appendFileSync(filePath, line + "\n", "utf-8"); + }, + removeLinesMatchingPattern: (filePath, pattern) => { + 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)); + fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); + } + } + }); + + // Import windowsPowershell module after mocking + windowsPowershell = (await import("./windowsPowershell.js")).default; + }); + + afterEach(() => { + // Clean up test files + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + // Reset mocks + mock.reset(); + }); + + describe("isInstalled", () => { + it("should return true when windows powershell is installed", () => { + assert.strictEqual(windowsPowershell.isInstalled(), true); + }); + + it("should call doesExecutableExistOnSystem with correct parameter", () => { + // Test that the method calls the helper with the right executable name + assert.strictEqual(windowsPowershell.isInstalled(), true); + }); + }); + + describe("setup", () => { + it("should add aliases for all provided tools", () => { + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "npx", aikidoCommand: "aikido-npx" }, + { 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')); + }); + + 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"); + assert.strictEqual(content.trim(), ""); + } + }); + }); + + describe("teardown", () => { + it("should remove npm, npx, and yarn aliases", () => { + const initialContent = [ + "# Windows PowerShell profile", + "Set-Alias npm aikido-npm", + "Set-Alias npx aikido-npx", + "Set-Alias yarn aikido-yarn", + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = windowsPowershell.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("Set-Alias npm ")); + assert.ok(!content.includes("Set-Alias npx ")); + assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should handle file that doesn't exist", () => { + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + const result = windowsPowershell.teardown(); + assert.strictEqual(result, true); + }); + + it("should handle file with no relevant aliases", () => { + const initialContent = [ + "# Windows PowerShell profile", + "Set-Alias ls Get-ChildItem", + "$env:PATH += ';C:\\Tools'" + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = windowsPowershell.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("$env:PATH ")); + }); + }); + + describe("shell properties", () => { + it("should have correct name", () => { + assert.strictEqual(windowsPowershell.name, "Windows PowerShell"); + }); + + it("should expose all required methods", () => { + assert.ok(typeof windowsPowershell.isInstalled === "function"); + assert.ok(typeof windowsPowershell.setup === "function"); + assert.ok(typeof windowsPowershell.teardown === "function"); + assert.ok(typeof windowsPowershell.name === "string"); + }); + }); + + describe("integration tests", () => { + it("should handle complete setup and teardown cycle", () => { + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { 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')); + + // Teardown + windowsPowershell.teardown(); + content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("Set-Alias npm ")); + assert.ok(!content.includes("Set-Alias yarn ")); + }); + + it("should handle multiple setup calls", () => { + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; + + windowsPowershell.setup(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 new file mode 100644 index 0000000..9943634 --- /dev/null +++ b/src/shell-integration/supported-shells/zsh.js @@ -0,0 +1,45 @@ +import { + addLineToFile, + doesExecutableExistOnSystem, + execAndGetOutput, + removeLinesMatchingPattern, +} from "../helpers.js"; + +const shellName = "Zsh"; +const executableName = "zsh"; +const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc"; + +function isInstalled() { + return doesExecutableExistOnSystem(executableName); +} + +function teardown() { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + + // 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)=/); + + return true; +} + +function setup(tools) { + const startupFile = execAndGetOutput(startupFileCommand, executableName); + teardown(); + + for (const tool of tools) { + addLineToFile( + startupFile, + `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` + ); + } + + return true; +} + +export default { + name: shellName, + isInstalled, + setup, + teardown, +}; diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js new file mode 100644 index 0000000..1e2f0bd --- /dev/null +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -0,0 +1,200 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import fs from "node:fs"; +import path from "path"; + +describe("Zsh shell integration", () => { + let mockStartupFile; + let zsh; + + beforeEach(async () => { + // Create temporary startup file for testing + mockStartupFile = path.join(tmpdir(), `test-zshrc-${Date.now()}`); + + // Mock the helpers module + mock.module("../helpers.js", { + namedExports: { + execAndGetOutput: () => mockStartupFile, + doesExecutableExistOnSystem: () => true, + addLineToFile: (filePath, line) => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + fs.appendFileSync(filePath, line + "\n", "utf-8"); + }, + removeLinesMatchingPattern: (filePath, pattern) => { + 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)); + fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); + }, + }, + }); + + // Import zsh module after mocking + zsh = (await import("./zsh.js")).default; + }); + + afterEach(() => { + // Clean up test files + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + // Reset mocks + mock.reset(); + }); + + describe("isInstalled", () => { + it("should return true when zsh is installed", () => { + assert.strictEqual(zsh.isInstalled(), true); + }); + + it("should call doesExecutableExistOnSystem with correct parameter", () => { + // Test that the method calls the helper with the right executable name + assert.strictEqual(zsh.isInstalled(), true); + }); + }); + + describe("setup", () => { + it("should add aliases for all provided tools", () => { + const tools = ["npm", "npx", "yarn"]; + + const result = zsh.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 = ["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); + + // File should be created during teardown call even if no tools are provided + if (fs.existsSync(mockStartupFile)) { + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.strictEqual(content.trim(), ""); + } + }); + }); + + describe("teardown", () => { + it("should remove npm, npx, and yarn aliases", () => { + const initialContent = [ + "#!/bin/zsh", + "alias npm='aikido-npm'", + "alias npx='aikido-npx'", + "alias yarn='aikido-yarn'", + "alias ls='ls --color=auto'", + "alias grep='grep --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = zsh.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("alias npx=")); + assert.ok(!content.includes("alias yarn=")); + assert.ok(content.includes("alias ls=")); + assert.ok(content.includes("alias grep=")); + }); + + it("should handle file that doesn't exist", () => { + if (fs.existsSync(mockStartupFile)) { + fs.unlinkSync(mockStartupFile); + } + + const result = zsh.teardown(); + assert.strictEqual(result, true); + }); + + it("should handle file with no relevant aliases", () => { + const initialContent = [ + "#!/bin/zsh", + "alias ls='ls --color=auto'", + "export PATH=$PATH:~/bin", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = zsh.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("alias ls=")); + assert.ok(content.includes("export PATH=")); + }); + }); + + describe("shell properties", () => { + it("should have correct name", () => { + assert.strictEqual(zsh.name, "Zsh"); + }); + + it("should expose all required methods", () => { + assert.ok(typeof zsh.isInstalled === "function"); + assert.ok(typeof zsh.setup === "function"); + assert.ok(typeof zsh.teardown === "function"); + assert.ok(typeof zsh.name === "string"); + }); + }); + + describe("integration tests", () => { + it("should handle complete setup and teardown cycle", () => { + const tools = ["npm", "yarn"]; + + // Setup + zsh.setup(tools); + let content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes('alias npm="aikido-npm"')); + assert.ok(content.includes('alias yarn="aikido-yarn"')); + + // Teardown + zsh.teardown(); + content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("alias yarn=")); + }); + + it("should handle multiple setup calls", () => { + const tools = ["npm"]; + + zsh.setup(tools); + zsh.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"); + }); + }); +}); diff --git a/src/shell-integration/teardown.js b/src/shell-integration/teardown.js index ecbee87..00a933f 100644 --- a/src/shell-integration/teardown.js +++ b/src/shell-integration/teardown.js @@ -1,9 +1,6 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { getAliases } from "./helpers.js"; -import fs from "fs"; -import { EOL } from "os"; export async function teardown() { ui.writeInformation( @@ -27,8 +24,26 @@ export async function teardown() { let updatedCount = 0; for (const shell of shells) { - if (removeAliasesForShell(shell)) { + let success = false; + try { + success = shell.teardown(); + } catch { + success = false; + } + + if (success) { + ui.writeInformation( + `${chalk.bold("- " + shell.name + ":")} ${chalk.green( + "Teardown successful" + )}` + ); updatedCount++; + } else { + ui.writeError( + `${chalk.bold("- " + shell.name + ":")} ${chalk.red( + "Teardown failed" + )}. Please check your ${shell.name} configuration.` + ); } } @@ -43,98 +58,3 @@ export async function teardown() { return; } } - -/** - * This function removes aliases for the given shell. - * It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.), - * and then removes the aliases for npm, npx, and yarn commands. - * If the aliases don't exist, it will report that they were not found. - * If the startup file does not exist, it will report that no aliases need to be removed. - * - * The shell startup script is loaded by the respective shell when it starts. - * This means that the aliases will be removed from the shell after it is restarted. - */ -function removeAliasesForShell(shell) { - if (!shell.startupFile) { - ui.writeError( - `- ${chalk.bold( - shell.name - )}: no startup file found. Cannot remove aliases.` - ); - return false; - } - - if (!fs.existsSync(shell.startupFile)) { - ui.writeInformation( - `- ${chalk.bold( - shell.name - )}: startup file does not exist. No aliases to remove.` - ); - return false; - } - - const aliases = getAliases(shell.startupFile); - const fileContent = fs.readFileSync(shell.startupFile, "utf-8"); - const { removedCount, notFoundCount } = removeAliasesFromFile( - aliases, - fileContent, - shell.startupFile - ); - - let summary = "- " + chalk.bold(shell.name) + ": "; - - if (removedCount > 0) { - summary += chalk.green(`${removedCount} aliases were removed`); - } - if (notFoundCount > 0) { - if (removedCount > 0) { - summary += ", "; - } - summary += chalk.yellow(`${notFoundCount} aliases were not found`); - } - if (removedCount === 0 && notFoundCount === 0) { - summary += chalk.yellow("no aliases found to remove"); - } - - ui.writeInformation(summary); - return removedCount > 0; -} - -/** - * This function removes the aliases from the startup file. - * It searches for exact matches of each alias line and removes them. - * eg: for bash it will remove 'alias npm="aikido-npm"' for npm from ~/.bashrc - * @returns an object with the counts of removed and not found aliases. - */ -export function removeAliasesFromFile(aliases, fileContent, startupFilePath) { - let removedCount = 0; - let notFoundCount = 0; - let updatedContent = fileContent; - - for (const alias of aliases) { - const lines = updatedContent.split(EOL); - let aliasLineIndex = lines.findIndex((line) => line.trim() === alias); - - if (aliasLineIndex !== -1) { - removedCount++; - - // Remove all occurrences of the alias line, in case it appears multiple times - while (aliasLineIndex !== -1) { - lines.splice(aliasLineIndex, 1); - aliasLineIndex = lines.findIndex((line) => line.trim() === alias); - } - updatedContent = lines.join(EOL); - } else { - notFoundCount++; - } - } - - if (removedCount > 0) { - fs.writeFileSync(startupFilePath, updatedContent, "utf-8"); - } - - return { - removedCount, - notFoundCount, - }; -} diff --git a/src/shell-integration/teardown.spec.js b/src/shell-integration/teardown.spec.js deleted file mode 100644 index 3f301c2..0000000 --- a/src/shell-integration/teardown.spec.js +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { EOL, tmpdir } from "node:os"; -import fs from "node:fs"; -import { getAliases } from "./helpers.js"; -import { removeAliasesFromFile } from "./teardown.js"; - -describe("teardown", () => { - 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.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`); - } - }); - - it(`should handle file with no aliases for ${shell}`, () => { - 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"); - - const updatedContent = readAndDeleteFile(filePath); - assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain unchanged"); - }); - - it(`should remove duplicate aliases from ${shell} file`, () => { - const lines = [ - `#!/usr/bin/env ${shell}`, - "", - ...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.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 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)"); - for (let i = 0; i < aliases.length; i++) { - assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`); - } - }); - - it(`should handle partial alias matches for ${shell}`, () => { - const lines = [ - `#!/usr/bin/env ${shell}`, - "", - 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"); - - 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"); - }); - }); - } - - // Test for each shell type using real getAliases() output - runRemovalTestsForEnvironment("bash", ".bashrc", [ - "alias npm='aikido-npm'", - "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" - ]); - - runRemovalTestsForEnvironment("zsh", ".zshrc", [ - "alias npm='aikido-npm'", - "alias npx='aikido-npx'", - "alias yarn='aikido-yarn'" - ]); - - runRemovalTestsForEnvironment("fish", ".fish", [ - 'alias npm "aikido-npm"', - 'alias npx "aikido-npx"', - 'alias yarn "aikido-yarn"' - ]); - - runRemovalTestsForEnvironment("pwsh", ".ps1", [ - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn" - ]); - - 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`; - 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"); - - // Cleanup - fs.rmSync(filePath, { force: true }); - }); - - 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`; - 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"); - - // Cleanup - fs.rmSync(filePath, { force: true }); - }); - }); -}); - -function createShellStartupScript(lines, fileExtension) { - const randomFileName = Math.random().toString(36).substring(2, 15); - const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`; - fs.writeFileSync(filePath, lines.join(EOL), "utf-8"); - return filePath; -} - -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 09300eade6a4c16d30d8409853910f489f5c1b5d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:40:09 +0200 Subject: [PATCH 08/57] Zsh: check if safe-chain is installed before running it. --- src/shell-integration/setup.js | 26 +++++ .../startup-scripts/init-zsh.sh | 81 ++++++++++++++++ src/shell-integration/supported-shells/zsh.js | 18 ++-- .../supported-shells/zsh.spec.js | 97 +++++++++++-------- 4 files changed, 177 insertions(+), 45 deletions(-) create mode 100644 src/shell-integration/startup-scripts/init-zsh.sh diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 85d319b..ce6a638 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -2,6 +2,10 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; import { knownAikidoTools } from "./helpers.js"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; /** * Loops over the detected shells and calls the setup function for each. @@ -13,6 +17,8 @@ export async function setup() { ); ui.emptyLine(); + copyStartupFiles(); + try { const shells = detectShells(); if (shells.length === 0) { @@ -72,3 +78,23 @@ function setupShell(shell) { return success; } + +function copyStartupFiles() { + const startupFiles = ["init-zsh.sh"]; + + for (const file of startupFiles) { + const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); + + // Create target directory if it doesn't exist + const targetDir = targetPath.substring(0, targetPath.lastIndexOf("/")); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Use absolute path for source + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const sourcePath = path.resolve(__dirname, "startup-scripts", file); + fs.copyFileSync(sourcePath, targetPath); + } +} diff --git a/src/shell-integration/startup-scripts/init-zsh.sh b/src/shell-integration/startup-scripts/init-zsh.sh new file mode 100644 index 0000000..b936eeb --- /dev/null +++ b/src/shell-integration/startup-scripts/init-zsh.sh @@ -0,0 +1,81 @@ + +function installIfCommandNotFound() { + local cmd="$1" + + # Check if the command already exists + if command -v "$cmd" > /dev/null 2>&1; then + return 0 + fi + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + local node_version=$(node -v | sed 's/v//' | cut -d'.' -f1) + if [ "$node_version" -lt 18 ]; then + return 2 + fi + + # Command not found, ask user if they want to install safe-chain + printf "The command '%s' is not available. Do you want to install safe-chain to provide it? (y/N): " "$cmd" + read -r response + + if [[ "$response" =~ ^[Yy]$ ]]; then + printf "Installing safe-chain...\n" + installSafeChain + + if [ $? -ne 0 ]; then + printf "\nFailed to install safe-chain. Exiting.\n" + return 1 + fi + + return 0 + else + printf "Skipping safe-chain installation. Using original command instead.\n" + return 2 + fi +} + +function installSafeChain() { + command npm install -g @aikidosec/safe-chain + + if [ $? -ne 0 ]; then + return 1 + fi + + printf "------\n" +} + +function wrapCommand() { + local original_cmd="$1" + local aikido_cmd="$2" + + # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ + # so that "$@" now contains only the arguments passed to the original command + shift 2 + + installIfCommandNotFound "$aikido_cmd" + local install_result=$? + if [ $install_result -eq 2 ]; then + command "$original_cmd" "$@" + else + "$aikido_cmd" "$@" + fi +} + +function npx() { + wrapCommand "npx" "aikido-npx" "$@" +} + +function yarn() { + wrapCommand "yarn" "aikido-yarn" "$@" +} + +function npm() { + if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + command npm "$@" + return + fi + + wrapCommand "npm" "aikido-npm" "$@" +} diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index 9943634..d9f63b0 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -20,19 +20,23 @@ function teardown() { // This will remove the safe-chain aliases for npm, npx, and yarn commands. removeLinesMatchingPattern(startupFile, /^alias\s+(npm|npx|yarn)=/); + // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-zsh.sh) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-zsh\.sh/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = execAndGetOutput(startupFileCommand, executableName); teardown(); - for (const tool of tools) { - addLineToFile( - startupFile, - `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-zsh.sh # Safe-chain Zsh initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index 1e2f0bd..327a914 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -59,49 +59,40 @@ describe("Zsh shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = ["npm", "npx", "yarn"]; - - const result = zsh.setup(tools); + it("should add source line for zsh initialization script", () => { + const result = zsh.setup(); 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') + content.includes( + "source ~/.safe-chain/scripts/init-zsh.sh # Safe-chain Zsh initialization script" + ) ); }); it("should call teardown before setup", () => { - // Pre-populate file with existing aliases + // Pre-populate file with existing source line fs.writeFileSync( mockStartupFile, - 'alias npm="old-npm"\nalias npx="old-npx"\n', + "source ~/.safe-chain/scripts/init-zsh.sh\n", "utf-8" ); - const tools = ["npm"]; - zsh.setup(tools); + zsh.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('alias npm="old-npm"')); - assert.ok(content.includes('alias npm="aikido-npm"')); + const sourceMatches = (content.match(/source.*init-zsh\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); - it("should handle empty tools array", () => { - const result = zsh.setup([]); + it("should handle empty startup file", () => { + const result = zsh.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"); - assert.strictEqual(content.trim(), ""); - } + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); }); }); @@ -129,6 +120,23 @@ describe("Zsh shell integration", () => { assert.ok(content.includes("alias grep=")); }); + it("should remove zsh initialization script source line", () => { + const initialContent = [ + "#!/bin/zsh", + "source ~/.safe-chain/scripts/init-zsh.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = zsh.teardown(); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("alias ls=")); + }); + it("should handle file that doesn't exist", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); @@ -138,7 +146,7 @@ describe("Zsh shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant aliases or source lines", () => { const initialContent = [ "#!/bin/zsh", "alias ls='ls --color=auto'", @@ -171,30 +179,43 @@ describe("Zsh shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = ["npm", "yarn"]; - // Setup - zsh.setup(tools); + zsh.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm="aikido-npm"')); - assert.ok(content.includes('alias yarn="aikido-yarn"')); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); // Teardown zsh.teardown(); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm=")); - assert.ok(!content.includes("alias yarn=")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); }); it("should handle multiple setup calls", () => { - const tools = ["npm"]; - - zsh.setup(tools); - zsh.setup(tools); + zsh.setup(); + zsh.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/alias npm="/g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/source.*init-zsh\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); + }); + + it("should handle mixed content with aliases and source lines", () => { + const initialContent = [ + "#!/bin/zsh", + "alias npm='old-npm'", + "source ~/.safe-chain/scripts/init-zsh.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + // Teardown should remove both aliases and source line + zsh.teardown(); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("alias ls=")); }); }); }); From 3825b94a0987219475d4ea68c9c0c71188227176 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:59:01 +0200 Subject: [PATCH 09/57] Fix command injection --- src/shell-integration/helpers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index eeae002..b47d3d6 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -1,4 +1,4 @@ -import { execSync } from "child_process"; +import { execSync, spawnSync } from "child_process"; import * as os from "os"; import fs from "fs"; @@ -13,9 +13,9 @@ export const knownAikidoTools = [ export function doesExecutableExistOnSystem(executableName) { try { if (os.platform() === "win32") { - execSync(`where ${executableName}`, { stdio: "ignore" }); + spawnSync("where", [executableName], { stdio: "ignore" }); } else { - execSync(`which ${executableName}`, { stdio: "ignore" }); + spawnSync("which", [executableName], { stdio: "ignore" }); } return true; } catch { @@ -46,6 +46,7 @@ export function addLineToFile(filePath, line) { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); } + const fileContent = fs.readFileSync(filePath, "utf-8"); const updatedContent = fileContent + os.EOL + line; fs.writeFileSync(filePath, updatedContent, "utf-8"); From 87bb095d4fbb1a4bc7f7ab9843d811ffc6877c1b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 17:03:27 +0200 Subject: [PATCH 10/57] Fixes the broken shell detection --- src/shell-integration/helpers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index b47d3d6..0295858 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -13,11 +13,12 @@ export const knownAikidoTools = [ export function doesExecutableExistOnSystem(executableName) { try { if (os.platform() === "win32") { - spawnSync("where", [executableName], { stdio: "ignore" }); + const result = spawnSync("where", [executableName], { stdio: "ignore" }); + return result.status === 0; } else { - spawnSync("which", [executableName], { stdio: "ignore" }); + const result = spawnSync("which", [executableName], { stdio: "ignore" }); + return result.status === 0; } - return true; } catch { return false; } From 9476927b877674370c78083a42db4cc152495b8e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 17:15:46 +0200 Subject: [PATCH 11/57] Fix tests --- src/shell-integration/supported-shells/bash.js | 4 ++-- .../supported-shells/bash.spec.js | 15 +++++++++++---- src/shell-integration/supported-shells/fish.js | 4 ++-- .../supported-shells/fish.spec.js | 15 +++++++++++---- src/shell-integration/supported-shells/zsh.js | 4 ++-- .../supported-shells/zsh.spec.js | 15 +++++++++++---- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js index b53d401..35d23c1 100644 --- a/src/shell-integration/supported-shells/bash.js +++ b/src/shell-integration/supported-shells/bash.js @@ -27,10 +27,10 @@ function setup(tools) { const startupFile = execAndGetOutput(startupFileCommand, executableName); teardown(); - for (const tool of tools) { + for (const { tool, aikidoCommand } of tools) { addLineToFile( startupFile, - `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` + `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}` ); } diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js index a7cc1bd..21be84e 100644 --- a/src/shell-integration/supported-shells/bash.spec.js +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -60,7 +60,11 @@ describe("Bash shell integration", () => { describe("setup", () => { it("should add aliases for all provided tools", () => { - const tools = ["npm", "npx", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "npx", aikidoCommand: "aikido-npx" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; const result = bash.setup(tools); assert.strictEqual(result, true); @@ -85,7 +89,7 @@ describe("Bash shell integration", () => { "utf-8" ); - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; bash.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -171,7 +175,10 @@ describe("Bash shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = ["npm", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; // Setup bash.setup(tools); @@ -187,7 +194,7 @@ describe("Bash shell integration", () => { }); it("should handle multiple setup calls", () => { - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; bash.setup(tools); bash.setup(tools); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index b8c3bb3..429d351 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -27,10 +27,10 @@ function setup(tools) { const startupFile = execAndGetOutput(startupFileCommand, executableName); teardown(); - for (const tool of tools) { + for (const { tool, aikidoCommand } of tools) { addLineToFile( startupFile, - `alias ${tool} "aikido-${tool}" # Safe-chain alias for ${tool}` + `alias ${tool} "${aikidoCommand}" # Safe-chain alias for ${tool}` ); } diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index af4834d..15344a3 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -60,7 +60,11 @@ describe("Fish shell integration", () => { describe("setup", () => { it("should add aliases for all provided tools", () => { - const tools = ["npm", "npx", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "npx", aikidoCommand: "aikido-npx" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; const result = fish.setup(tools); assert.strictEqual(result, true); @@ -75,7 +79,7 @@ describe("Fish shell integration", () => { // Pre-populate file with existing aliases fs.writeFileSync(mockStartupFile, 'alias npm "old-npm"\nalias npx "old-npx"\n', "utf-8"); - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; fish.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -161,7 +165,10 @@ describe("Fish shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = ["npm", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; // Setup fish.setup(tools); @@ -177,7 +184,7 @@ describe("Fish shell integration", () => { }); it("should handle multiple setup calls", () => { - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; fish.setup(tools); fish.setup(tools); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index 9943634..d0b179b 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -27,10 +27,10 @@ function setup(tools) { const startupFile = execAndGetOutput(startupFileCommand, executableName); teardown(); - for (const tool of tools) { + for (const { tool, aikidoCommand } of tools) { addLineToFile( startupFile, - `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` + `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}` ); } diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index 1e2f0bd..37cc20f 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -60,7 +60,11 @@ describe("Zsh shell integration", () => { describe("setup", () => { it("should add aliases for all provided tools", () => { - const tools = ["npm", "npx", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "npx", aikidoCommand: "aikido-npx" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; const result = zsh.setup(tools); assert.strictEqual(result, true); @@ -85,7 +89,7 @@ describe("Zsh shell integration", () => { "utf-8" ); - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; zsh.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -171,7 +175,10 @@ describe("Zsh shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = ["npm", "yarn"]; + const tools = [ + { tool: "npm", aikidoCommand: "aikido-npm" }, + { tool: "yarn", aikidoCommand: "aikido-yarn" } + ]; // Setup zsh.setup(tools); @@ -187,7 +194,7 @@ describe("Zsh shell integration", () => { }); it("should handle multiple setup calls", () => { - const tools = ["npm"]; + const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; zsh.setup(tools); zsh.setup(tools); From 41bf3252d925d2e5d521bb8916dd2b661b60e852 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 11:15:21 +0200 Subject: [PATCH 12/57] 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 13/57] 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 14/57] 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 15/57] 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 16/57] 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; } From adc8ffedb6d165fcad336d3a279e04533c63b832 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 15:17:19 +0200 Subject: [PATCH 17/57] Fix tests --- package.json | 3 ++- src/shell-integration/supported-shells/zsh.spec.js | 4 ++-- 2 files changed, 4 insertions(+), 3 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/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index fbe8a96..4b5a0f1 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -120,7 +120,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"); @@ -210,7 +210,7 @@ describe("Zsh shell integration", () => { fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); // Teardown should remove both aliases and source line - zsh.teardown(); + zsh.teardown(knownAikidoTools); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); From 4424ba2e5b2b9eda8b9cef2edf89dcf18f5c451a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 15:18:59 +0200 Subject: [PATCH 18/57] Fix 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 24d4862dfdfd552a5d3e79d088daee90e558aba4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 15:55:58 +0200 Subject: [PATCH 19/57] Fix broken shell integration with nvm for Zsh and Bash --- src/shell-integration/setup.js | 2 +- .../{init-zsh.sh => init-posix.sh} | 0 .../supported-shells/bash.js | 18 ++++-- .../supported-shells/bash.spec.js | 64 +++++++++---------- src/shell-integration/supported-shells/zsh.js | 8 +-- .../supported-shells/zsh.spec.js | 24 ++++--- 6 files changed, 63 insertions(+), 53 deletions(-) rename src/shell-integration/startup-scripts/{init-zsh.sh => init-posix.sh} (100%) diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 3f9bbb3..999f3d2 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -81,7 +81,7 @@ function setupShell(shell) { } function copyStartupFiles() { - const startupFiles = ["init-zsh.sh"]; + const startupFiles = ["init-posix.sh"]; for (const file of startupFiles) { const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); diff --git a/src/shell-integration/startup-scripts/init-zsh.sh b/src/shell-integration/startup-scripts/init-posix.sh similarity index 100% rename from src/shell-integration/startup-scripts/init-zsh.sh rename to src/shell-integration/startup-scripts/init-posix.sh diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js index 66b844d..3c4b1f9 100644 --- a/src/shell-integration/supported-shells/bash.js +++ b/src/shell-integration/supported-shells/bash.js @@ -21,18 +21,22 @@ function teardown(tools) { removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); } + // Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js index ce666e5..e23addb 100644 --- a/src/shell-integration/supported-shells/bash.spec.js +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -66,37 +66,16 @@ describe("Bash shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = bash.setup(tools); + it("should add source line for bash initialization script", () => { + const result = bash.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('alias npm="aikido-npm" # Safe-chain alias for npm') + content.includes( + "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" + ) ); - 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 = bash.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"); - assert.strictEqual(content.trim(), ""); - } }); }); @@ -174,14 +153,14 @@ describe("Bash shell integration", () => { // Setup bash.setup(tools); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm="aikido-npm"')); - assert.ok(content.includes('alias yarn="aikido-yarn"')); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); // Teardown bash.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm=")); - assert.ok(!content.includes("alias yarn=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); }); it("should handle multiple setup calls", () => { @@ -192,8 +171,29 @@ describe("Bash shell integration", () => { bash.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"); + const sourceMatches = (content.match(/source.*init-posix\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); + }); + + it("should handle mixed content with aliases and source lines", () => { + const initialContent = [ + "#!/bin/bash", + "alias npm='old-npm'", + "source ~/.safe-chain/scripts/init-posix.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + // Teardown should remove both aliases and source line + bash.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); + assert.ok(content.includes("alias ls=")); }); }); }); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index 82f03af..b5167fe 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -21,21 +21,21 @@ function teardown(tools) { removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); } - // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-zsh.sh) + // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh) removeLinesMatchingPattern( startupFile, - /^source\s+~\/\.safe-chain\/scripts\/init-zsh\.sh/ + /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/ ); return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); addLineToFile( startupFile, - `source ~/.safe-chain/scripts/init-zsh.sh # Safe-chain Zsh initialization script` + `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script` ); return true; diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index 4b5a0f1..95c12ac 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -73,7 +73,7 @@ describe("Zsh shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - "source ~/.safe-chain/scripts/init-zsh.sh # Safe-chain Zsh initialization script" + "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script" ) ); }); @@ -83,7 +83,7 @@ describe("Zsh shell integration", () => { assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); }); }); @@ -114,7 +114,7 @@ describe("Zsh shell integration", () => { it("should remove zsh initialization script source line", () => { const initialContent = [ "#!/bin/zsh", - "source ~/.safe-chain/scripts/init-zsh.sh", + "source ~/.safe-chain/scripts/init-posix.sh", "alias ls='ls --color=auto'", ].join("\n"); @@ -124,7 +124,9 @@ describe("Zsh shell integration", () => { assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); assert.ok(content.includes("alias ls=")); }); @@ -178,12 +180,14 @@ describe("Zsh shell integration", () => { // Setup zsh.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); // Teardown zsh.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); }); it("should handle multiple setup calls", () => { @@ -194,7 +198,7 @@ describe("Zsh shell integration", () => { zsh.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const sourceMatches = (content.match(/source.*init-zsh\.sh/g) || []) + const sourceMatches = (content.match(/source.*init-posix\.sh/g) || []) .length; assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); @@ -203,7 +207,7 @@ describe("Zsh shell integration", () => { const initialContent = [ "#!/bin/zsh", "alias npm='old-npm'", - "source ~/.safe-chain/scripts/init-zsh.sh", + "source ~/.safe-chain/scripts/init-posix.sh", "alias ls='ls --color=auto'", ].join("\n"); @@ -213,7 +217,9 @@ describe("Zsh shell integration", () => { zsh.teardown(knownAikidoTools); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); - assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); assert.ok(content.includes("alias ls=")); }); }); From eba1e9cc8eace78323f6ac489369c1859a5aa194 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 15:58:49 +0200 Subject: [PATCH 20/57] Add wrappers for pnpm and pnpx in the script --- src/shell-integration/startup-scripts/init-posix.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shell-integration/startup-scripts/init-posix.sh b/src/shell-integration/startup-scripts/init-posix.sh index b936eeb..09a5926 100644 --- a/src/shell-integration/startup-scripts/init-posix.sh +++ b/src/shell-integration/startup-scripts/init-posix.sh @@ -69,6 +69,14 @@ function yarn() { wrapCommand "yarn" "aikido-yarn" "$@" } +function pnpm() { + wrapCommand "pnpm" "aikido-pnpm" "$@" +} + +function pnpx() { + wrapCommand "pnpx" "aikido-pnpx" "$@" +} + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command From a9e96be2b41e6515e5d0227c5dd08707cf58fdfb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 22 Jul 2025 09:49:03 +0200 Subject: [PATCH 21/57] Use safe-chain-test in the docs instead of eslint-js --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33dcc26..40f67da 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: - 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 + npm install safe-chain-test ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. From 577b09bd39c3882da1e5b1fba499dc4f4bcb65e8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 23 Jul 2025 11:16:38 +0200 Subject: [PATCH 22/57] Use powershell functions to wrap npm, npx, yarn, pnpm and pnpx --- src/shell-integration/setup.js | 2 +- .../startup-scripts/init-pwsh.ps1 | 122 ++++++++++++++++++ .../supported-shells/powershell.js | 18 ++- .../supported-shells/powershell.spec.js | 87 ++++++------- .../supported-shells/windowsPowershell.js | 18 ++- .../windowsPowershell.spec.js | 87 ++++++------- 6 files changed, 217 insertions(+), 117 deletions(-) create mode 100644 src/shell-integration/startup-scripts/init-pwsh.ps1 diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 999f3d2..4cc2968 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -81,7 +81,7 @@ function setupShell(shell) { } function copyStartupFiles() { - const startupFiles = ["init-posix.sh"]; + const startupFiles = ["init-posix.sh", "init-pwsh.ps1"]; for (const file of startupFiles) { const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 new file mode 100644 index 0000000..58a19c9 --- /dev/null +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -0,0 +1,122 @@ +function Test-CommandAvailable { + param([string]$Command) + + try { + Get-Command $Command -ErrorAction Stop | Out-Null + return $true + } catch { + return $false + } +} + +function Invoke-RealCommand { + param( + [string]$Command, + [string[]]$Arguments + ) + + # Find the real executable to avoid calling our wrapped functions + $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 + if ($realCommand) { + & $realCommand.Source @Arguments + } else { + # Fallback: try to call the .cmd version directly + & "$Command.cmd" @Arguments + } +} + +function Install-IfCommandNotFound { + param([string]$Command) + + # Check if the command already exists + if (Test-CommandAvailable $Command) { + return 0 + } + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + try { + $nodeVersion = (node -v) -replace 'v', '' | ForEach-Object { $_.Split('.')[0] } + if ([int]$nodeVersion -lt 18) { + return 2 + } + } catch { + return 2 + } + + # Command not found, ask user if they want to install safe-chain + $response = Read-Host "The command '$Command' is not available. Do you want to install safe-chain to provide it? (y/N)" + + if ($response -match '^[Yy]$') { + Write-Host "Installing safe-chain..." + $installResult = Install-SafeChain + + if ($installResult -ne 0) { + Write-Host "`nFailed to install safe-chain. Exiting." + return 1 + } + + return 0 + } else { + Write-Host "Skipping safe-chain installation. Using original command instead." + return 2 + } +} + +function Install-SafeChain { + try { + Invoke-RealCommand "npm" @("install", "-g", "@aikidosec/safe-chain") | Out-Null + + if ($LASTEXITCODE -ne 0) { + return 1 + } + + Write-Host "------" + return 0 + } catch { + return 1 + } +} + +function Invoke-WrappedCommand { + param( + [string]$OriginalCmd, + [string]$AikidoCmd, + [string[]]$Arguments + ) + + $installResult = Install-IfCommandNotFound $AikidoCmd + + if ($installResult -eq 2) { + Invoke-RealCommand $OriginalCmd $Arguments + } else { + & $AikidoCmd @Arguments + } +} + +function npx { + Invoke-WrappedCommand "npx" "aikido-npx" $args +} + +function yarn { + Invoke-WrappedCommand "yarn" "aikido-yarn" $args +} + +function pnpm { + Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args +} + +function pnpx { + Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args +} + +function npm { + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) { + Invoke-RealCommand "npm" $args + return + } + + Invoke-WrappedCommand "npm" "aikido-npm" $args +} \ 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 4690bb6..47524c2 100644 --- a/src/shell-integration/supported-shells/powershell.js +++ b/src/shell-integration/supported-shells/powershell.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Remove the line that sources the safe-chain PowerShell initialization script + removeLinesMatchingPattern( + startupFile, + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 9afade7..57d5098 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -69,49 +69,43 @@ describe("PowerShell Core shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = powershell.setup(tools); + it("should add init-pwsh.ps1 source line", () => { + const result = powershell.setup(); 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") + content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") ); - 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"); - assert.strictEqual(content.trim(), ""); - } }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# PowerShell profile", - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn", + ". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script", + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = powershell.teardown(knownAikidoTools); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should remove old-style aliases from earlier versions", () => { + const initialContent = [ + "# PowerShell profile", + "Set-Alias npm aikido-npm # Safe-chain alias for npm", + "Set-Alias npx aikido-npx # Safe-chain alias for npx", + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn", "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -138,7 +132,7 @@ describe("PowerShell Core shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant content", () => { const initialContent = [ "# PowerShell profile", "Set-Alias ls Get-ChildItem", @@ -171,34 +165,25 @@ describe("PowerShell Core shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - // Setup - powershell.setup(tools); + powershell.setup(); 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(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); // Teardown - powershell.teardown(tools); + powershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("Set-Alias npm ")); - assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - powershell.setup(tools); - powershell.teardown(tools); - powershell.setup(tools); + powershell.setup(); + powershell.teardown(knownAikidoTools); + powershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/Set-Alias npm /g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/\. "\$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh\.ps1"/g) || []).length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); }); diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js index 118a0b9..03ff7f8 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.js +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Remove the line that sources the safe-chain PowerShell initialization script + removeLinesMatchingPattern( + startupFile, + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js index 85da9f1..96677a8 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -69,49 +69,43 @@ describe("Windows PowerShell shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = windowsPowershell.setup(tools); + it("should add init-pwsh.ps1 source line", () => { + const result = windowsPowershell.setup(); 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") + content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") ); - 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"); - assert.strictEqual(content.trim(), ""); - } }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# Windows PowerShell profile", - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn", + ". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script", + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = windowsPowershell.teardown(knownAikidoTools); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should remove old-style aliases from earlier versions", () => { + const initialContent = [ + "# Windows PowerShell profile", + "Set-Alias npm aikido-npm # Safe-chain alias for npm", + "Set-Alias npx aikido-npx # Safe-chain alias for npx", + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn", "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -138,7 +132,7 @@ describe("Windows PowerShell shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant content", () => { const initialContent = [ "# Windows PowerShell profile", "Set-Alias ls Get-ChildItem", @@ -171,34 +165,25 @@ describe("Windows PowerShell shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - // Setup - windowsPowershell.setup(tools); + windowsPowershell.setup(); 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(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); // Teardown - windowsPowershell.teardown(tools); + windowsPowershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("Set-Alias npm ")); - assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - windowsPowershell.setup(tools); - windowsPowershell.teardown(tools); - windowsPowershell.setup(tools); + windowsPowershell.setup(); + windowsPowershell.teardown(knownAikidoTools); + windowsPowershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/Set-Alias npm /g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/\. "\$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh\.ps1"/g) || []).length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); }); From 248bb9f7b6cc0d2e74de6326da1a4388295c71a8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 23 Jul 2025 11:19:53 +0200 Subject: [PATCH 23/57] Take environment path separators into account when getting the target dir --- src/shell-integration/setup.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 999f3d2..9339ef5 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -84,10 +84,9 @@ function copyStartupFiles() { const startupFiles = ["init-posix.sh"]; for (const file of startupFiles) { + const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); - // Create target directory if it doesn't exist - const targetDir = targetPath.substring(0, targetPath.lastIndexOf("/")); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } From bc690c2aa653dba2f9ff634d208c9bad395b8eff Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 23 Jul 2025 11:20:36 +0200 Subject: [PATCH 24/57] Add CONTRIBUTING --- .github/CONTRIBUTING | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/CONTRIBUTING diff --git a/.github/CONTRIBUTING b/.github/CONTRIBUTING new file mode 100644 index 0000000..b7d2a37 --- /dev/null +++ b/.github/CONTRIBUTING @@ -0,0 +1,45 @@ +# Contributing to Aikido Safe Chain + +Thank you for your interest in contributing to the Aikido Safe Chain! Before you start, please take a moment to read through our guidelines. + +## Reporting Issues + +If you encounter any issues while using the Aikido Safe Chain, please report them on our [GitHub Issues page](https://github.com/AikidoSec/safe-chain). + +Before creating a new issue, please check if the issue has already been reported or addressed in the documentation. If you find a similar issue, feel free to add any additional information that might help us resolve it. + +Please try to create bug reports that are: + +- **Reproducible**: Include steps to reproduce the issue. +- **Clear**: Describe the issue in detail, including what you expected to happen and what + actually happened. +- **Environment**: Specify your environment, including the version of Node.js, the package manager you are using and its version, and your operating system. +- **Logs**: Include any relevant logs or error messages. +- **Scoped**: Please report a single issue per report. + +## Contributing Code + +To avoid your contribution is rejected, please engage with the community and maintainers before starting work on a new feature or significant change. You can do this by opening an issue to discuss your ideas or by discussing your ideas in an existing issue. This helps to avoid to accept contributions and avoids duplication of work. + +If you would like to contribute code to the Aikido Safe Chain, please follow these steps: + +1. **Fork the repository**: Create a fork of the Aikido Safe Chain repository on GitHub. +2. **Clone your fork**: Clone your forked repository to your local machine. +3. **Create a new branch**: Create a new branch for your changes. Use a descriptive name for the branch that reflects the changes you are making. +4. **Make your changes**: Implement your changes in the new branch. Make sure to follow the coding style and conventions used in the project. +5. **Test your changes**: Please ensure that your changes are covered by tests and that all tests pass. +6. **Commit your changes**: Commit your changes with a clear and descriptive commit message. +7. **Push your changes**: Push your changes to your forked repository. +8. **Create a pull request**: Open a pull request against the main repository. Provide a clear description of your changes and reference any related issues. + +### Development Environment + +To set up your development environment, you can follow these steps: + +1. **Install Node.js**: Make sure you have Node.js version 18 or above installed on your machine. +2. **Install dependencies**: Run `npm install` in the root directory of the project to install the necessary dependencies. +3. **Run tests**: You can run the tests using `npm test` to ensure that everything is working correctly. + +### Code Style + +Our style guides are documented in the .editorconfig file in the root of the repository. Use Prettier to format your code according to these guidelines. You can run Prettier manually or set it up to run automatically on save in your code editor. From ca5d3ecb2a2b62266e30463deaf03dd65c329b56 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 23 Jul 2025 11:36:45 +0200 Subject: [PATCH 25/57] Use functions to wrap package managers and detect if the aikido commands are available --- src/shell-integration/setup.js | 2 +- .../startup-scripts/init-fish.fish | 85 +++++++++++++++++++ .../supported-shells/fish.js | 18 ++-- .../supported-shells/fish.spec.js | 58 +++++-------- 4 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 src/shell-integration/startup-scripts/init-fish.fish diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index ada658b..792aa3c 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -81,7 +81,7 @@ function setupShell(shell) { } function copyStartupFiles() { - const startupFiles = ["init-posix.sh", "init-pwsh.ps1"]; + const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; for (const file of startupFiles) { const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish new file mode 100644 index 0000000..3a6c18e --- /dev/null +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -0,0 +1,85 @@ +function installIfCommandNotFound + set cmd $argv[1] + + # Check if the command already exists + if type -q $cmd + return 0 + end + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + set node_version (node -v | sed 's/v//' | cut -d'.' -f1) + if test $node_version -lt 18 + return 2 + end + + # Command not found, ask user if they want to install safe-chain + read -l response -P "The command '$cmd' is not available. Do you want to install safe-chain to provide it? (y/N): " + + if string match -qi 'y*' $response + printf "Installing safe-chain...\n" + installSafeChain + + if test $status -ne 0 + printf "\nFailed to install safe-chain. Exiting.\n" + return 1 + end + + return 0 + else + printf "Skipping safe-chain installation. Using original command instead.\n" + return 2 + end +end + +function installSafeChain + command npm install -g @aikidosec/safe-chain + + if test $status -ne 0 + return 1 + end + + printf "------\n" +end + +function wrapCommand + set original_cmd $argv[1] + set aikido_cmd $argv[2] + set cmd_args $argv[3..-1] + + installIfCommandNotFound $aikido_cmd + set install_result $status + + if test $install_result -eq 2 + command $original_cmd $cmd_args + else + $aikido_cmd $cmd_args + end +end + +function npx + wrapCommand "npx" "aikido-npx" $argv +end + +function yarn + wrapCommand "yarn" "aikido-yarn" $argv +end + +function pnpm + wrapCommand "pnpm" "aikido-pnpm" $argv +end + +function pnpx + wrapCommand "pnpx" "aikido-pnpx" $argv +end + +function npm + if test (count $argv) -eq 1 -a \( "$argv[1]" = "-v" -o "$argv[1]" = "--version" \) + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + command npm $argv + return + end + + wrapCommand "npm" "aikido-npm" $argv +end \ No newline at end of file diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index fc6fc85..7b2c683 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool} "${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index 5f1ab64..e138957 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -66,47 +66,34 @@ describe("Fish shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = fish.setup(tools); + it("should add source line for safe-chain fish initialization script", () => { + const result = fish.setup(); 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') + content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script') ); }); - it("should handle empty tools array", () => { - const result = fish.setup([]); - assert.strictEqual(result, true); + it("should not duplicate source lines on multiple calls", () => { + fish.setup(); + fish.setup(); - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } + const content = fs.readFileSync(mockStartupFile, "utf-8"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)"); }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove npm, npx, yarn aliases and source line", () => { const initialContent = [ "#!/usr/bin/env fish", "alias npm 'aikido-npm'", "alias npx 'aikido-npx'", "alias yarn 'aikido-yarn'", + "source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", "alias ls 'ls --color=auto'", "alias grep 'grep --color=auto'", ].join("\n"); @@ -120,6 +107,7 @@ describe("Fish shell integration", () => { assert.ok(!content.includes("alias npm ")); assert.ok(!content.includes("alias npx ")); assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); assert.ok(content.includes("alias ls ")); assert.ok(content.includes("alias grep ")); }); @@ -133,7 +121,7 @@ describe("Fish shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant aliases or source lines", () => { const initialContent = [ "#!/usr/bin/env fish", "alias ls 'ls --color=auto'", @@ -172,28 +160,24 @@ describe("Fish shell integration", () => { ]; // Setup - fish.setup(tools); + fish.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm "aikido-npm"')); - assert.ok(content.includes('alias yarn "aikido-yarn"')); + assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish')); // Teardown fish.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm ")); - assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - fish.setup(tools); - fish.teardown(tools); - fish.setup(tools); + fish.setup(); + fish.teardown(knownAikidoTools); + fish.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/alias npm "/g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle"); }); }); }); From 5df4671988f116f602e5eb089b2fde00dee97712 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 23 Jul 2025 11:43:13 +0200 Subject: [PATCH 26/57] Fix paths in tests --- .../supported-shells/powershell.spec.js | 23 ++++++++++++++----- .../windowsPowershell.spec.js | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 57d5098..3a15376 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -75,7 +75,9 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") + content.includes( + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' + ) ); }); }); @@ -84,7 +86,7 @@ describe("PowerShell Core shell integration", () => { it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# PowerShell profile", - ". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script", + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -95,7 +97,9 @@ describe("PowerShell Core shell integration", () => { assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); }); @@ -168,12 +172,16 @@ describe("PowerShell Core shell integration", () => { // Setup powershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); // Teardown powershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); }); it("should handle multiple setup calls", () => { @@ -182,7 +190,10 @@ describe("PowerShell Core shell integration", () => { powershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const sourceMatches = (content.match(/\. "\$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh\.ps1"/g) || []).length; + const sourceMatches = ( + content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + [] + ).length; assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js index 96677a8..c201c60 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -75,7 +75,9 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") + content.includes( + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' + ) ); }); }); @@ -84,7 +86,7 @@ describe("Windows PowerShell shell integration", () => { it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# Windows PowerShell profile", - ". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script", + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -95,7 +97,9 @@ describe("Windows PowerShell shell integration", () => { assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); }); @@ -168,12 +172,16 @@ describe("Windows PowerShell shell integration", () => { // Setup windowsPowershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); // Teardown windowsPowershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\"")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); }); it("should handle multiple setup calls", () => { @@ -182,7 +190,10 @@ describe("Windows PowerShell shell integration", () => { windowsPowershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const sourceMatches = (content.match(/\. "\$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh\.ps1"/g) || []).length; + const sourceMatches = ( + content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + [] + ).length; assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); From d9ea0e2efca4055347a0fbacc721411733e9811a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 24 Jul 2025 16:57:10 +0200 Subject: [PATCH 27/57] Modify posix script: don't automatically install --- package.json | 3 +- .../startup-scripts/init-posix.sh | 75 +++++-------------- 2 files changed, 22 insertions(+), 56 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/startup-scripts/init-posix.sh b/src/shell-integration/startup-scripts/init-posix.sh index 09a5926..3b6d13b 100644 --- a/src/shell-integration/startup-scripts/init-posix.sh +++ b/src/shell-integration/startup-scripts/init-posix.sh @@ -1,80 +1,45 @@ -function installIfCommandNotFound() { - local cmd="$1" - - # Check if the command already exists - if command -v "$cmd" > /dev/null 2>&1; then - return 0 - fi - - # Check if Node.js version is below 18 - # Safe-chain requires Node.js 18 or higher - local node_version=$(node -v | sed 's/v//' | cut -d'.' -f1) - if [ "$node_version" -lt 18 ]; then - return 2 - fi - - # Command not found, ask user if they want to install safe-chain - printf "The command '%s' is not available. Do you want to install safe-chain to provide it? (y/N): " "$cmd" - read -r response - - if [[ "$response" =~ ^[Yy]$ ]]; then - printf "Installing safe-chain...\n" - installSafeChain - - if [ $? -ne 0 ]; then - printf "\nFailed to install safe-chain. Exiting.\n" - return 1 - fi - - return 0 - else - printf "Skipping safe-chain installation. Using original command instead.\n" - return 2 - fi +function printSafeChainWarning() { + # \033[43;30m is used to set the background color to yellow and text color to black + # \033[0m is used to reset the text formatting + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will be run directly.\n" "$1" + # \033[36m is used to set the text color to cyan + printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" } -function installSafeChain() { - command npm install -g @aikidosec/safe-chain - - if [ $? -ne 0 ]; then - return 1 - fi - - printf "------\n" -} - -function wrapCommand() { +function wrapSafeChainCommand() { local original_cmd="$1" local aikido_cmd="$2" # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ # so that "$@" now contains only the arguments passed to the original command shift 2 - - installIfCommandNotFound "$aikido_cmd" - local install_result=$? - if [ $install_result -eq 2 ]; then - command "$original_cmd" "$@" - else + + if command -v "$aikido_cmd" > /dev/null 2>&1; then + # If the aikido command is available, just run it with the provided arguments "$aikido_cmd" "$@" + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning "$original_cmd" + + command "$original_cmd" "$@" fi } function npx() { - wrapCommand "npx" "aikido-npx" "$@" + wrapSafeChainCommand "npx" "aikido-npx" "$@" } function yarn() { - wrapCommand "yarn" "aikido-yarn" "$@" + wrapSafeChainCommand "yarn" "aikido-yarn" "$@" } function pnpm() { - wrapCommand "pnpm" "aikido-pnpm" "$@" + wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@" } function pnpx() { - wrapCommand "pnpx" "aikido-pnpx" "$@" + wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" } function npm() { @@ -85,5 +50,5 @@ function npm() { return fi - wrapCommand "npm" "aikido-npm" "$@" + wrapSafeChainCommand "npm" "aikido-npm" "$@" } From 0e6d96fe1d7c65488cfad2f95e143ade24eb4da8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 28 Jul 2025 10:37:36 +0200 Subject: [PATCH 28/57] Powershell: only print warning when safe-chain is unavailable --- .../startup-scripts/init-pwsh.ps1 | 84 +++++-------------- 1 file changed, 21 insertions(+), 63 deletions(-) diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 index 58a19c9..7d85ee1 100644 --- a/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -1,10 +1,24 @@ +function Write-SafeChainWarning { + param([string]$Command) + + # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" + Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline + Write-Host " safe-chain is not available to protect you from installing malware. $Command will be run directly." + + # Cyan text for the install command + Write-Host "Install safe-chain by using " -NoNewline + Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline + Write-Host "." +} + function Test-CommandAvailable { param([string]$Command) try { Get-Command $Command -ErrorAction Stop | Out-Null return $true - } catch { + } + catch { return $false } } @@ -19,62 +33,6 @@ function Invoke-RealCommand { $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 if ($realCommand) { & $realCommand.Source @Arguments - } else { - # Fallback: try to call the .cmd version directly - & "$Command.cmd" @Arguments - } -} - -function Install-IfCommandNotFound { - param([string]$Command) - - # Check if the command already exists - if (Test-CommandAvailable $Command) { - return 0 - } - - # Check if Node.js version is below 18 - # Safe-chain requires Node.js 18 or higher - try { - $nodeVersion = (node -v) -replace 'v', '' | ForEach-Object { $_.Split('.')[0] } - if ([int]$nodeVersion -lt 18) { - return 2 - } - } catch { - return 2 - } - - # Command not found, ask user if they want to install safe-chain - $response = Read-Host "The command '$Command' is not available. Do you want to install safe-chain to provide it? (y/N)" - - if ($response -match '^[Yy]$') { - Write-Host "Installing safe-chain..." - $installResult = Install-SafeChain - - if ($installResult -ne 0) { - Write-Host "`nFailed to install safe-chain. Exiting." - return 1 - } - - return 0 - } else { - Write-Host "Skipping safe-chain installation. Using original command instead." - return 2 - } -} - -function Install-SafeChain { - try { - Invoke-RealCommand "npm" @("install", "-g", "@aikidosec/safe-chain") | Out-Null - - if ($LASTEXITCODE -ne 0) { - return 1 - } - - Write-Host "------" - return 0 - } catch { - return 1 } } @@ -84,14 +42,14 @@ function Invoke-WrappedCommand { [string]$AikidoCmd, [string[]]$Arguments ) - - $installResult = Install-IfCommandNotFound $AikidoCmd - - if ($installResult -eq 2) { - Invoke-RealCommand $OriginalCmd $Arguments - } else { + + if (Test-CommandAvailable $AikidoCmd) { & $AikidoCmd @Arguments } + else { + Write-SafeChainWarning $OriginalCmd + Invoke-RealCommand $OriginalCmd $Arguments + } } function npx { From 32e2408ad0553fcc3cba5572dae2457699951d43 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 28 Jul 2025 11:00:01 +0200 Subject: [PATCH 29/57] Fish: only show warning instead of auto-installing safe-chain --- .../startup-scripts/init-fish.fish | 77 ++++++------------- 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish index 3a6c18e..86d7fcc 100644 --- a/src/shell-integration/startup-scripts/init-fish.fish +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -1,76 +1,49 @@ -function installIfCommandNotFound - set cmd $argv[1] +function printSafeChainWarning + set original_cmd $argv[1] - # Check if the command already exists - if type -q $cmd - return 0 - end + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" + set_color -b yellow black + printf "Warning:" + set_color normal + printf " safe-chain is not available to protect you from installing malware. %s will be run directly.\n" $original_cmd - # Check if Node.js version is below 18 - # Safe-chain requires Node.js 18 or higher - set node_version (node -v | sed 's/v//' | cut -d'.' -f1) - if test $node_version -lt 18 - return 2 - end - - # Command not found, ask user if they want to install safe-chain - read -l response -P "The command '$cmd' is not available. Do you want to install safe-chain to provide it? (y/N): " - - if string match -qi 'y*' $response - printf "Installing safe-chain...\n" - installSafeChain - - if test $status -ne 0 - printf "\nFailed to install safe-chain. Exiting.\n" - return 1 - end - - return 0 - else - printf "Skipping safe-chain installation. Using original command instead.\n" - return 2 - end + # Cyan text for the install command + printf "Install safe-chain by using " + set_color cyan + printf "npm install -g @aikidosec/safe-chain" + set_color normal + printf ".\n" end -function installSafeChain - command npm install -g @aikidosec/safe-chain - - if test $status -ne 0 - return 1 - end - - printf "------\n" -end - -function wrapCommand +function wrapSafeChainCommand set original_cmd $argv[1] set aikido_cmd $argv[2] set cmd_args $argv[3..-1] - installIfCommandNotFound $aikido_cmd - set install_result $status - - if test $install_result -eq 2 - command $original_cmd $cmd_args - else + if type -q $aikido_cmd + # If the aikido command is available, just run it with the provided arguments $aikido_cmd $cmd_args + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning $original_cmd + command $original_cmd $cmd_args end end function npx - wrapCommand "npx" "aikido-npx" $argv + wrapSafeChainCommand "npx" "aikido-npx" $argv end function yarn - wrapCommand "yarn" "aikido-yarn" $argv + wrapSafeChainCommand "yarn" "aikido-yarn" $argv end function pnpm - wrapCommand "pnpm" "aikido-pnpm" $argv + wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv end function pnpx - wrapCommand "pnpx" "aikido-pnpx" $argv + wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv end function npm @@ -81,5 +54,5 @@ function npm return end - wrapCommand "npm" "aikido-npm" $argv + wrapSafeChainCommand "npm" "aikido-npm" $argv end \ No newline at end of file From 5b4d014ef2cbf7f826f25a4342655317cbc0c529 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 28 Jul 2025 12:41:14 +0200 Subject: [PATCH 30/57] Cleanup --- 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 784892826cf9da2e8c3762d520c6b89b0954317b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 28 Jul 2025 15:01:33 +0200 Subject: [PATCH 31/57] Shorten contribution file, add bug template --- .github/CONTRIBUTING | 57 +++++++++++--------------- .github/ISSUE_TEMPLATE/bug_report.yml | 58 +++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 33 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/CONTRIBUTING b/.github/CONTRIBUTING index b7d2a37..78e0820 100644 --- a/.github/CONTRIBUTING +++ b/.github/CONTRIBUTING @@ -1,45 +1,36 @@ # Contributing to Aikido Safe Chain -Thank you for your interest in contributing to the Aikido Safe Chain! Before you start, please take a moment to read through our guidelines. +Thank you for your interest in contributing! Please discuss significant changes in an issue first to avoid duplication of work. ## Reporting Issues -If you encounter any issues while using the Aikido Safe Chain, please report them on our [GitHub Issues page](https://github.com/AikidoSec/safe-chain). - -Before creating a new issue, please check if the issue has already been reported or addressed in the documentation. If you find a similar issue, feel free to add any additional information that might help us resolve it. - -Please try to create bug reports that are: - -- **Reproducible**: Include steps to reproduce the issue. -- **Clear**: Describe the issue in detail, including what you expected to happen and what - actually happened. -- **Environment**: Specify your environment, including the version of Node.js, the package manager you are using and its version, and your operating system. -- **Logs**: Include any relevant logs or error messages. -- **Scoped**: Please report a single issue per report. +Report issues on our [GitHub Issues page](https://github.com/AikidoSec/safe-chain). Check existing issues first and include: +- Steps to reproduce +- Expected vs actual behavior +- Environment details (Node.js version, OS, package manager) +- Relevant logs/errors ## Contributing Code -To avoid your contribution is rejected, please engage with the community and maintainers before starting work on a new feature or significant change. You can do this by opening an issue to discuss your ideas or by discussing your ideas in an existing issue. This helps to avoid to accept contributions and avoids duplication of work. +1. Fork and clone the repository +2. Create a descriptive branch name +3. Make your changes following existing code style +4. Add tests for your changes +5. Run `npm test` and `npm run lint` +6. Push and create a pull request -If you would like to contribute code to the Aikido Safe Chain, please follow these steps: +## Development -1. **Fork the repository**: Create a fork of the Aikido Safe Chain repository on GitHub. -2. **Clone your fork**: Clone your forked repository to your local machine. -3. **Create a new branch**: Create a new branch for your changes. Use a descriptive name for the branch that reflects the changes you are making. -4. **Make your changes**: Implement your changes in the new branch. Make sure to follow the coding style and conventions used in the project. -5. **Test your changes**: Please ensure that your changes are covered by tests and that all tests pass. -6. **Commit your changes**: Commit your changes with a clear and descriptive commit message. -7. **Push your changes**: Push your changes to your forked repository. -8. **Create a pull request**: Open a pull request against the main repository. Provide a clear description of your changes and reference any related issues. +**Setup:** +```bash +npm install +``` -### Development Environment +**Commands:** +- `npm test` - Run tests +- `npm run test:watch` - Watch mode +- `npm run lint` - Check code style -To set up your development environment, you can follow these steps: - -1. **Install Node.js**: Make sure you have Node.js version 18 or above installed on your machine. -2. **Install dependencies**: Run `npm install` in the root directory of the project to install the necessary dependencies. -3. **Run tests**: You can run the tests using `npm test` to ensure that everything is working correctly. - -### Code Style - -Our style guides are documented in the .editorconfig file in the root of the repository. Use Prettier to format your code according to these guidelines. You can run Prettier manually or set it up to run automatically on save in your code editor. +**Requirements:** +- Node.js 18+ +- Follow .editorconfig formatting diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a64f50a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Bug Report +description: Report a bug to help us improve +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the sections below to help us understand and reproduce the issue. + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is + placeholder: Describe what happened and what you expected to happen + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Run command '...' + 2. Install package '...' + 3. See error + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Your system information + placeholder: | + - OS: [e.g. macOS 14.0, Ubuntu 22.04, Windows 11] + - Node.js version: [e.g. 18.17.0] + - Package manager: [e.g. npm 9.6.7, yarn 1.22.19, pnpm 8.6.0] + - Safe Chain version: [e.g. 1.0.0] + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Error Logs + description: Relevant error messages or logs + placeholder: Paste any error messages or logs here + render: shell + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other context about the problem + placeholder: Screenshots, related issues, workarounds, etc. \ No newline at end of file From b7e7ee9f733f32b64403167ea184a6353120cdf1 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 28 Jul 2025 18:53:19 +0000 Subject: [PATCH 32/57] Update .github/CONTRIBUTING --- .github/CONTRIBUTING | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING b/.github/CONTRIBUTING index 78e0820..1fd93f8 100644 --- a/.github/CONTRIBUTING +++ b/.github/CONTRIBUTING @@ -14,7 +14,7 @@ Report issues on our [GitHub Issues page](https://github.com/AikidoSec/safe-chai 1. Fork and clone the repository 2. Create a descriptive branch name -3. Make your changes following existing code style +3. Make your changes following the existing code style 4. Add tests for your changes 5. Run `npm test` and `npm run lint` 6. Push and create a pull request From 0c56c3d1f91534f28ff447fde84a4fda63430f62 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 29 Jul 2025 11:09:49 +0200 Subject: [PATCH 33/57] Basic e2e test --- .github/workflows/e2e.yml | 37 +++++++ package.json | 1 + test/e2e/Dockerfile | 32 ++++++ test/e2e/setup.e2e.spec.js | 195 +++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 .github/workflows/e2e.yml create mode 100644 test/e2e/Dockerfile create mode 100644 test/e2e/setup.e2e.spec.js diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..5fa8d3f --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,37 @@ +name: E2E Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test + + - name: Run E2E tests + run: npm run test:e2e + + - name: Clean up Docker resources + if: always() + run: | + # Clean up any remaining containers and images + docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f + docker images -q safe-chain-e2e-test | xargs -r docker rmi -f \ No newline at end of file diff --git a/package.json b/package.json index 4ab4c95..8049b02 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks **/*.spec.js", "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js", + "test:e2e": "node --test test/e2e/**/*.e2e.spec.js", "lint": "eslint ." }, "repository": { diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile new file mode 100644 index 0000000..0b9e24d --- /dev/null +++ b/test/e2e/Dockerfile @@ -0,0 +1,32 @@ +FROM node:18-alpine + +# Install bash and basic utilities (Alpine uses apk, not apt-get) +RUN apk add --no-cache bash curl + +# Create a test user to simulate real user environment (Alpine syntax) +RUN addgroup -S testuser && adduser -S testuser -G testuser -s /bin/bash + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application +COPY . . + +# Switch to test user +USER testuser + +# Create home directory structure that bash expects +RUN mkdir -p /home/testuser + +# Set environment variables for testing +ENV HOME=/home/testuser +ENV SHELL=/bin/bash + +# Default command runs our test +CMD ["bash", "test/e2e/test-setup.sh"] \ No newline at end of file diff --git a/test/e2e/setup.e2e.spec.js b/test/e2e/setup.e2e.spec.js new file mode 100644 index 0000000..7c4d905 --- /dev/null +++ b/test/e2e/setup.e2e.spec.js @@ -0,0 +1,195 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import { execSync, spawn } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, "../.."); + +describe("E2E: safe-chain setup command", () => { + const imageName = "safe-chain-e2e-test"; + const containerName = "safe-chain-e2e-test-container"; + + before(async () => { + console.log("Building Docker image for e2e tests..."); + try { + execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, { + cwd: projectRoot, + stdio: "inherit", + }); + console.log("Docker image built successfully"); + } catch (error) { + throw new Error(`Failed to build Docker image: ${error.message}`); + } + }); + + after(async () => { + // Clean up: remove container and image + try { + execSync(`docker rm -f ${containerName}`, { stdio: "ignore" }); + } catch { + // Container might not exist, ignore + } + + try { + execSync(`docker rmi ${imageName}`, { stdio: "ignore" }); + } catch { + // Image might be in use, ignore + } + }); + + it("should successfully run safe-chain setup and create aliases", async () => { + // Run the container and capture output + const result = await runDockerTest([ + "node", "bin/safe-chain.js", "setup" + ]); + + // Verify setup completed successfully + assert.ok( + result.stdout.includes("Setup successful"), + "Setup should report success" + ); + + assert.strictEqual( + result.exitCode, + 0, + `Setup should exit with code 0, got ${result.exitCode}` + ); + }); + + it("should create correct aliases in .bashrc", async () => { + // Run setup and then check .bashrc contents + const result = await runDockerTest([ + "bash", "-c", ` + node bin/safe-chain.js setup && + echo "=== BASHRC CONTENTS ===" && + cat /home/testuser/.bashrc + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + + const bashrcContent = result.stdout; + + // Check for all expected aliases + const expectedAliases = [ + 'alias npm="aikido-npm" # Safe-chain alias for npm', + 'alias npx="aikido-npx" # Safe-chain alias for npx', + 'alias yarn="aikido-yarn" # Safe-chain alias for yarn', + 'alias pnpm="aikido-pnpm" # Safe-chain alias for pnpm', + 'alias pnpx="aikido-pnpx" # Safe-chain alias for pnpx' + ]; + + for (const expectedAlias of expectedAliases) { + assert.ok( + bashrcContent.includes(expectedAlias), + `Should contain alias: ${expectedAlias}` + ); + } + }); + + it("should be idempotent (not create duplicate aliases)", async () => { + // Run setup twice and check for duplicates + const result = await runDockerTest([ + "bash", "-c", ` + node bin/safe-chain.js setup && + node bin/safe-chain.js setup && + echo "=== ALIAS COUNT ===" && + grep -c 'alias npm="aikido-npm"' /home/testuser/.bashrc || echo 0 + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + + // Extract the count from output + const lines = result.stdout.split('\n'); + const countLine = lines.find(line => line.match(/^\d+$/)); + const aliasCount = parseInt(countLine || '0'); + + assert.strictEqual( + aliasCount, + 1, + `Should have exactly 1 npm alias, found ${aliasCount}` + ); + }); + + it("should work with fresh .bashrc file", async () => { + // Ensure no .bashrc exists initially + const result = await runDockerTest([ + "bash", "-c", ` + rm -f /home/testuser/.bashrc && + node bin/safe-chain.js setup && + test -f /home/testuser/.bashrc && echo "BASHRC_CREATED" || + echo "BASHRC_NOT_CREATED" + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + assert.ok( + result.stdout.includes("BASHRC_CREATED"), + ".bashrc should be created if it doesn't exist" + ); + }); + + it("should detect bash shell correctly", async () => { + const result = await runDockerTest([ + "node", "bin/safe-chain.js", "setup" + ]); + + assert.strictEqual(result.exitCode, 0, "Setup should succeed"); + assert.ok( + result.stdout.includes("Detected") && result.stdout.includes("Bash"), + "Should detect Bash shell" + ); + }); + + /** + * Helper function to run a command in Docker container and return result + */ + async function runDockerTest(command) { + return new Promise((resolve, reject) => { + const dockerArgs = [ + "run", "--rm", + "--name", containerName, + imageName, + ...command + ]; + + const child = spawn("docker", dockerArgs, { + cwd: projectRoot, + stdio: ["pipe", "pipe", "pipe"] + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + resolve({ + exitCode: code, + stdout, + stderr + }); + }); + + child.on("error", (error) => { + reject(new Error(`Docker command failed: ${error.message}`)); + }); + + // Set timeout to prevent hanging tests + setTimeout(() => { + child.kill(); + reject(new Error("Test timed out after 60 seconds")); + }, 60000); + }); + } +}); \ No newline at end of file From 73b209a5f63e10f97e5a20dc838fc01a1bd704da Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 29 Jul 2025 11:12:09 +0200 Subject: [PATCH 34/57] bump node version --- .github/workflows/e2e.yml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5fa8d3f..47c4d6f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -2,36 +2,36 @@ name: E2E Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: e2e-tests: runs-on: ubuntu-latest - + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run unit tests - run: npm test - - - name: Run E2E tests - run: npm run test:e2e - - - name: Clean up Docker resources - if: always() - run: | - # Clean up any remaining containers and images - docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f - docker images -q safe-chain-e2e-test | xargs -r docker rmi -f \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test + + - name: Run E2E tests + run: npm run test:e2e + + - name: Clean up Docker resources + if: always() + run: | + # Clean up any remaining containers and images + docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f + docker images -q safe-chain-e2e-test | xargs -r docker rmi -f From 05ebb3f19ee50c71ff3daa69bccea4ef084aafe4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:10:46 +0200 Subject: [PATCH 35/57] First setup and teardown tests --- package-lock.json | 17 +++ package.json | 1 + test/e2e/DockerTestContainer.js | 113 +++++++++++++++++ test/e2e/Dockerfile | 31 ++--- test/e2e/parseShellOutput.js | 100 +++++++++++++++ test/e2e/setup.e2e.spec.js | 208 +++++++------------------------- 6 files changed, 292 insertions(+), 178 deletions(-) create mode 100644 test/e2e/DockerTestContainer.js create mode 100644 test/e2e/parseShellOutput.js diff --git a/package-lock.json b/package-lock.json index 260ee8b..747aedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@inquirer/prompts": "^7.4.1", "abbrev": "^3.0.1", "chalk": "^5.4.1", + "node-pty": "^1.0.0", "npm-registry-fetch": "^18.0.2", "ora": "^8.2.0", "semver": "^7.7.2" @@ -3905,6 +3906,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3921,6 +3928,16 @@ "node": ">= 0.6" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/npm-package-arg": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", diff --git a/package.json b/package.json index 8049b02..eceeef9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@inquirer/prompts": "^7.4.1", "abbrev": "^3.0.1", "chalk": "^5.4.1", + "node-pty": "^1.0.0", "npm-registry-fetch": "^18.0.2", "ora": "^8.2.0", "semver": "^7.7.2" diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js new file mode 100644 index 0000000..3351c08 --- /dev/null +++ b/test/e2e/DockerTestContainer.js @@ -0,0 +1,113 @@ +import { execSync } from "node:child_process"; +import * as pty from "node-pty"; +import { parseShellOutput } from "./parseShellOutput.js"; + +export class DockerTestContainer { + constructor(imageName, containerName) { + this.imageName = imageName; + this.containerName = containerName; + this.isRunning = false; + } + + async start() { + if (this.isRunning) { + throw new Error("Container is already running"); + } + + try { + // Start a long-running container that we can exec commands into + execSync( + `docker run -d --name ${this.containerName} ${this.imageName} sleep infinity`, + { stdio: "ignore" } + ); + this.isRunning = true; + } catch (error) { + throw new Error(`Failed to start container: ${error.message}`); + } + } + + async openShell(shell) { + let ptyProcess = pty.spawn( + "docker", + ["exec", "-it", this.containerName, shell], + { + name: "xterm-color", + cols: 80, + rows: 30, + } + ); + + await new Promise((resolve, reject) => { + ptyProcess.on("data", (data) => { + if (data.includes("\u001b[?2004h")) { + // This indicates that the shell is ready + resolve(); + } + }); + + ptyProcess.on("error", (err) => { + reject(err); + }); + }); + + function runCommand(command) { + if (!ptyProcess) { + throw new Error("Shell is not running"); + } + + return new Promise((resolve) => { + let allData = []; + + ptyProcess.on("data", handleInput); + + const timeout = setTimeout(() => { + // Fallback in case the command doesn't finish in a reasonable time + resolve({ allData, output: parseShellOutput(allData), command }); + ptyProcess.removeListener("data", handleInput); + }, 10000); + + function handleInput(data) { + allData.push(data); + + if (data.includes("\u001b[?2004h")) { + // This indicates that the command has finished executing + resolve({ allData, output: parseShellOutput(allData), command }); + ptyProcess.removeListener("data", handleInput); + clearTimeout(timeout); + } + } + + ptyProcess.write(`${command}\n`); + }); + } + + return { runCommand }; + } + + async stop() { + if (!this.isRunning) { + return; // Already stopped + } + + try { + // Force stop and remove the container + execSync(`docker kill ${this.containerName}`, { + stdio: "ignore", + timeout: 10000, + }); + } catch { + // Container might already be stopped + } + + try { + execSync(`docker rm -f ${this.containerName}`, { + stdio: "ignore", + timeout: 5000, + }); + } catch { + // Container might already be removed + } + + this.isRunning = false; + } +} diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 0b9e24d..8518957 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -1,10 +1,6 @@ -FROM node:18-alpine +FROM node:24 as builder -# Install bash and basic utilities (Alpine uses apk, not apt-get) -RUN apk add --no-cache bash curl - -# Create a test user to simulate real user environment (Alpine syntax) -RUN addgroup -S testuser && adduser -S testuser -G testuser -s /bin/bash +ENV CI=true # Set working directory WORKDIR /app @@ -18,15 +14,20 @@ RUN npm install # Copy the rest of the application COPY . . -# Switch to test user -USER testuser +# Build the application +RUN npm --no-git-tag-version version 1.0.0 --allow-same-version +RUN npm pack -# Create home directory structure that bash expects -RUN mkdir -p /home/testuser +FROM mcr.microsoft.com/devcontainers/javascript-node as runner -# Set environment variables for testing -ENV HOME=/home/testuser -ENV SHELL=/bin/bash +WORKDIR /app -# Default command runs our test -CMD ["bash", "test/e2e/test-setup.sh"] \ No newline at end of file +COPY --from=builder /app/*.tgz /app/ + +# # Install the application package globally +RUN npm install -g /app/*.tgz + +RUN mkdir /testapp +RUN cd /testapp && npm init -y + +# ENV SHELL=/bin/bash diff --git a/test/e2e/parseShellOutput.js b/test/e2e/parseShellOutput.js new file mode 100644 index 0000000..29a581b --- /dev/null +++ b/test/e2e/parseShellOutput.js @@ -0,0 +1,100 @@ +const escapeChar = "\u001b"; +const startMarker = `${escapeChar}[?2004l`; +const endMarker = `${escapeChar}[?2004h`; + +export function parseShellOutput(rawData) { + const stringData = rawData.join(""); + + let output = getDataBetweenStartAndEndMarkers(stringData); + output = processBackspaces(output); + output = processEraseCommands(output); + output = removeOscSequences(output); + output = removeAnsiSgrSequences(output); + output = removeRemainingEscapeSequences(output); + + return output.trim(); +} + +function getDataBetweenStartAndEndMarkers(data) { + if (!data.includes(startMarker) || !data.includes(endMarker)) { + return data; + } + + const startIndex = data.indexOf(startMarker); + const endIndex = data.indexOf(endMarker, startIndex + startMarker.length); + + if (startIndex === -1 || endIndex === -1) { + return ""; + } + + return data.slice(startIndex + startMarker.length, endIndex); +} + +function processBackspaces(data) { + const result = []; + + for (let i = 0; i < data.length; i++) { + const char = data[i]; + + if (char === "\b") { + // Backspace: remove the previous character if it exists + if (result.length > 0) { + result.pop(); + } + } else { + result.push(char); + } + } + + return result.join(""); +} + +function removeOscSequences(data) { + return data.replace(/\u001b\][0-9]*;[^\u0007\u001b]*(\u0007|\u001b\\)/g, ""); +} + +function removeAnsiSgrSequences(data) { + return data.replace(/\u001b\[[0-9;]*m/g, ""); +} + +function processEraseCommands(data) { + const lines = data.split("\n"); + const result = []; + + for (let line of lines) { + // Process erase in line commands + line = line.replace(/\u001b\[K/g, ""); // Erase to end of line + line = line.replace(/\u001b\[0K/g, ""); // Erase to end of line + line = line.replace(/\u001b\[1K/g, ""); // Erase from start of line to cursor + line = line.replace(/\u001b\[2K/g, ""); // Erase entire line - remove the whole line + + // Skip lines that were completely erased + if (line.includes("\u001b[2K")) { + continue; + } + + result.push(line); + } + + // Process erase in display commands + let output = result.join("\n"); + output = output.replace(/\u001b\[J/g, ""); // Erase to end of display + output = output.replace(/\u001b\[0J/g, ""); // Erase to end of display + output = output.replace(/\u001b\[1J/g, ""); // Erase from start to cursor + output = output.replace(/\u001b\[2J/g, ""); // Erase entire display + + return output; +} + +function removeRemainingEscapeSequences(data) { + // Remove mode setting sequences like \u001b[?1h, \u001b[?1l + data = data.replace(/\u001b\[\?[0-9]+[hl]/g, ""); + + // Remove any other CSI sequences we haven't handled + data = data.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, ""); + + // Remove incomplete or malformed escape sequences + data = data.replace(/\u001b[^\u001b]*/g, ""); + + return data; +} diff --git a/test/e2e/setup.e2e.spec.js b/test/e2e/setup.e2e.spec.js index 7c4d905..16b2669 100644 --- a/test/e2e/setup.e2e.spec.js +++ b/test/e2e/setup.e2e.spec.js @@ -1,8 +1,9 @@ -import { describe, it, before, after } from "node:test"; -import assert from "node:assert"; -import { execSync, spawn } from "node:child_process"; +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { execSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -11,185 +12,66 @@ const projectRoot = path.resolve(__dirname, "../.."); describe("E2E: safe-chain setup command", () => { const imageName = "safe-chain-e2e-test"; const containerName = "safe-chain-e2e-test-container"; + let container; before(async () => { - console.log("Building Docker image for e2e tests..."); + // Build the Docker image for the test environment try { execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, { cwd: projectRoot, - stdio: "inherit", + stdio: "ignore", }); - console.log("Docker image built successfully"); } catch (error) { - throw new Error(`Failed to build Docker image: ${error.message}`); + throw new Error(`Failed to setup test environment: ${error.message}`); } }); - after(async () => { - // Clean up: remove container and image - try { - execSync(`docker rm -f ${containerName}`, { stdio: "ignore" }); - } catch { - // Container might not exist, ignore - } - - try { - execSync(`docker rmi ${imageName}`, { stdio: "ignore" }); - } catch { - // Image might be in use, ignore + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(imageName, containerName); + + await container.start(); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; } }); - it("should successfully run safe-chain setup and create aliases", async () => { - // Run the container and capture output - const result = await runDockerTest([ - "node", "bin/safe-chain.js", "setup" - ]); + for (let shell of ["bash", "zsh"]) { + it(`safe-chain setup wraps npm command after installation for ${shell}`, async () => { + // setting up the container + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup"); - // Verify setup completed successfully - assert.ok( - result.stdout.includes("Setup successful"), - "Setup should report success" - ); - - assert.strictEqual( - result.exitCode, - 0, - `Setup should exit with code 0, got ${result.exitCode}` - ); - }); + const projectShell = await container.openShell(shell); + await projectShell.runCommand("cd /testapp"); + const result = await projectShell.runCommand("npm i axios"); - it("should create correct aliases in .bashrc", async () => { - // Run setup and then check .bashrc contents - const result = await runDockerTest([ - "bash", "-c", ` - node bin/safe-chain.js setup && - echo "=== BASHRC CONTENTS ===" && - cat /home/testuser/.bashrc - ` - ]); - - assert.strictEqual(result.exitCode, 0, "Commands should succeed"); - - const bashrcContent = result.stdout; - - // Check for all expected aliases - const expectedAliases = [ - 'alias npm="aikido-npm" # Safe-chain alias for npm', - 'alias npx="aikido-npx" # Safe-chain alias for npx', - 'alias yarn="aikido-yarn" # Safe-chain alias for yarn', - 'alias pnpm="aikido-pnpm" # Safe-chain alias for pnpm', - 'alias pnpx="aikido-pnpx" # Safe-chain alias for pnpx' - ]; - - for (const expectedAlias of expectedAliases) { assert.ok( - bashrcContent.includes(expectedAlias), - `Should contain alias: ${expectedAlias}` + result.output.includes("Scanning for malicious packages..."), + "Expected npm command to be wrapped by safe-chain" ); - } - }); + }); - it("should be idempotent (not create duplicate aliases)", async () => { - // Run setup twice and check for duplicates - const result = await runDockerTest([ - "bash", "-c", ` - node bin/safe-chain.js setup && - node bin/safe-chain.js setup && - echo "=== ALIAS COUNT ===" && - grep -c 'alias npm="aikido-npm"' /home/testuser/.bashrc || echo 0 - ` - ]); + it(`safe-chain teardown unwraps npm command after uninstallation for ${shell}`, async () => { + // setting up the container + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup"); + await installationShell.runCommand("safe-chain teardown"); - assert.strictEqual(result.exitCode, 0, "Commands should succeed"); - - // Extract the count from output - const lines = result.stdout.split('\n'); - const countLine = lines.find(line => line.match(/^\d+$/)); - const aliasCount = parseInt(countLine || '0'); - - assert.strictEqual( - aliasCount, - 1, - `Should have exactly 1 npm alias, found ${aliasCount}` - ); - }); + const projectShell = await container.openShell(shell); + await projectShell.runCommand("cd /testapp"); + await projectShell.runCommand("npm i axios"); + const result = await projectShell.runCommand("npm i axios"); - it("should work with fresh .bashrc file", async () => { - // Ensure no .bashrc exists initially - const result = await runDockerTest([ - "bash", "-c", ` - rm -f /home/testuser/.bashrc && - node bin/safe-chain.js setup && - test -f /home/testuser/.bashrc && echo "BASHRC_CREATED" || - echo "BASHRC_NOT_CREATED" - ` - ]); - - assert.strictEqual(result.exitCode, 0, "Commands should succeed"); - assert.ok( - result.stdout.includes("BASHRC_CREATED"), - ".bashrc should be created if it doesn't exist" - ); - }); - - it("should detect bash shell correctly", async () => { - const result = await runDockerTest([ - "node", "bin/safe-chain.js", "setup" - ]); - - assert.strictEqual(result.exitCode, 0, "Setup should succeed"); - assert.ok( - result.stdout.includes("Detected") && result.stdout.includes("Bash"), - "Should detect Bash shell" - ); - }); - - /** - * Helper function to run a command in Docker container and return result - */ - async function runDockerTest(command) { - return new Promise((resolve, reject) => { - const dockerArgs = [ - "run", "--rm", - "--name", containerName, - imageName, - ...command - ]; - - const child = spawn("docker", dockerArgs, { - cwd: projectRoot, - stdio: ["pipe", "pipe", "pipe"] - }); - - let stdout = ""; - let stderr = ""; - - child.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - child.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - child.on("close", (code) => { - resolve({ - exitCode: code, - stdout, - stderr - }); - }); - - child.on("error", (error) => { - reject(new Error(`Docker command failed: ${error.message}`)); - }); - - // Set timeout to prevent hanging tests - setTimeout(() => { - child.kill(); - reject(new Error("Test timed out after 60 seconds")); - }, 60000); + assert.ok( + !result.output.includes("Scanning for malicious packages..."), + "Expected npm command to not be wrapped by safe-chain after teardown" + ); }); } -}); \ No newline at end of file +}); From 9ad90d8073fc1dec2f493fe0d913e03aeddc3555 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:19:14 +0200 Subject: [PATCH 36/57] Fix linting errors --- test/e2e/parseShellOutput.js | 4 ++++ test/e2e/{setup.e2e.spec.js => setup.teardown.e2e.spec.js} | 0 2 files changed, 4 insertions(+) rename test/e2e/{setup.e2e.spec.js => setup.teardown.e2e.spec.js} (100%) diff --git a/test/e2e/parseShellOutput.js b/test/e2e/parseShellOutput.js index 29a581b..4c2d998 100644 --- a/test/e2e/parseShellOutput.js +++ b/test/e2e/parseShellOutput.js @@ -2,6 +2,10 @@ const escapeChar = "\u001b"; const startMarker = `${escapeChar}[?2004l`; const endMarker = `${escapeChar}[?2004h`; +/* eslint-disable no-control-regex */ +// This module removes control characters and escape sequences from shell output. +// So it is allowed to use control characters in the regex patterns here. + export function parseShellOutput(rawData) { const stringData = rawData.join(""); diff --git a/test/e2e/setup.e2e.spec.js b/test/e2e/setup.teardown.e2e.spec.js similarity index 100% rename from test/e2e/setup.e2e.spec.js rename to test/e2e/setup.teardown.e2e.spec.js From dc3a7bf6e26cd037d7c088e05fa9415ef4d66039 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:21:05 +0200 Subject: [PATCH 37/57] Cleanup comments --- test/e2e/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 8518957..1e5a3df 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -30,4 +30,3 @@ RUN npm install -g /app/*.tgz RUN mkdir /testapp RUN cd /testapp && npm init -y -# ENV SHELL=/bin/bash From b1ca2d2dc584f04510fde8e6291849a627b0a2c7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:37:37 +0200 Subject: [PATCH 38/57] Move project to separate folder --- .github/workflows/test-on-pr.yml | 5 ++++- {bin => safe-chain/bin}/aikido-npm.js | 0 {bin => safe-chain/bin}/aikido-npx.js | 0 {bin => safe-chain/bin}/aikido-pnpm.js | 0 {bin => safe-chain/bin}/aikido-pnpx.js | 0 {bin => safe-chain/bin}/aikido-yarn.js | 0 {bin => safe-chain/bin}/safe-chain.js | 0 package-lock.json => safe-chain/package-lock.json | 0 package.json => safe-chain/package.json | 0 {src => safe-chain/src}/api/aikido.js | 0 {src => safe-chain/src}/api/npmApi.js | 0 {src => safe-chain/src}/config/configFile.js | 0 {src => safe-chain/src}/environment/environment.js | 0 {src => safe-chain/src}/environment/userInteraction.js | 0 {src => safe-chain/src}/main.js | 0 .../src}/packagemanager/_shared/matchesCommand.js | 0 .../src}/packagemanager/currentPackageManager.js | 0 .../src}/packagemanager/npm/createPackageManager.js | 0 .../npm/dependencyScanner/commandArgumentScanner.js | 0 .../packagemanager/npm/dependencyScanner/dryRunScanner.js | 0 .../src}/packagemanager/npm/dependencyScanner/nullScanner.js | 0 .../npm/parsing/parseNpmInstallDryRunOutput.js | 0 .../npm/parsing/parseNpmInstallDryRunOutput.spec.js | 0 .../npm/parsing/parsePackagesFromInstallArgs.js | 0 .../npm/parsing/parsePackagesFromInstallArgs.spec.js | 0 {src => safe-chain/src}/packagemanager/npm/runNpmCommand.js | 0 {src => safe-chain/src}/packagemanager/npm/utils/cmd-list.js | 0 .../src}/packagemanager/npm/utils/npmCommands.js | 0 .../src}/packagemanager/npx/createPackageManager.js | 0 .../npx/dependencyScanner/commandArgumentScanner.js | 0 .../packagemanager/npx/parsing/parsePackagesFromArguments.js | 0 .../npx/parsing/parsePackagesFromArguments.spec.js | 0 {src => safe-chain/src}/packagemanager/npx/runNpxCommand.js | 0 .../src}/packagemanager/pnpm/createPackageManager.js | 0 .../pnpm/dependencyScanner/commandArgumentScanner.js | 0 .../pnpm/parsing/parsePackagesFromArguments.js | 0 .../pnpm/parsing/parsePackagesFromArguments.spec.js | 0 .../src}/packagemanager/pnpm/runPnpmCommand.js | 0 .../src}/packagemanager/yarn/createPackageManager.js | 0 .../yarn/dependencyScanner/commandArgumentScanner.js | 0 .../yarn/parsing/parsePackagesFromArguments.js | 0 .../yarn/parsing/parsePackagesFromArguments.spec.js | 0 .../src}/packagemanager/yarn/runYarnCommand.js | 0 {src => safe-chain/src}/scanning/audit/index.js | 0 {src => safe-chain/src}/scanning/index.js | 0 {src => safe-chain/src}/scanning/index.scanCommand.spec.js | 0 .../src}/scanning/index.shouldScanCommand.spec.js | 0 {src => safe-chain/src}/scanning/malwareDatabase.js | 0 {src => safe-chain/src}/shell-integration/helpers.js | 0 {src => safe-chain/src}/shell-integration/setup.js | 0 {src => safe-chain/src}/shell-integration/shellDetection.js | 0 .../src}/shell-integration/supported-shells/bash.js | 0 .../src}/shell-integration/supported-shells/bash.spec.js | 0 .../src}/shell-integration/supported-shells/fish.js | 0 .../src}/shell-integration/supported-shells/fish.spec.js | 0 .../src}/shell-integration/supported-shells/powershell.js | 0 .../shell-integration/supported-shells/powershell.spec.js | 0 .../shell-integration/supported-shells/windowsPowershell.js | 0 .../supported-shells/windowsPowershell.spec.js | 0 .../src}/shell-integration/supported-shells/zsh.js | 0 .../src}/shell-integration/supported-shells/zsh.spec.js | 0 {src => safe-chain/src}/shell-integration/teardown.js | 0 62 files changed, 4 insertions(+), 1 deletion(-) rename {bin => safe-chain/bin}/aikido-npm.js (100%) rename {bin => safe-chain/bin}/aikido-npx.js (100%) rename {bin => safe-chain/bin}/aikido-pnpm.js (100%) rename {bin => safe-chain/bin}/aikido-pnpx.js (100%) rename {bin => safe-chain/bin}/aikido-yarn.js (100%) rename {bin => safe-chain/bin}/safe-chain.js (100%) rename package-lock.json => safe-chain/package-lock.json (100%) rename package.json => safe-chain/package.json (100%) rename {src => safe-chain/src}/api/aikido.js (100%) rename {src => safe-chain/src}/api/npmApi.js (100%) rename {src => safe-chain/src}/config/configFile.js (100%) rename {src => safe-chain/src}/environment/environment.js (100%) rename {src => safe-chain/src}/environment/userInteraction.js (100%) rename {src => safe-chain/src}/main.js (100%) rename {src => safe-chain/src}/packagemanager/_shared/matchesCommand.js (100%) rename {src => safe-chain/src}/packagemanager/currentPackageManager.js (100%) rename {src => safe-chain/src}/packagemanager/npm/createPackageManager.js (100%) rename {src => safe-chain/src}/packagemanager/npm/dependencyScanner/commandArgumentScanner.js (100%) rename {src => safe-chain/src}/packagemanager/npm/dependencyScanner/dryRunScanner.js (100%) rename {src => safe-chain/src}/packagemanager/npm/dependencyScanner/nullScanner.js (100%) rename {src => safe-chain/src}/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js (100%) rename {src => safe-chain/src}/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js (100%) rename {src => safe-chain/src}/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js (100%) rename {src => safe-chain/src}/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js (100%) rename {src => safe-chain/src}/packagemanager/npm/runNpmCommand.js (100%) rename {src => safe-chain/src}/packagemanager/npm/utils/cmd-list.js (100%) rename {src => safe-chain/src}/packagemanager/npm/utils/npmCommands.js (100%) rename {src => safe-chain/src}/packagemanager/npx/createPackageManager.js (100%) rename {src => safe-chain/src}/packagemanager/npx/dependencyScanner/commandArgumentScanner.js (100%) rename {src => safe-chain/src}/packagemanager/npx/parsing/parsePackagesFromArguments.js (100%) rename {src => safe-chain/src}/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js (100%) rename {src => safe-chain/src}/packagemanager/npx/runNpxCommand.js (100%) rename {src => safe-chain/src}/packagemanager/pnpm/createPackageManager.js (100%) rename {src => safe-chain/src}/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js (100%) rename {src => safe-chain/src}/packagemanager/pnpm/parsing/parsePackagesFromArguments.js (100%) rename {src => safe-chain/src}/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js (100%) rename {src => safe-chain/src}/packagemanager/pnpm/runPnpmCommand.js (100%) rename {src => safe-chain/src}/packagemanager/yarn/createPackageManager.js (100%) rename {src => safe-chain/src}/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js (100%) rename {src => safe-chain/src}/packagemanager/yarn/parsing/parsePackagesFromArguments.js (100%) rename {src => safe-chain/src}/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js (100%) rename {src => safe-chain/src}/packagemanager/yarn/runYarnCommand.js (100%) rename {src => safe-chain/src}/scanning/audit/index.js (100%) rename {src => safe-chain/src}/scanning/index.js (100%) rename {src => safe-chain/src}/scanning/index.scanCommand.spec.js (100%) rename {src => safe-chain/src}/scanning/index.shouldScanCommand.spec.js (100%) rename {src => safe-chain/src}/scanning/malwareDatabase.js (100%) rename {src => safe-chain/src}/shell-integration/helpers.js (100%) rename {src => safe-chain/src}/shell-integration/setup.js (100%) rename {src => safe-chain/src}/shell-integration/shellDetection.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/bash.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/bash.spec.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/fish.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/fish.spec.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/powershell.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/powershell.spec.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/windowsPowershell.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/windowsPowershell.spec.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/zsh.js (100%) rename {src => safe-chain/src}/shell-integration/supported-shells/zsh.spec.js (100%) rename {src => safe-chain/src}/shell-integration/teardown.js (100%) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index b726376..881d93e 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -18,10 +18,13 @@ jobs: with: node-version: "lts/*" + - name: Go to safe-chain directory + run: cd safe-chain + - name: Install dependencies run: npm ci - - name: Run tests + - name: Run unit tests run: npm test - name: Run ESLint diff --git a/bin/aikido-npm.js b/safe-chain/bin/aikido-npm.js similarity index 100% rename from bin/aikido-npm.js rename to safe-chain/bin/aikido-npm.js diff --git a/bin/aikido-npx.js b/safe-chain/bin/aikido-npx.js similarity index 100% rename from bin/aikido-npx.js rename to safe-chain/bin/aikido-npx.js diff --git a/bin/aikido-pnpm.js b/safe-chain/bin/aikido-pnpm.js similarity index 100% rename from bin/aikido-pnpm.js rename to safe-chain/bin/aikido-pnpm.js diff --git a/bin/aikido-pnpx.js b/safe-chain/bin/aikido-pnpx.js similarity index 100% rename from bin/aikido-pnpx.js rename to safe-chain/bin/aikido-pnpx.js diff --git a/bin/aikido-yarn.js b/safe-chain/bin/aikido-yarn.js similarity index 100% rename from bin/aikido-yarn.js rename to safe-chain/bin/aikido-yarn.js diff --git a/bin/safe-chain.js b/safe-chain/bin/safe-chain.js similarity index 100% rename from bin/safe-chain.js rename to safe-chain/bin/safe-chain.js diff --git a/package-lock.json b/safe-chain/package-lock.json similarity index 100% rename from package-lock.json rename to safe-chain/package-lock.json diff --git a/package.json b/safe-chain/package.json similarity index 100% rename from package.json rename to safe-chain/package.json diff --git a/src/api/aikido.js b/safe-chain/src/api/aikido.js similarity index 100% rename from src/api/aikido.js rename to safe-chain/src/api/aikido.js diff --git a/src/api/npmApi.js b/safe-chain/src/api/npmApi.js similarity index 100% rename from src/api/npmApi.js rename to safe-chain/src/api/npmApi.js diff --git a/src/config/configFile.js b/safe-chain/src/config/configFile.js similarity index 100% rename from src/config/configFile.js rename to safe-chain/src/config/configFile.js diff --git a/src/environment/environment.js b/safe-chain/src/environment/environment.js similarity index 100% rename from src/environment/environment.js rename to safe-chain/src/environment/environment.js diff --git a/src/environment/userInteraction.js b/safe-chain/src/environment/userInteraction.js similarity index 100% rename from src/environment/userInteraction.js rename to safe-chain/src/environment/userInteraction.js diff --git a/src/main.js b/safe-chain/src/main.js similarity index 100% rename from src/main.js rename to safe-chain/src/main.js diff --git a/src/packagemanager/_shared/matchesCommand.js b/safe-chain/src/packagemanager/_shared/matchesCommand.js similarity index 100% rename from src/packagemanager/_shared/matchesCommand.js rename to safe-chain/src/packagemanager/_shared/matchesCommand.js diff --git a/src/packagemanager/currentPackageManager.js b/safe-chain/src/packagemanager/currentPackageManager.js similarity index 100% rename from src/packagemanager/currentPackageManager.js rename to safe-chain/src/packagemanager/currentPackageManager.js diff --git a/src/packagemanager/npm/createPackageManager.js b/safe-chain/src/packagemanager/npm/createPackageManager.js similarity index 100% rename from src/packagemanager/npm/createPackageManager.js rename to safe-chain/src/packagemanager/npm/createPackageManager.js diff --git a/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js rename to safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js diff --git a/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js similarity index 100% rename from src/packagemanager/npm/dependencyScanner/dryRunScanner.js rename to safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js diff --git a/src/packagemanager/npm/dependencyScanner/nullScanner.js b/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js similarity index 100% rename from src/packagemanager/npm/dependencyScanner/nullScanner.js rename to safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js diff --git a/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js b/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js similarity index 100% rename from src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js rename to safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js diff --git a/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js b/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js similarity index 100% rename from src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js rename to safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js diff --git a/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js b/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js similarity index 100% rename from src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js rename to safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js diff --git a/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js b/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js similarity index 100% rename from src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js rename to safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js diff --git a/src/packagemanager/npm/runNpmCommand.js b/safe-chain/src/packagemanager/npm/runNpmCommand.js similarity index 100% rename from src/packagemanager/npm/runNpmCommand.js rename to safe-chain/src/packagemanager/npm/runNpmCommand.js diff --git a/src/packagemanager/npm/utils/cmd-list.js b/safe-chain/src/packagemanager/npm/utils/cmd-list.js similarity index 100% rename from src/packagemanager/npm/utils/cmd-list.js rename to safe-chain/src/packagemanager/npm/utils/cmd-list.js diff --git a/src/packagemanager/npm/utils/npmCommands.js b/safe-chain/src/packagemanager/npm/utils/npmCommands.js similarity index 100% rename from src/packagemanager/npm/utils/npmCommands.js rename to safe-chain/src/packagemanager/npm/utils/npmCommands.js diff --git a/src/packagemanager/npx/createPackageManager.js b/safe-chain/src/packagemanager/npx/createPackageManager.js similarity index 100% rename from src/packagemanager/npx/createPackageManager.js rename to safe-chain/src/packagemanager/npx/createPackageManager.js diff --git a/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js rename to safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js diff --git a/src/packagemanager/npx/parsing/parsePackagesFromArguments.js b/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js similarity index 100% rename from src/packagemanager/npx/parsing/parsePackagesFromArguments.js rename to safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js diff --git a/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js b/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js rename to safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js diff --git a/src/packagemanager/npx/runNpxCommand.js b/safe-chain/src/packagemanager/npx/runNpxCommand.js similarity index 100% rename from src/packagemanager/npx/runNpxCommand.js rename to safe-chain/src/packagemanager/npx/runNpxCommand.js diff --git a/src/packagemanager/pnpm/createPackageManager.js b/safe-chain/src/packagemanager/pnpm/createPackageManager.js similarity index 100% rename from src/packagemanager/pnpm/createPackageManager.js rename to safe-chain/src/packagemanager/pnpm/createPackageManager.js diff --git a/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js b/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js rename to safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js diff --git a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js b/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js similarity index 100% rename from src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js rename to safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js diff --git a/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js b/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js rename to safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js diff --git a/src/packagemanager/pnpm/runPnpmCommand.js b/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js similarity index 100% rename from src/packagemanager/pnpm/runPnpmCommand.js rename to safe-chain/src/packagemanager/pnpm/runPnpmCommand.js diff --git a/src/packagemanager/yarn/createPackageManager.js b/safe-chain/src/packagemanager/yarn/createPackageManager.js similarity index 100% rename from src/packagemanager/yarn/createPackageManager.js rename to safe-chain/src/packagemanager/yarn/createPackageManager.js diff --git a/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js b/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js rename to safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js diff --git a/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js b/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js similarity index 100% rename from src/packagemanager/yarn/parsing/parsePackagesFromArguments.js rename to safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js diff --git a/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js b/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js rename to safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js diff --git a/src/packagemanager/yarn/runYarnCommand.js b/safe-chain/src/packagemanager/yarn/runYarnCommand.js similarity index 100% rename from src/packagemanager/yarn/runYarnCommand.js rename to safe-chain/src/packagemanager/yarn/runYarnCommand.js diff --git a/src/scanning/audit/index.js b/safe-chain/src/scanning/audit/index.js similarity index 100% rename from src/scanning/audit/index.js rename to safe-chain/src/scanning/audit/index.js diff --git a/src/scanning/index.js b/safe-chain/src/scanning/index.js similarity index 100% rename from src/scanning/index.js rename to safe-chain/src/scanning/index.js diff --git a/src/scanning/index.scanCommand.spec.js b/safe-chain/src/scanning/index.scanCommand.spec.js similarity index 100% rename from src/scanning/index.scanCommand.spec.js rename to safe-chain/src/scanning/index.scanCommand.spec.js diff --git a/src/scanning/index.shouldScanCommand.spec.js b/safe-chain/src/scanning/index.shouldScanCommand.spec.js similarity index 100% rename from src/scanning/index.shouldScanCommand.spec.js rename to safe-chain/src/scanning/index.shouldScanCommand.spec.js diff --git a/src/scanning/malwareDatabase.js b/safe-chain/src/scanning/malwareDatabase.js similarity index 100% rename from src/scanning/malwareDatabase.js rename to safe-chain/src/scanning/malwareDatabase.js diff --git a/src/shell-integration/helpers.js b/safe-chain/src/shell-integration/helpers.js similarity index 100% rename from src/shell-integration/helpers.js rename to safe-chain/src/shell-integration/helpers.js diff --git a/src/shell-integration/setup.js b/safe-chain/src/shell-integration/setup.js similarity index 100% rename from src/shell-integration/setup.js rename to safe-chain/src/shell-integration/setup.js diff --git a/src/shell-integration/shellDetection.js b/safe-chain/src/shell-integration/shellDetection.js similarity index 100% rename from src/shell-integration/shellDetection.js rename to safe-chain/src/shell-integration/shellDetection.js diff --git a/src/shell-integration/supported-shells/bash.js b/safe-chain/src/shell-integration/supported-shells/bash.js similarity index 100% rename from src/shell-integration/supported-shells/bash.js rename to safe-chain/src/shell-integration/supported-shells/bash.js diff --git a/src/shell-integration/supported-shells/bash.spec.js b/safe-chain/src/shell-integration/supported-shells/bash.spec.js similarity index 100% rename from src/shell-integration/supported-shells/bash.spec.js rename to safe-chain/src/shell-integration/supported-shells/bash.spec.js diff --git a/src/shell-integration/supported-shells/fish.js b/safe-chain/src/shell-integration/supported-shells/fish.js similarity index 100% rename from src/shell-integration/supported-shells/fish.js rename to safe-chain/src/shell-integration/supported-shells/fish.js diff --git a/src/shell-integration/supported-shells/fish.spec.js b/safe-chain/src/shell-integration/supported-shells/fish.spec.js similarity index 100% rename from src/shell-integration/supported-shells/fish.spec.js rename to safe-chain/src/shell-integration/supported-shells/fish.spec.js diff --git a/src/shell-integration/supported-shells/powershell.js b/safe-chain/src/shell-integration/supported-shells/powershell.js similarity index 100% rename from src/shell-integration/supported-shells/powershell.js rename to safe-chain/src/shell-integration/supported-shells/powershell.js diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/safe-chain/src/shell-integration/supported-shells/powershell.spec.js similarity index 100% rename from src/shell-integration/supported-shells/powershell.spec.js rename to safe-chain/src/shell-integration/supported-shells/powershell.spec.js diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js similarity index 100% rename from src/shell-integration/supported-shells/windowsPowershell.js rename to safe-chain/src/shell-integration/supported-shells/windowsPowershell.js diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js similarity index 100% rename from src/shell-integration/supported-shells/windowsPowershell.spec.js rename to safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js diff --git a/src/shell-integration/supported-shells/zsh.js b/safe-chain/src/shell-integration/supported-shells/zsh.js similarity index 100% rename from src/shell-integration/supported-shells/zsh.js rename to safe-chain/src/shell-integration/supported-shells/zsh.js diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/safe-chain/src/shell-integration/supported-shells/zsh.spec.js similarity index 100% rename from src/shell-integration/supported-shells/zsh.spec.js rename to safe-chain/src/shell-integration/supported-shells/zsh.spec.js diff --git a/src/shell-integration/teardown.js b/safe-chain/src/shell-integration/teardown.js similarity index 100% rename from src/shell-integration/teardown.js rename to safe-chain/src/shell-integration/teardown.js From 822e734a4ef55f768d8726778ea563543f28d927 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:39:33 +0200 Subject: [PATCH 39/57] Set working directory for unit tests --- .github/workflows/test-on-pr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 881d93e..fb4d87c 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -8,6 +8,9 @@ on: jobs: test: runs-on: ubuntu-latest + defaults: + run: + working-directory: safe-chain steps: - name: Checkout code @@ -18,9 +21,6 @@ jobs: with: node-version: "lts/*" - - name: Go to safe-chain directory - run: cd safe-chain - - name: Install dependencies run: npm ci From cc58c48c9b37280e8252e17d5878682e46cdbc03 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:41:01 +0200 Subject: [PATCH 40/57] Move linter file --- .github/workflows/test-on-pr.yml | 6 ++++-- eslint.config.js => safe-chain/eslint.config.js | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename eslint.config.js => safe-chain/eslint.config.js (100%) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index fb4d87c..8744a19 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -1,4 +1,4 @@ -name: Run Unit Tests +name: Run tests on: pull_request: @@ -6,7 +6,9 @@ on: - main jobs: - test: + unit-test: + name: Run unit tests and linting + runs-on: ubuntu-latest defaults: run: diff --git a/eslint.config.js b/safe-chain/eslint.config.js similarity index 100% rename from eslint.config.js rename to safe-chain/eslint.config.js From c966660a098b17ae00351d30726c8203dee3e4b5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:43:11 +0200 Subject: [PATCH 41/57] Remove e2e workflow --- .github/workflows/e2e.yml | 37 -------------------------------- .github/workflows/test-on-pr.yml | 32 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 47c4d6f..0000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: E2E Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - e2e-tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "24" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Run unit tests - run: npm test - - - name: Run E2E tests - run: npm run test:e2e - - - name: Clean up Docker resources - if: always() - run: | - # Clean up any remaining containers and images - docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f - docker images -q safe-chain-e2e-test | xargs -r docker rmi -f diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 8744a19..a68c07b 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -31,3 +31,35 @@ jobs: - name: Run ESLint run: npm run lint + + e2e-tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: test/e2e + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "npm" + + # - name: Install dependencies + # run: npm ci + + # - name: Run unit tests + # run: npm test + + # - name: Run E2E tests + # run: npm run test:e2e + + # - name: Clean up Docker resources + # if: always() + # run: | + # # Clean up any remaining containers and images + # docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f + # docker images -q safe-chain-e2e-test | xargs -r docker rmi -f From af53a8fbb86248baceebd3f3bc69865bf92932b8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:54:57 +0200 Subject: [PATCH 42/57] Run e2e tests on PR check --- .github/workflows/test-on-pr.yml | 24 +++++++++++----------- safe-chain/package.json | 1 - test/e2e/package-lock.json | 32 +++++++++++++++++++++++++++++ test/e2e/package.json | 15 ++++++++++++++ test/e2e/setup.teardown.e2e.spec.js | 5 ++--- 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 test/e2e/package-lock.json create mode 100644 test/e2e/package.json diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index a68c07b..c9302e9 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -48,18 +48,18 @@ jobs: node-version: "24" cache: "npm" - # - name: Install dependencies - # run: npm ci + - name: Install dependencies + run: npm ci - # - name: Run unit tests - # run: npm test + - name: Run unit tests + run: npm test - # - name: Run E2E tests - # run: npm run test:e2e + - name: Run E2E tests + run: npm run test:e2e - # - name: Clean up Docker resources - # if: always() - # run: | - # # Clean up any remaining containers and images - # docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f - # docker images -q safe-chain-e2e-test | xargs -r docker rmi -f + - name: Clean up Docker resources + if: always() + run: | + # Clean up any remaining containers and images + docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f + docker images -q safe-chain-e2e-test | xargs -r docker rmi -f diff --git a/safe-chain/package.json b/safe-chain/package.json index eceeef9..1fb7788 100644 --- a/safe-chain/package.json +++ b/safe-chain/package.json @@ -4,7 +4,6 @@ "scripts": { "test": "node --test --experimental-test-module-mocks **/*.spec.js", "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js", - "test:e2e": "node --test test/e2e/**/*.e2e.spec.js", "lint": "eslint ." }, "repository": { diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json new file mode 100644 index 0000000..55aabb7 --- /dev/null +++ b/test/e2e/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "@aikidosec/safe-chain-e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@aikidosec/safe-chain-e2e-tests", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "node-pty": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + } + } +} diff --git a/test/e2e/package.json b/test/e2e/package.json new file mode 100644 index 0000000..4748762 --- /dev/null +++ b/test/e2e/package.json @@ -0,0 +1,15 @@ +{ + "name": "@aikidosec/safe-chain-e2e-tests", + "version": "1.0.0", + "description": "End-to-end tests for the Aikido Safe Chain", + "scripts": { + "test": "node --test **/*.spec.js" + }, + "keywords": [], + "author": "Aikido Security", + "license": "AGPL-3.0-or-later", + "type": "module", + "dependencies": { + "node-pty": "^1.0.0" + } +} diff --git a/test/e2e/setup.teardown.e2e.spec.js b/test/e2e/setup.teardown.e2e.spec.js index 16b2669..ad3f264 100644 --- a/test/e2e/setup.teardown.e2e.spec.js +++ b/test/e2e/setup.teardown.e2e.spec.js @@ -7,7 +7,6 @@ import assert from "node:assert"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const projectRoot = path.resolve(__dirname, "../.."); describe("E2E: safe-chain setup command", () => { const imageName = "safe-chain-e2e-test"; @@ -17,8 +16,8 @@ describe("E2E: safe-chain setup command", () => { before(async () => { // Build the Docker image for the test environment try { - execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, { - cwd: projectRoot, + execSync(`docker build -t ${imageName} -f Dockerfile ../../safe-chain`, { + cwd: __dirname, stdio: "ignore", }); } catch (error) { From e7aadc42a3ebcf6fda7957edbc837e404e975e6d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 16:58:51 +0200 Subject: [PATCH 43/57] Add jobname to e2e tests --- .github/workflows/test-on-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index c9302e9..cff72dd 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -33,6 +33,7 @@ jobs: run: npm run lint e2e-tests: + name: Run E2E tests runs-on: ubuntu-latest defaults: run: From 1bbfb5425df1f4c237edacf04f33ffb36819694c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 17:03:17 +0200 Subject: [PATCH 44/57] Remove test package --- .github/workflows/test-on-pr.yml | 9 +++------ safe-chain/package-lock.json | 17 ----------------- safe-chain/package.json | 1 - 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index cff72dd..475a3ad 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -34,10 +34,11 @@ jobs: e2e-tests: name: Run E2E tests + runs-on: ubuntu-latest defaults: run: - working-directory: test/e2e + working-directory: "test/e2e" steps: - name: Checkout code @@ -46,8 +47,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: "24" - cache: "npm" + node-version: "lts/*" - name: Install dependencies run: npm ci @@ -55,9 +55,6 @@ jobs: - name: Run unit tests run: npm test - - name: Run E2E tests - run: npm run test:e2e - - name: Clean up Docker resources if: always() run: | diff --git a/safe-chain/package-lock.json b/safe-chain/package-lock.json index 747aedb..260ee8b 100644 --- a/safe-chain/package-lock.json +++ b/safe-chain/package-lock.json @@ -12,7 +12,6 @@ "@inquirer/prompts": "^7.4.1", "abbrev": "^3.0.1", "chalk": "^5.4.1", - "node-pty": "^1.0.0", "npm-registry-fetch": "^18.0.2", "ora": "^8.2.0", "semver": "^7.7.2" @@ -3906,12 +3905,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3928,16 +3921,6 @@ "node": ">= 0.6" } }, - "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "nan": "^2.17.0" - } - }, "node_modules/npm-package-arg": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", diff --git a/safe-chain/package.json b/safe-chain/package.json index 1fb7788..4ab4c95 100644 --- a/safe-chain/package.json +++ b/safe-chain/package.json @@ -27,7 +27,6 @@ "@inquirer/prompts": "^7.4.1", "abbrev": "^3.0.1", "chalk": "^5.4.1", - "node-pty": "^1.0.0", "npm-registry-fetch": "^18.0.2", "ora": "^8.2.0", "semver": "^7.7.2" From 733887744d10609d62d312a8994537a2839731e3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 17:08:25 +0200 Subject: [PATCH 45/57] Add docs to npm package --- .github/workflows/build-and-release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 455ccd6..cb24501 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -8,6 +8,9 @@ on: jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: safe-chain steps: - name: Checkout code @@ -27,6 +30,12 @@ jobs: version="${{ github.ref_name }}" echo "tag=$version" >> $GITHUB_OUTPUT + - name: Add docs + run: | + cp ../README.md ./README.md + cp ../LICENSE ./LICENSE + cp -r ../docs ./docs + - name: Set the version run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} From 9f5380b7a8247697e5c7d9439eecf028a6b7b48b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 30 Jul 2025 17:21:13 +0200 Subject: [PATCH 46/57] Pin docker images --- test/e2e/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 1e5a3df..4d919ef 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24 as builder +FROM node:24-bookworm as builder ENV CI=true @@ -18,7 +18,7 @@ COPY . . RUN npm --no-git-tag-version version 1.0.0 --allow-same-version RUN npm pack -FROM mcr.microsoft.com/devcontainers/javascript-node as runner +FROM mcr.microsoft.com/devcontainers/javascript-node:22-bookworm as runner WORKDIR /app From d6d80d8f03599343adc0e14f29883df55587695f Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 31 Jul 2025 14:26:14 +0000 Subject: [PATCH 47/57] Update src/shell-integration/startup-scripts/init-fish.fish --- src/shell-integration/startup-scripts/init-fish.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish index 86d7fcc..b6de588 100644 --- a/src/shell-integration/startup-scripts/init-fish.fish +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -55,4 +55,4 @@ function npm end wrapSafeChainCommand "npm" "aikido-npm" $argv -end \ No newline at end of file +end From 2a7e2e3ed8d6c8f502463d9adba6dfd009730088 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Thu, 31 Jul 2025 14:26:29 +0000 Subject: [PATCH 48/57] Update src/shell-integration/startup-scripts/init-pwsh.ps1 --- src/shell-integration/startup-scripts/init-pwsh.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 index 7d85ee1..16acb92 100644 --- a/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -77,4 +77,4 @@ function npm { } Invoke-WrappedCommand "npm" "aikido-npm" $args -} \ No newline at end of file +} From b8ac2e3bc108c53b1be028107324055ccf3d2ec1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 4 Aug 2025 10:19:54 +0200 Subject: [PATCH 49/57] Change text as suggested --- src/shell-integration/startup-scripts/init-posix.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/startup-scripts/init-posix.sh b/src/shell-integration/startup-scripts/init-posix.sh index 3b6d13b..01b23c4 100644 --- a/src/shell-integration/startup-scripts/init-posix.sh +++ b/src/shell-integration/startup-scripts/init-posix.sh @@ -2,7 +2,7 @@ function printSafeChainWarning() { # \033[43;30m is used to set the background color to yellow and text color to black # \033[0m is used to reset the text formatting - printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will be run directly.\n" "$1" + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" # \033[36m is used to set the text color to cyan printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" } From 432769bca068fcae5908f9b6d5a6cb4d7d68f0c2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 4 Aug 2025 10:20:44 +0200 Subject: [PATCH 50/57] Change text as suggested --- src/shell-integration/startup-scripts/init-pwsh.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 index 16acb92..7fb44d6 100644 --- a/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -3,7 +3,7 @@ function Write-SafeChainWarning { # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline - Write-Host " safe-chain is not available to protect you from installing malware. $Command will be run directly." + Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." # Cyan text for the install command Write-Host "Install safe-chain by using " -NoNewline From c5b9722a4024f23aec9da1e47164680ae8d058c1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 4 Aug 2025 10:22:33 +0200 Subject: [PATCH 51/57] Change text as suggested --- src/shell-integration/startup-scripts/init-fish.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish index b6de588..0190883 100644 --- a/src/shell-integration/startup-scripts/init-fish.fish +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -5,7 +5,7 @@ function printSafeChainWarning set_color -b yellow black printf "Warning:" set_color normal - printf " safe-chain is not available to protect you from installing malware. %s will be run directly.\n" $original_cmd + printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd # Cyan text for the install command printf "Install safe-chain by using " From b29bc2e6dce750f884cf59c59d9ca3c6fb601f72 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 11:50:08 +0200 Subject: [PATCH 52/57] Move merged scripts to correct location --- .../src}/shell-integration/startup-scripts/init-fish.fish | 0 .../src}/shell-integration/startup-scripts/init-posix.sh | 0 .../src}/shell-integration/startup-scripts/init-pwsh.ps1 | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {src => safe-chain/src}/shell-integration/startup-scripts/init-fish.fish (100%) rename {src => safe-chain/src}/shell-integration/startup-scripts/init-posix.sh (100%) rename {src => safe-chain/src}/shell-integration/startup-scripts/init-pwsh.ps1 (100%) diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/safe-chain/src/shell-integration/startup-scripts/init-fish.fish similarity index 100% rename from src/shell-integration/startup-scripts/init-fish.fish rename to safe-chain/src/shell-integration/startup-scripts/init-fish.fish diff --git a/src/shell-integration/startup-scripts/init-posix.sh b/safe-chain/src/shell-integration/startup-scripts/init-posix.sh similarity index 100% rename from src/shell-integration/startup-scripts/init-posix.sh rename to safe-chain/src/shell-integration/startup-scripts/init-posix.sh diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 similarity index 100% rename from src/shell-integration/startup-scripts/init-pwsh.ps1 rename to safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 From 8fe228c4761f73849c75ee90e02fdfda71c962b7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 13:35:40 +0200 Subject: [PATCH 53/57] Undo move of files to safe-chain --- .github/workflows/build-and-release.yml | 9 --------- .npmignore | 5 +++++ {safe-chain/bin => bin}/aikido-npm.js | 0 {safe-chain/bin => bin}/aikido-npx.js | 0 {safe-chain/bin => bin}/aikido-pnpm.js | 0 {safe-chain/bin => bin}/aikido-pnpx.js | 0 {safe-chain/bin => bin}/aikido-yarn.js | 0 {safe-chain/bin => bin}/safe-chain.js | 0 safe-chain/eslint.config.js => eslint.config.js | 0 safe-chain/package-lock.json => package-lock.json | 0 safe-chain/package.json => package.json | 0 {safe-chain/src => src}/api/aikido.js | 0 {safe-chain/src => src}/api/npmApi.js | 0 {safe-chain/src => src}/config/configFile.js | 0 {safe-chain/src => src}/environment/environment.js | 0 {safe-chain/src => src}/environment/userInteraction.js | 0 {safe-chain/src => src}/main.js | 0 .../src => src}/packagemanager/_shared/matchesCommand.js | 0 .../src => src}/packagemanager/currentPackageManager.js | 0 .../packagemanager/npm/createPackageManager.js | 0 .../npm/dependencyScanner/commandArgumentScanner.js | 0 .../npm/dependencyScanner/dryRunScanner.js | 0 .../packagemanager/npm/dependencyScanner/nullScanner.js | 0 .../npm/parsing/parseNpmInstallDryRunOutput.js | 0 .../npm/parsing/parseNpmInstallDryRunOutput.spec.js | 0 .../npm/parsing/parsePackagesFromInstallArgs.js | 0 .../npm/parsing/parsePackagesFromInstallArgs.spec.js | 0 .../src => src}/packagemanager/npm/runNpmCommand.js | 0 .../src => src}/packagemanager/npm/utils/cmd-list.js | 0 .../src => src}/packagemanager/npm/utils/npmCommands.js | 0 .../packagemanager/npx/createPackageManager.js | 0 .../npx/dependencyScanner/commandArgumentScanner.js | 0 .../npx/parsing/parsePackagesFromArguments.js | 0 .../npx/parsing/parsePackagesFromArguments.spec.js | 0 .../src => src}/packagemanager/npx/runNpxCommand.js | 0 .../packagemanager/pnpm/createPackageManager.js | 0 .../pnpm/dependencyScanner/commandArgumentScanner.js | 0 .../pnpm/parsing/parsePackagesFromArguments.js | 0 .../pnpm/parsing/parsePackagesFromArguments.spec.js | 0 .../src => src}/packagemanager/pnpm/runPnpmCommand.js | 0 .../packagemanager/yarn/createPackageManager.js | 0 .../yarn/dependencyScanner/commandArgumentScanner.js | 0 .../yarn/parsing/parsePackagesFromArguments.js | 0 .../yarn/parsing/parsePackagesFromArguments.spec.js | 0 .../src => src}/packagemanager/yarn/runYarnCommand.js | 0 {safe-chain/src => src}/scanning/audit/index.js | 0 {safe-chain/src => src}/scanning/index.js | 0 .../src => src}/scanning/index.scanCommand.spec.js | 0 .../src => src}/scanning/index.shouldScanCommand.spec.js | 0 {safe-chain/src => src}/scanning/malwareDatabase.js | 0 {safe-chain/src => src}/shell-integration/helpers.js | 0 {safe-chain/src => src}/shell-integration/setup.js | 0 .../src => src}/shell-integration/shellDetection.js | 0 .../shell-integration/startup-scripts/init-fish.fish | 0 .../shell-integration/startup-scripts/init-posix.sh | 0 .../shell-integration/startup-scripts/init-pwsh.ps1 | 0 .../shell-integration/supported-shells/bash.js | 0 .../shell-integration/supported-shells/bash.spec.js | 0 .../shell-integration/supported-shells/fish.js | 0 .../shell-integration/supported-shells/fish.spec.js | 0 .../shell-integration/supported-shells/powershell.js | 0 .../supported-shells/powershell.spec.js | 0 .../supported-shells/windowsPowershell.js | 0 .../supported-shells/windowsPowershell.spec.js | 0 .../shell-integration/supported-shells/zsh.js | 0 .../shell-integration/supported-shells/zsh.spec.js | 0 {safe-chain/src => src}/shell-integration/teardown.js | 0 test/e2e/setup.teardown.e2e.spec.js | 3 ++- 68 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 .npmignore rename {safe-chain/bin => bin}/aikido-npm.js (100%) rename {safe-chain/bin => bin}/aikido-npx.js (100%) rename {safe-chain/bin => bin}/aikido-pnpm.js (100%) rename {safe-chain/bin => bin}/aikido-pnpx.js (100%) rename {safe-chain/bin => bin}/aikido-yarn.js (100%) rename {safe-chain/bin => bin}/safe-chain.js (100%) rename safe-chain/eslint.config.js => eslint.config.js (100%) rename safe-chain/package-lock.json => package-lock.json (100%) rename safe-chain/package.json => package.json (100%) rename {safe-chain/src => src}/api/aikido.js (100%) rename {safe-chain/src => src}/api/npmApi.js (100%) rename {safe-chain/src => src}/config/configFile.js (100%) rename {safe-chain/src => src}/environment/environment.js (100%) rename {safe-chain/src => src}/environment/userInteraction.js (100%) rename {safe-chain/src => src}/main.js (100%) rename {safe-chain/src => src}/packagemanager/_shared/matchesCommand.js (100%) rename {safe-chain/src => src}/packagemanager/currentPackageManager.js (100%) rename {safe-chain/src => src}/packagemanager/npm/createPackageManager.js (100%) rename {safe-chain/src => src}/packagemanager/npm/dependencyScanner/commandArgumentScanner.js (100%) rename {safe-chain/src => src}/packagemanager/npm/dependencyScanner/dryRunScanner.js (100%) rename {safe-chain/src => src}/packagemanager/npm/dependencyScanner/nullScanner.js (100%) rename {safe-chain/src => src}/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js (100%) rename {safe-chain/src => src}/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js (100%) rename {safe-chain/src => src}/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js (100%) rename {safe-chain/src => src}/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js (100%) rename {safe-chain/src => src}/packagemanager/npm/runNpmCommand.js (100%) rename {safe-chain/src => src}/packagemanager/npm/utils/cmd-list.js (100%) rename {safe-chain/src => src}/packagemanager/npm/utils/npmCommands.js (100%) rename {safe-chain/src => src}/packagemanager/npx/createPackageManager.js (100%) rename {safe-chain/src => src}/packagemanager/npx/dependencyScanner/commandArgumentScanner.js (100%) rename {safe-chain/src => src}/packagemanager/npx/parsing/parsePackagesFromArguments.js (100%) rename {safe-chain/src => src}/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js (100%) rename {safe-chain/src => src}/packagemanager/npx/runNpxCommand.js (100%) rename {safe-chain/src => src}/packagemanager/pnpm/createPackageManager.js (100%) rename {safe-chain/src => src}/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js (100%) rename {safe-chain/src => src}/packagemanager/pnpm/parsing/parsePackagesFromArguments.js (100%) rename {safe-chain/src => src}/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js (100%) rename {safe-chain/src => src}/packagemanager/pnpm/runPnpmCommand.js (100%) rename {safe-chain/src => src}/packagemanager/yarn/createPackageManager.js (100%) rename {safe-chain/src => src}/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js (100%) rename {safe-chain/src => src}/packagemanager/yarn/parsing/parsePackagesFromArguments.js (100%) rename {safe-chain/src => src}/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js (100%) rename {safe-chain/src => src}/packagemanager/yarn/runYarnCommand.js (100%) rename {safe-chain/src => src}/scanning/audit/index.js (100%) rename {safe-chain/src => src}/scanning/index.js (100%) rename {safe-chain/src => src}/scanning/index.scanCommand.spec.js (100%) rename {safe-chain/src => src}/scanning/index.shouldScanCommand.spec.js (100%) rename {safe-chain/src => src}/scanning/malwareDatabase.js (100%) rename {safe-chain/src => src}/shell-integration/helpers.js (100%) rename {safe-chain/src => src}/shell-integration/setup.js (100%) rename {safe-chain/src => src}/shell-integration/shellDetection.js (100%) rename {safe-chain/src => src}/shell-integration/startup-scripts/init-fish.fish (100%) rename {safe-chain/src => src}/shell-integration/startup-scripts/init-posix.sh (100%) rename {safe-chain/src => src}/shell-integration/startup-scripts/init-pwsh.ps1 (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/bash.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/bash.spec.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/fish.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/fish.spec.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/powershell.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/powershell.spec.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/windowsPowershell.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/windowsPowershell.spec.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/zsh.js (100%) rename {safe-chain/src => src}/shell-integration/supported-shells/zsh.spec.js (100%) rename {safe-chain/src => src}/shell-integration/teardown.js (100%) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index cb24501..455ccd6 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -8,9 +8,6 @@ on: jobs: build: runs-on: ubuntu-latest - defaults: - run: - working-directory: safe-chain steps: - name: Checkout code @@ -30,12 +27,6 @@ jobs: version="${{ github.ref_name }}" echo "tag=$version" >> $GITHUB_OUTPUT - - name: Add docs - run: | - cp ../README.md ./README.md - cp ../LICENSE ./LICENSE - cp -r ../docs ./docs - - name: Set the version run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..72f297f --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ + +.github +.claude +test/e2e + diff --git a/safe-chain/bin/aikido-npm.js b/bin/aikido-npm.js similarity index 100% rename from safe-chain/bin/aikido-npm.js rename to bin/aikido-npm.js diff --git a/safe-chain/bin/aikido-npx.js b/bin/aikido-npx.js similarity index 100% rename from safe-chain/bin/aikido-npx.js rename to bin/aikido-npx.js diff --git a/safe-chain/bin/aikido-pnpm.js b/bin/aikido-pnpm.js similarity index 100% rename from safe-chain/bin/aikido-pnpm.js rename to bin/aikido-pnpm.js diff --git a/safe-chain/bin/aikido-pnpx.js b/bin/aikido-pnpx.js similarity index 100% rename from safe-chain/bin/aikido-pnpx.js rename to bin/aikido-pnpx.js diff --git a/safe-chain/bin/aikido-yarn.js b/bin/aikido-yarn.js similarity index 100% rename from safe-chain/bin/aikido-yarn.js rename to bin/aikido-yarn.js diff --git a/safe-chain/bin/safe-chain.js b/bin/safe-chain.js similarity index 100% rename from safe-chain/bin/safe-chain.js rename to bin/safe-chain.js diff --git a/safe-chain/eslint.config.js b/eslint.config.js similarity index 100% rename from safe-chain/eslint.config.js rename to eslint.config.js diff --git a/safe-chain/package-lock.json b/package-lock.json similarity index 100% rename from safe-chain/package-lock.json rename to package-lock.json diff --git a/safe-chain/package.json b/package.json similarity index 100% rename from safe-chain/package.json rename to package.json diff --git a/safe-chain/src/api/aikido.js b/src/api/aikido.js similarity index 100% rename from safe-chain/src/api/aikido.js rename to src/api/aikido.js diff --git a/safe-chain/src/api/npmApi.js b/src/api/npmApi.js similarity index 100% rename from safe-chain/src/api/npmApi.js rename to src/api/npmApi.js diff --git a/safe-chain/src/config/configFile.js b/src/config/configFile.js similarity index 100% rename from safe-chain/src/config/configFile.js rename to src/config/configFile.js diff --git a/safe-chain/src/environment/environment.js b/src/environment/environment.js similarity index 100% rename from safe-chain/src/environment/environment.js rename to src/environment/environment.js diff --git a/safe-chain/src/environment/userInteraction.js b/src/environment/userInteraction.js similarity index 100% rename from safe-chain/src/environment/userInteraction.js rename to src/environment/userInteraction.js diff --git a/safe-chain/src/main.js b/src/main.js similarity index 100% rename from safe-chain/src/main.js rename to src/main.js diff --git a/safe-chain/src/packagemanager/_shared/matchesCommand.js b/src/packagemanager/_shared/matchesCommand.js similarity index 100% rename from safe-chain/src/packagemanager/_shared/matchesCommand.js rename to src/packagemanager/_shared/matchesCommand.js diff --git a/safe-chain/src/packagemanager/currentPackageManager.js b/src/packagemanager/currentPackageManager.js similarity index 100% rename from safe-chain/src/packagemanager/currentPackageManager.js rename to src/packagemanager/currentPackageManager.js diff --git a/safe-chain/src/packagemanager/npm/createPackageManager.js b/src/packagemanager/npm/createPackageManager.js similarity index 100% rename from safe-chain/src/packagemanager/npm/createPackageManager.js rename to src/packagemanager/npm/createPackageManager.js diff --git a/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js rename to src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js diff --git a/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/src/packagemanager/npm/dependencyScanner/dryRunScanner.js similarity index 100% rename from safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js rename to src/packagemanager/npm/dependencyScanner/dryRunScanner.js diff --git a/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js b/src/packagemanager/npm/dependencyScanner/nullScanner.js similarity index 100% rename from safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js rename to src/packagemanager/npm/dependencyScanner/nullScanner.js diff --git a/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js b/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js similarity index 100% rename from safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js rename to src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js diff --git a/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js b/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js similarity index 100% rename from safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js rename to src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js diff --git a/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js b/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js similarity index 100% rename from safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js rename to src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js diff --git a/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js b/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js similarity index 100% rename from safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js rename to src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js diff --git a/safe-chain/src/packagemanager/npm/runNpmCommand.js b/src/packagemanager/npm/runNpmCommand.js similarity index 100% rename from safe-chain/src/packagemanager/npm/runNpmCommand.js rename to src/packagemanager/npm/runNpmCommand.js diff --git a/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/src/packagemanager/npm/utils/cmd-list.js similarity index 100% rename from safe-chain/src/packagemanager/npm/utils/cmd-list.js rename to src/packagemanager/npm/utils/cmd-list.js diff --git a/safe-chain/src/packagemanager/npm/utils/npmCommands.js b/src/packagemanager/npm/utils/npmCommands.js similarity index 100% rename from safe-chain/src/packagemanager/npm/utils/npmCommands.js rename to src/packagemanager/npm/utils/npmCommands.js diff --git a/safe-chain/src/packagemanager/npx/createPackageManager.js b/src/packagemanager/npx/createPackageManager.js similarity index 100% rename from safe-chain/src/packagemanager/npx/createPackageManager.js rename to src/packagemanager/npx/createPackageManager.js diff --git a/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js rename to src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js diff --git a/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js b/src/packagemanager/npx/parsing/parsePackagesFromArguments.js similarity index 100% rename from safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js rename to src/packagemanager/npx/parsing/parsePackagesFromArguments.js diff --git a/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js b/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js rename to src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js diff --git a/safe-chain/src/packagemanager/npx/runNpxCommand.js b/src/packagemanager/npx/runNpxCommand.js similarity index 100% rename from safe-chain/src/packagemanager/npx/runNpxCommand.js rename to src/packagemanager/npx/runNpxCommand.js diff --git a/safe-chain/src/packagemanager/pnpm/createPackageManager.js b/src/packagemanager/pnpm/createPackageManager.js similarity index 100% rename from safe-chain/src/packagemanager/pnpm/createPackageManager.js rename to src/packagemanager/pnpm/createPackageManager.js diff --git a/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js b/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js rename to src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js diff --git a/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js similarity index 100% rename from safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js rename to src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js diff --git a/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js b/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js rename to src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js diff --git a/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/src/packagemanager/pnpm/runPnpmCommand.js similarity index 100% rename from safe-chain/src/packagemanager/pnpm/runPnpmCommand.js rename to src/packagemanager/pnpm/runPnpmCommand.js diff --git a/safe-chain/src/packagemanager/yarn/createPackageManager.js b/src/packagemanager/yarn/createPackageManager.js similarity index 100% rename from safe-chain/src/packagemanager/yarn/createPackageManager.js rename to src/packagemanager/yarn/createPackageManager.js diff --git a/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js b/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js similarity index 100% rename from safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js rename to src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js diff --git a/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js b/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js similarity index 100% rename from safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js rename to src/packagemanager/yarn/parsing/parsePackagesFromArguments.js diff --git a/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js b/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js similarity index 100% rename from safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js rename to src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js diff --git a/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/src/packagemanager/yarn/runYarnCommand.js similarity index 100% rename from safe-chain/src/packagemanager/yarn/runYarnCommand.js rename to src/packagemanager/yarn/runYarnCommand.js diff --git a/safe-chain/src/scanning/audit/index.js b/src/scanning/audit/index.js similarity index 100% rename from safe-chain/src/scanning/audit/index.js rename to src/scanning/audit/index.js diff --git a/safe-chain/src/scanning/index.js b/src/scanning/index.js similarity index 100% rename from safe-chain/src/scanning/index.js rename to src/scanning/index.js diff --git a/safe-chain/src/scanning/index.scanCommand.spec.js b/src/scanning/index.scanCommand.spec.js similarity index 100% rename from safe-chain/src/scanning/index.scanCommand.spec.js rename to src/scanning/index.scanCommand.spec.js diff --git a/safe-chain/src/scanning/index.shouldScanCommand.spec.js b/src/scanning/index.shouldScanCommand.spec.js similarity index 100% rename from safe-chain/src/scanning/index.shouldScanCommand.spec.js rename to src/scanning/index.shouldScanCommand.spec.js diff --git a/safe-chain/src/scanning/malwareDatabase.js b/src/scanning/malwareDatabase.js similarity index 100% rename from safe-chain/src/scanning/malwareDatabase.js rename to src/scanning/malwareDatabase.js diff --git a/safe-chain/src/shell-integration/helpers.js b/src/shell-integration/helpers.js similarity index 100% rename from safe-chain/src/shell-integration/helpers.js rename to src/shell-integration/helpers.js diff --git a/safe-chain/src/shell-integration/setup.js b/src/shell-integration/setup.js similarity index 100% rename from safe-chain/src/shell-integration/setup.js rename to src/shell-integration/setup.js diff --git a/safe-chain/src/shell-integration/shellDetection.js b/src/shell-integration/shellDetection.js similarity index 100% rename from safe-chain/src/shell-integration/shellDetection.js rename to src/shell-integration/shellDetection.js diff --git a/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish similarity index 100% rename from safe-chain/src/shell-integration/startup-scripts/init-fish.fish rename to src/shell-integration/startup-scripts/init-fish.fish diff --git a/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/src/shell-integration/startup-scripts/init-posix.sh similarity index 100% rename from safe-chain/src/shell-integration/startup-scripts/init-posix.sh rename to src/shell-integration/startup-scripts/init-posix.sh diff --git a/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 similarity index 100% rename from safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 rename to src/shell-integration/startup-scripts/init-pwsh.ps1 diff --git a/safe-chain/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/bash.js rename to src/shell-integration/supported-shells/bash.js diff --git a/safe-chain/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/bash.spec.js rename to src/shell-integration/supported-shells/bash.spec.js diff --git a/safe-chain/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/fish.js rename to src/shell-integration/supported-shells/fish.js diff --git a/safe-chain/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/fish.spec.js rename to src/shell-integration/supported-shells/fish.spec.js diff --git a/safe-chain/src/shell-integration/supported-shells/powershell.js b/src/shell-integration/supported-shells/powershell.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/powershell.js rename to src/shell-integration/supported-shells/powershell.js diff --git a/safe-chain/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/powershell.spec.js rename to src/shell-integration/supported-shells/powershell.spec.js diff --git a/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/windowsPowershell.js rename to src/shell-integration/supported-shells/windowsPowershell.js diff --git a/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js rename to src/shell-integration/supported-shells/windowsPowershell.spec.js diff --git a/safe-chain/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/zsh.js rename to src/shell-integration/supported-shells/zsh.js diff --git a/safe-chain/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js similarity index 100% rename from safe-chain/src/shell-integration/supported-shells/zsh.spec.js rename to src/shell-integration/supported-shells/zsh.spec.js diff --git a/safe-chain/src/shell-integration/teardown.js b/src/shell-integration/teardown.js similarity index 100% rename from safe-chain/src/shell-integration/teardown.js rename to src/shell-integration/teardown.js diff --git a/test/e2e/setup.teardown.e2e.spec.js b/test/e2e/setup.teardown.e2e.spec.js index ad3f264..804bf29 100644 --- a/test/e2e/setup.teardown.e2e.spec.js +++ b/test/e2e/setup.teardown.e2e.spec.js @@ -16,7 +16,8 @@ describe("E2E: safe-chain setup command", () => { before(async () => { // Build the Docker image for the test environment try { - execSync(`docker build -t ${imageName} -f Dockerfile ../../safe-chain`, { + const sourceDir = path.join(__dirname, "../.."); + execSync(`docker build -t ${imageName} -f Dockerfile ${sourceDir}`, { cwd: __dirname, stdio: "ignore", }); From 7dd93d490eb46eb93311ed318221ff1fdf1bf77c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 13:37:47 +0200 Subject: [PATCH 54/57] Fix unit tests --- .github/workflows/test-on-pr.yml | 3 --- package.json | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 475a3ad..e291a50 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -10,9 +10,6 @@ jobs: name: Run unit tests and linting runs-on: ubuntu-latest - defaults: - run: - working-directory: safe-chain steps: - name: Checkout code diff --git a/package.json b/package.json index 4ab4c95..ab5cf00 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "@aikidosec/safe-chain", "version": "1.0.0", "scripts": { - "test": "node --test --experimental-test-module-mocks **/*.spec.js", - "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js", + "test": "node --test --experimental-test-module-mocks src/**/*.spec.js", + "test:watch": "node --test --watch --experimental-test-module-mocks src/**/*.spec.js", "lint": "eslint ." }, "repository": { From 24a9cd94e5c027f94df4feced70ca92fefbe2ba7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 13:39:09 +0200 Subject: [PATCH 55/57] Fix linter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab5cf00..89c7277 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks src/**/*.spec.js", "test:watch": "node --test --watch --experimental-test-module-mocks src/**/*.spec.js", - "lint": "eslint ." + "lint": "eslint src" }, "repository": { "type": "git", From 956acc477547822da28442e0220f9c1565fa0c93 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 13:43:27 +0200 Subject: [PATCH 56/57] Make sure all tests are run --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 89c7277..c269bf7 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "@aikidosec/safe-chain", "version": "1.0.0", "scripts": { - "test": "node --test --experimental-test-module-mocks src/**/*.spec.js", - "test:watch": "node --test --watch --experimental-test-module-mocks src/**/*.spec.js", + "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", + "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", "lint": "eslint src" }, "repository": { From 35f43f6787be0dfe614477fd930040f989ba9274 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 5 Aug 2025 13:51:39 +0200 Subject: [PATCH 57/57] Exclude test/e2e from default linter --- eslint.config.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 27f6599..b210b69 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,5 @@ import js from "@eslint/js"; -import { defineConfig } from "@eslint/config-helpers"; +import { defineConfig, globalIgnores } from "@eslint/config-helpers"; import globals from "globals"; import importPlugin from "eslint-plugin-import"; @@ -22,4 +22,5 @@ export default defineConfig([ }, rules: {}, }, + globalIgnores(['test/e2e']), ]); diff --git a/package.json b/package.json index c269bf7..20278fa 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "eslint src" + "lint": "eslint ." }, "repository": { "type": "git",