diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index ada658b..792aa3c 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -81,7 +81,7 @@ function setupShell(shell) { } function copyStartupFiles() { - const startupFiles = ["init-posix.sh", "init-pwsh.ps1"]; + const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; for (const file of startupFiles) { const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish new file mode 100644 index 0000000..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-pwsh.ps1 b/src/shell-integration/startup-scripts/init-pwsh.ps1 index 16acb92..7fb44d6 100644 --- a/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -3,7 +3,7 @@ function Write-SafeChainWarning { # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline - Write-Host " safe-chain is not available to protect you from installing malware. $Command will be run directly." + Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." # Cyan text for the install command Write-Host "Install safe-chain by using " -NoNewline 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"); }); }); });