From 09300eade6a4c16d30d8409853910f489f5c1b5d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 17 Jul 2025 16:40:09 +0200 Subject: [PATCH] Zsh: check if safe-chain is installed before running it. --- src/shell-integration/setup.js | 26 +++++ .../startup-scripts/init-zsh.sh | 81 ++++++++++++++++ src/shell-integration/supported-shells/zsh.js | 18 ++-- .../supported-shells/zsh.spec.js | 97 +++++++++++-------- 4 files changed, 177 insertions(+), 45 deletions(-) create mode 100644 src/shell-integration/startup-scripts/init-zsh.sh diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 85d319b..ce6a638 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) { @@ -72,3 +78,23 @@ function setupShell(shell) { return success; } + +function copyStartupFiles() { + const startupFiles = ["init-zsh.sh"]; + + for (const file of startupFiles) { + const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); + + // Create target directory if it doesn't exist + const targetDir = targetPath.substring(0, targetPath.lastIndexOf("/")); + 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-zsh.sh b/src/shell-integration/startup-scripts/init-zsh.sh new file mode 100644 index 0000000..b936eeb --- /dev/null +++ b/src/shell-integration/startup-scripts/init-zsh.sh @@ -0,0 +1,81 @@ + +function installIfCommandNotFound() { + local cmd="$1" + + # Check if the command already exists + if command -v "$cmd" > /dev/null 2>&1; then + return 0 + fi + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + local node_version=$(node -v | sed 's/v//' | cut -d'.' -f1) + if [ "$node_version" -lt 18 ]; then + return 2 + fi + + # Command not found, ask user if they want to install safe-chain + printf "The command '%s' is not available. Do you want to install safe-chain to provide it? (y/N): " "$cmd" + read -r response + + if [[ "$response" =~ ^[Yy]$ ]]; then + printf "Installing safe-chain...\n" + installSafeChain + + if [ $? -ne 0 ]; then + printf "\nFailed to install safe-chain. Exiting.\n" + return 1 + fi + + return 0 + else + printf "Skipping safe-chain installation. Using original command instead.\n" + return 2 + fi +} + +function installSafeChain() { + command npm install -g @aikidosec/safe-chain + + if [ $? -ne 0 ]; then + return 1 + fi + + printf "------\n" +} + +function wrapCommand() { + 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 + + installIfCommandNotFound "$aikido_cmd" + local install_result=$? + if [ $install_result -eq 2 ]; then + command "$original_cmd" "$@" + else + "$aikido_cmd" "$@" + fi +} + +function npx() { + wrapCommand "npx" "aikido-npx" "$@" +} + +function yarn() { + wrapCommand "yarn" "aikido-yarn" "$@" +} + +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 + + wrapCommand "npm" "aikido-npm" "$@" +} diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js index 9943634..d9f63b0 100644 --- a/src/shell-integration/supported-shells/zsh.js +++ b/src/shell-integration/supported-shells/zsh.js @@ -20,19 +20,23 @@ function teardown() { // This will remove the safe-chain aliases for npm, npx, and yarn commands. removeLinesMatchingPattern(startupFile, /^alias\s+(npm|npx|yarn)=/); + // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-zsh.sh) + removeLinesMatchingPattern( + startupFile, + /^source\s+~\/\.safe-chain\/scripts\/init-zsh\.sh/ + ); + return true; } -function setup(tools) { +function setup() { const startupFile = execAndGetOutput(startupFileCommand, executableName); teardown(); - for (const tool of tools) { - addLineToFile( - startupFile, - `alias ${tool}="aikido-${tool}" # Safe-chain alias for ${tool}` - ); - } + addLineToFile( + startupFile, + `source ~/.safe-chain/scripts/init-zsh.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 1e2f0bd..327a914 100644 --- a/src/shell-integration/supported-shells/zsh.spec.js +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -59,49 +59,40 @@ describe("Zsh shell integration", () => { }); describe("setup", () => { - it("should add aliases for all provided tools", () => { - const tools = ["npm", "npx", "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-zsh.sh # Safe-chain Zsh initialization script" + ) ); }); it("should call teardown before setup", () => { - // Pre-populate file with existing aliases + // Pre-populate file with existing source line fs.writeFileSync( mockStartupFile, - 'alias npm="old-npm"\nalias npx="old-npx"\n', + "source ~/.safe-chain/scripts/init-zsh.sh\n", "utf-8" ); - const tools = ["npm"]; - zsh.setup(tools); + zsh.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes('alias npm="old-npm"')); - assert.ok(content.includes('alias npm="aikido-npm"')); + const sourceMatches = (content.match(/source.*init-zsh\.sh/g) || []) + .length; + assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); - 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-zsh.sh")); }); }); @@ -129,6 +120,23 @@ 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-zsh.sh", + "alias ls='ls --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("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("alias ls=")); + }); + it("should handle file that doesn't exist", () => { if (fs.existsSync(mockStartupFile)) { fs.unlinkSync(mockStartupFile); @@ -138,7 +146,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'", @@ -171,30 +179,43 @@ describe("Zsh shell integration", () => { describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { - const tools = ["npm", "yarn"]; - // 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-zsh.sh")); // Teardown zsh.teardown(); 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-zsh.sh")); }); it("should handle multiple setup calls", () => { - const tools = ["npm"]; - - zsh.setup(tools); - zsh.setup(tools); + zsh.setup(); + zsh.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.*init-zsh\.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-zsh.sh", + "alias ls='ls --color=auto'", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + // Teardown should remove both aliases and source line + zsh.teardown(); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("alias npm=")); + assert.ok(!content.includes("source ~/.safe-chain/scripts/init-zsh.sh")); + assert.ok(content.includes("alias ls=")); }); }); });