From fe1ca396b437e0cc70ae173ba9d7fac13cd6c854 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:35:03 +0200 Subject: [PATCH 1/8] 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 3825b94a0987219475d4ea68c9c0c71188227176 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:59:01 +0200 Subject: [PATCH 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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" }