diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index e5be973..792aa3c 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -2,6 +2,10 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; import { knownAikidoTools } from "./helpers.js"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; /** * Loops over the detected shells and calls the setup function for each. @@ -13,6 +17,8 @@ export async function setup() { ); ui.emptyLine(); + copyStartupFiles(); + try { const shells = detectShells(); if (shells.length === 0) { @@ -73,3 +79,22 @@ function setupShell(shell) { return success; } + +function copyStartupFiles() { + const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; + + for (const file of startupFiles) { + const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); + const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Use absolute path for source + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const sourcePath = path.resolve(__dirname, "startup-scripts", file); + fs.copyFileSync(sourcePath, targetPath); + } +} diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish new file mode 100644 index 0000000..0190883 --- /dev/null +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -0,0 +1,58 @@ +function printSafeChainWarning + set original_cmd $argv[1] + + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" + set_color -b yellow black + printf "Warning:" + set_color normal + printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd + + # Cyan text for the install command + printf "Install safe-chain by using " + set_color cyan + printf "npm install -g @aikidosec/safe-chain" + set_color normal + printf ".\n" +end + +function wrapSafeChainCommand + set original_cmd $argv[1] + set aikido_cmd $argv[2] + set cmd_args $argv[3..-1] + + if type -q $aikido_cmd + # If the aikido command is available, just run it with the provided arguments + $aikido_cmd $cmd_args + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning $original_cmd + command $original_cmd $cmd_args + end +end + +function npx + wrapSafeChainCommand "npx" "aikido-npx" $argv +end + +function yarn + wrapSafeChainCommand "yarn" "aikido-yarn" $argv +end + +function pnpm + wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv +end + +function pnpx + wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv +end + +function npm + if test (count $argv) -eq 1 -a \( "$argv[1]" = "-v" -o "$argv[1]" = "--version" \) + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + command npm $argv + return + end + + wrapSafeChainCommand "npm" "aikido-npm" $argv +end diff --git a/src/shell-integration/startup-scripts/init-posix.sh b/src/shell-integration/startup-scripts/init-posix.sh new file mode 100644 index 0000000..01b23c4 --- /dev/null +++ b/src/shell-integration/startup-scripts/init-posix.sh @@ -0,0 +1,54 @@ + +function printSafeChainWarning() { + # \033[43;30m is used to set the background color to yellow and text color to black + # \033[0m is used to reset the text formatting + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" + # \033[36m is used to set the text color to cyan + printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" +} + +function wrapSafeChainCommand() { + local original_cmd="$1" + local aikido_cmd="$2" + + # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ + # so that "$@" now contains only the arguments passed to the original command + shift 2 + + if command -v "$aikido_cmd" > /dev/null 2>&1; then + # If the aikido command is available, just run it with the provided arguments + "$aikido_cmd" "$@" + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning "$original_cmd" + + command "$original_cmd" "$@" + fi +} + +function npx() { + wrapSafeChainCommand "npx" "aikido-npx" "$@" +} + +function yarn() { + wrapSafeChainCommand "yarn" "aikido-yarn" "$@" +} + +function pnpm() { + wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@" +} + +function pnpx() { + wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" +} + +function npm() { + if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + command npm "$@" + return + fi + + wrapSafeChainCommand "npm" "aikido-npm" "$@" +} diff --git a/src/shell-integration/startup-scripts/init-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 new file mode 100644 index 0000000..7fb44d6 --- /dev/null +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -0,0 +1,80 @@ +function Write-SafeChainWarning { + param([string]$Command) + + # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" + Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline + Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." + + # Cyan text for the install command + Write-Host "Install safe-chain by using " -NoNewline + Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline + Write-Host "." +} + +function Test-CommandAvailable { + param([string]$Command) + + try { + Get-Command $Command -ErrorAction Stop | Out-Null + return $true + } + catch { + return $false + } +} + +function Invoke-RealCommand { + param( + [string]$Command, + [string[]]$Arguments + ) + + # Find the real executable to avoid calling our wrapped functions + $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 + if ($realCommand) { + & $realCommand.Source @Arguments + } +} + +function Invoke-WrappedCommand { + param( + [string]$OriginalCmd, + [string]$AikidoCmd, + [string[]]$Arguments + ) + + if (Test-CommandAvailable $AikidoCmd) { + & $AikidoCmd @Arguments + } + else { + Write-SafeChainWarning $OriginalCmd + Invoke-RealCommand $OriginalCmd $Arguments + } +} + +function npx { + Invoke-WrappedCommand "npx" "aikido-npx" $args +} + +function yarn { + Invoke-WrappedCommand "yarn" "aikido-yarn" $args +} + +function pnpm { + Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args +} + +function pnpx { + Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args +} + +function npm { + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) { + Invoke-RealCommand "npm" $args + return + } + + Invoke-WrappedCommand "npm" "aikido-npm" $args +} diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js index 66b844d..3c4b1f9 100644 --- a/src/shell-integration/supported-shells/bash.js +++ b/src/shell-integration/supported-shells/bash.js @@ -21,18 +21,22 @@ function teardown(tools) { removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); } + // Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js index ce666e5..e23addb 100644 --- a/src/shell-integration/supported-shells/bash.spec.js +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -66,37 +66,16 @@ describe("Bash shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = bash.setup(tools); + it("should add source line for bash initialization script", () => { + const result = bash.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('alias npm="aikido-npm" # Safe-chain alias for npm') + content.includes( + "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" + ) ); - assert.ok( - content.includes('alias npx="aikido-npx" # Safe-chain alias for npx') - ); - assert.ok( - content.includes('alias yarn="aikido-yarn" # Safe-chain alias for yarn') - ); - }); - - it("should handle empty tools array", () => { - const result = bash.setup([]); - assert.strictEqual(result, true); - - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } }); }); @@ -174,14 +153,14 @@ describe("Bash shell integration", () => { // Setup bash.setup(tools); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm="aikido-npm"')); - assert.ok(content.includes('alias yarn="aikido-yarn"')); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); // Teardown bash.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm=")); - assert.ok(!content.includes("alias yarn=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); }); it("should handle multiple setup calls", () => { @@ -192,8 +171,29 @@ describe("Bash shell integration", () => { bash.setup(tools); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/alias npm="/g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/source.*init-posix\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); + }); + + it("should handle mixed content with aliases and source lines", () => { + const initialContent = [ + "#!/bin/bash", + "alias npm='old-npm'", + "source ~/.safe-chain/scripts/init-posix.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + // Teardown should remove both aliases and source line + bash.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); + assert.ok(content.includes("alias ls=")); }); }); }); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index fc6fc85..7b2c683 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool} "${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index 5f1ab64..e138957 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -66,47 +66,34 @@ describe("Fish shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = fish.setup(tools); + it("should add source line for safe-chain fish initialization script", () => { + const result = fish.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('alias npm "aikido-npm" # Safe-chain alias for npm') - ); - assert.ok( - content.includes('alias npx "aikido-npx" # Safe-chain alias for npx') - ); - assert.ok( - content.includes('alias yarn "aikido-yarn" # Safe-chain alias for yarn') + content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script') ); }); - it("should handle empty tools array", () => { - const result = fish.setup([]); - assert.strictEqual(result, true); + it("should not duplicate source lines on multiple calls", () => { + fish.setup(); + fish.setup(); - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } + const content = fs.readFileSync(mockStartupFile, "utf-8"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)"); }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove npm, npx, yarn aliases and source line", () => { const initialContent = [ "#!/usr/bin/env fish", "alias npm 'aikido-npm'", "alias npx 'aikido-npx'", "alias yarn 'aikido-yarn'", + "source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", "alias ls 'ls --color=auto'", "alias grep 'grep --color=auto'", ].join("\n"); @@ -120,6 +107,7 @@ describe("Fish shell integration", () => { assert.ok(!content.includes("alias npm ")); assert.ok(!content.includes("alias npx ")); assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); assert.ok(content.includes("alias ls ")); assert.ok(content.includes("alias grep ")); }); @@ -133,7 +121,7 @@ describe("Fish shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant aliases or source lines", () => { const initialContent = [ "#!/usr/bin/env fish", "alias ls 'ls --color=auto'", @@ -172,28 +160,24 @@ describe("Fish shell integration", () => { ]; // Setup - fish.setup(tools); + fish.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm "aikido-npm"')); - assert.ok(content.includes('alias yarn "aikido-yarn"')); + assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish')); // Teardown fish.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm ")); - assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - fish.setup(tools); - fish.teardown(tools); - fish.setup(tools); + fish.setup(); + fish.teardown(knownAikidoTools); + fish.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/alias npm "/g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle"); }); }); }); diff --git a/src/shell-integration/supported-shells/powershell.js b/src/shell-integration/supported-shells/powershell.js index 4690bb6..47524c2 100644 --- a/src/shell-integration/supported-shells/powershell.js +++ b/src/shell-integration/supported-shells/powershell.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Remove the line that sources the safe-chain PowerShell initialization script + removeLinesMatchingPattern( + startupFile, + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js index 9afade7..3a15376 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -69,49 +69,47 @@ describe("PowerShell Core shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = powershell.setup(tools); + it("should add init-pwsh.ps1 source line", () => { + const result = powershell.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok( - content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm") - ); - 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" + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' ) ); }); - - it("should handle empty tools array", () => { - const result = powershell.setup([]); - assert.strictEqual(result, true); - - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } - }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# PowerShell profile", - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn", + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = powershell.teardown(knownAikidoTools); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should remove old-style aliases from earlier versions", () => { + const initialContent = [ + "# PowerShell profile", + "Set-Alias npm aikido-npm # Safe-chain alias for npm", + "Set-Alias npx aikido-npx # Safe-chain alias for npx", + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn", "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -138,7 +136,7 @@ describe("PowerShell Core shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant content", () => { const initialContent = [ "# PowerShell profile", "Set-Alias ls Get-ChildItem", @@ -171,34 +169,32 @@ describe("PowerShell Core shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - // Setup - powershell.setup(tools); + powershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("Set-Alias npm aikido-npm")); - assert.ok(content.includes("Set-Alias yarn aikido-yarn")); + assert.ok( + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); // Teardown - powershell.teardown(tools); + powershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("Set-Alias npm ")); - assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - powershell.setup(tools); - powershell.teardown(tools); - powershell.setup(tools); + powershell.setup(); + powershell.teardown(knownAikidoTools); + powershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/Set-Alias npm /g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = ( + content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + [] + ).length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); }); diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js index 118a0b9..03ff7f8 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.js +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Remove the line that sources the safe-chain PowerShell initialization script + removeLinesMatchingPattern( + startupFile, + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js index 85da9f1..c201c60 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -69,49 +69,47 @@ describe("Windows PowerShell shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = windowsPowershell.setup(tools); + it("should add init-pwsh.ps1 source line", () => { + const result = windowsPowershell.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok( - content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm") - ); - 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" + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' ) ); }); - - it("should handle empty tools array", () => { - const result = windowsPowershell.setup([]); - assert.strictEqual(result, true); - - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } - }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# Windows PowerShell profile", - "Set-Alias npm aikido-npm", - "Set-Alias npx aikido-npx", - "Set-Alias yarn aikido-yarn", + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + "Set-Alias ls Get-ChildItem", + "Set-Alias grep Select-String", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = windowsPowershell.teardown(knownAikidoTools); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); + assert.ok(content.includes("Set-Alias ls ")); + assert.ok(content.includes("Set-Alias grep ")); + }); + + it("should remove old-style aliases from earlier versions", () => { + const initialContent = [ + "# Windows PowerShell profile", + "Set-Alias npm aikido-npm # Safe-chain alias for npm", + "Set-Alias npx aikido-npx # Safe-chain alias for npx", + "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn", "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -138,7 +136,7 @@ describe("Windows PowerShell shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant content", () => { const initialContent = [ "# Windows PowerShell profile", "Set-Alias ls Get-ChildItem", @@ -171,34 +169,32 @@ describe("Windows PowerShell shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - // Setup - windowsPowershell.setup(tools); + windowsPowershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("Set-Alias npm aikido-npm")); - assert.ok(content.includes("Set-Alias yarn aikido-yarn")); + assert.ok( + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); // Teardown - windowsPowershell.teardown(tools); + windowsPowershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("Set-Alias npm ")); - assert.ok(!content.includes("Set-Alias yarn ")); + assert.ok( + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + ); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - windowsPowershell.setup(tools); - windowsPowershell.teardown(tools); - windowsPowershell.setup(tools); + windowsPowershell.setup(); + windowsPowershell.teardown(knownAikidoTools); + windowsPowershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/Set-Alias npm /g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = ( + content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + [] + ).length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); }); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index 965c814..b5167fe 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -21,18 +21,22 @@ function teardown(tools) { removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`)); } + // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js index e284c50..95c12ac 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -66,37 +66,24 @@ describe("Zsh shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = zsh.setup(tools); + it("should add source line for zsh initialization script", () => { + const result = zsh.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('alias npm="aikido-npm" # Safe-chain alias for npm') - ); - assert.ok( - content.includes('alias npx="aikido-npx" # Safe-chain alias for npx') - ); - assert.ok( - content.includes('alias yarn="aikido-yarn" # Safe-chain alias for yarn') + content.includes( + "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script" + ) ); }); - it("should handle empty tools array", () => { - const result = zsh.setup([]); + it("should handle empty startup file", () => { + const result = zsh.setup(); assert.strictEqual(result, true); - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); }); }); @@ -124,6 +111,25 @@ describe("Zsh shell integration", () => { assert.ok(content.includes("alias grep=")); }); + it("should remove zsh initialization script source line", () => { + const initialContent = [ + "#!/bin/zsh", + "source ~/.safe-chain/scripts/init-posix.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + const result = zsh.teardown(knownAikidoTools); + assert.strictEqual(result, true); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); + assert.ok(content.includes("alias ls=")); + }); + it("should handle file that doesn't exist", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); @@ -133,7 +139,7 @@ describe("Zsh shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant aliases or source lines", () => { const initialContent = [ "#!/bin/zsh", "alias ls='ls --color=auto'", @@ -172,16 +178,16 @@ describe("Zsh shell integration", () => { ]; // Setup - zsh.setup(tools); + zsh.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm="aikido-npm"')); - assert.ok(content.includes('alias yarn="aikido-yarn"')); + assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); // Teardown zsh.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm=")); - assert.ok(!content.includes("alias yarn=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); }); it("should handle multiple setup calls", () => { @@ -192,8 +198,29 @@ describe("Zsh shell integration", () => { 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"); + const sourceMatches = (content.match(/source.*init-posix\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); + }); + + it("should handle mixed content with aliases and source lines", () => { + const initialContent = [ + "#!/bin/zsh", + "alias npm='old-npm'", + "source ~/.safe-chain/scripts/init-posix.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + // Teardown should remove both aliases and source line + zsh.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok( + !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + ); + assert.ok(content.includes("alias ls=")); }); }); });