From 36c195f5a90857c27a8f30b7d9f0bd89087adbf9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 18 Jul 2025 14:23:51 +0200 Subject: [PATCH] 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");