diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index ada658b..792aa3c 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -81,7 +81,7 @@ function setupShell(shell) { } function copyStartupFiles() { - const startupFiles = ["init-posix.sh", "init-pwsh.ps1"]; + const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; for (const file of startupFiles) { const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); diff --git a/src/shell-integration/startup-scripts/init-fish.fish b/src/shell-integration/startup-scripts/init-fish.fish new file mode 100644 index 0000000..3a6c18e --- /dev/null +++ b/src/shell-integration/startup-scripts/init-fish.fish @@ -0,0 +1,85 @@ +function installIfCommandNotFound + set cmd $argv[1] + + # Check if the command already exists + if type -q $cmd + return 0 + end + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + set node_version (node -v | sed 's/v//' | cut -d'.' -f1) + if test $node_version -lt 18 + return 2 + end + + # Command not found, ask user if they want to install safe-chain + read -l response -P "The command '$cmd' is not available. Do you want to install safe-chain to provide it? (y/N): " + + if string match -qi 'y*' $response + printf "Installing safe-chain...\n" + installSafeChain + + if test $status -ne 0 + printf "\nFailed to install safe-chain. Exiting.\n" + return 1 + end + + return 0 + else + printf "Skipping safe-chain installation. Using original command instead.\n" + return 2 + end +end + +function installSafeChain + command npm install -g @aikidosec/safe-chain + + if test $status -ne 0 + return 1 + end + + printf "------\n" +end + +function wrapCommand + set original_cmd $argv[1] + set aikido_cmd $argv[2] + set cmd_args $argv[3..-1] + + installIfCommandNotFound $aikido_cmd + set install_result $status + + if test $install_result -eq 2 + command $original_cmd $cmd_args + else + $aikido_cmd $cmd_args + end +end + +function npx + wrapCommand "npx" "aikido-npx" $argv +end + +function yarn + wrapCommand "yarn" "aikido-yarn" $argv +end + +function pnpm + wrapCommand "pnpm" "aikido-pnpm" $argv +end + +function pnpx + wrapCommand "pnpx" "aikido-pnpx" $argv +end + +function npm + if test (count $argv) -eq 1 -a \( "$argv[1]" = "-v" -o "$argv[1]" = "--version" \) + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + command npm $argv + return + end + + wrapCommand "npm" "aikido-npm" $argv +end \ No newline at end of file diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js index fc6fc85..7b2c683 100644 --- a/src/shell-integration/supported-shells/fish.js +++ b/src/shell-integration/supported-shells/fish.js @@ -24,18 +24,22 @@ function teardown(tools) { ); } + // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = getStartupFile(); - for (const { tool, aikidoCommand } of tools) { - addLineToFile( - startupFile, - `alias ${tool} "${aikidoCommand}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script` + ); return true; } diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js index 5f1ab64..e138957 100644 --- a/src/shell-integration/supported-shells/fish.spec.js +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -66,47 +66,34 @@ describe("Fish shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - ]; - - const result = fish.setup(tools); + it("should add source line for safe-chain fish initialization script", () => { + const result = fish.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('alias npm "aikido-npm" # Safe-chain alias for npm') - ); - assert.ok( - content.includes('alias npx "aikido-npx" # Safe-chain alias for npx') - ); - assert.ok( - content.includes('alias yarn "aikido-yarn" # Safe-chain alias for yarn') + content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script') ); }); - it("should handle empty tools array", () => { - const result = fish.setup([]); - assert.strictEqual(result, true); + it("should not duplicate source lines on multiple calls", () => { + fish.setup(); + fish.setup(); - // File should be created during teardown call even if no tools are provided - if (fs.existsSync(mockStartupFile)) { - const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.strictEqual(content.trim(), ""); - } + const content = fs.readFileSync(mockStartupFile, "utf-8"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)"); }); }); describe("teardown", () => { - it("should remove npm, npx, and yarn aliases", () => { + it("should remove npm, npx, yarn aliases and source line", () => { const initialContent = [ "#!/usr/bin/env fish", "alias npm 'aikido-npm'", "alias npx 'aikido-npx'", "alias yarn 'aikido-yarn'", + "source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", "alias ls 'ls --color=auto'", "alias grep 'grep --color=auto'", ].join("\n"); @@ -120,6 +107,7 @@ describe("Fish shell integration", () => { assert.ok(!content.includes("alias npm ")); assert.ok(!content.includes("alias npx ")); assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); assert.ok(content.includes("alias ls ")); assert.ok(content.includes("alias grep ")); }); @@ -133,7 +121,7 @@ describe("Fish shell integration", () => { assert.strictEqual(result, true); }); - it("should handle file with no relevant aliases", () => { + it("should handle file with no relevant aliases or source lines", () => { const initialContent = [ "#!/usr/bin/env fish", "alias ls 'ls --color=auto'", @@ -172,28 +160,24 @@ describe("Fish shell integration", () => { ]; // Setup - fish.setup(tools); + fish.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('alias npm "aikido-npm"')); - assert.ok(content.includes('alias yarn "aikido-yarn"')); + assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish')); // Teardown fish.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("alias npm ")); - assert.ok(!content.includes("alias yarn ")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); }); it("should handle multiple setup calls", () => { - const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }]; - - fish.setup(tools); - fish.teardown(tools); - fish.setup(tools); + fish.setup(); + fish.teardown(knownAikidoTools); + fish.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const npmMatches = (content.match(/alias npm "/g) || []).length; - assert.strictEqual(npmMatches, 1, "Should not duplicate aliases"); + const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle"); }); }); });