diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index 999f3d2..4cc2968 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"]; + const startupFiles = ["init-posix.sh", "init-pwsh.ps1"]; for (const file of startupFiles) { const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); 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..58a19c9 --- /dev/null +++ b/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -0,0 +1,122 @@ +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 + } else { + # Fallback: try to call the .cmd version directly + & "$Command.cmd" @Arguments + } +} + +function Install-IfCommandNotFound { + param([string]$Command) + + # Check if the command already exists + if (Test-CommandAvailable $Command) { + return 0 + } + + # Check if Node.js version is below 18 + # Safe-chain requires Node.js 18 or higher + try { + $nodeVersion = (node -v) -replace 'v', '' | ForEach-Object { $_.Split('.')[0] } + if ([int]$nodeVersion -lt 18) { + return 2 + } + } catch { + return 2 + } + + # Command not found, ask user if they want to install safe-chain + $response = Read-Host "The command '$Command' is not available. Do you want to install safe-chain to provide it? (y/N)" + + if ($response -match '^[Yy]$') { + Write-Host "Installing safe-chain..." + $installResult = Install-SafeChain + + if ($installResult -ne 0) { + Write-Host "`nFailed to install safe-chain. Exiting." + return 1 + } + + return 0 + } else { + Write-Host "Skipping safe-chain installation. Using original command instead." + return 2 + } +} + +function Install-SafeChain { + try { + Invoke-RealCommand "npm" @("install", "-g", "@aikidosec/safe-chain") | Out-Null + + if ($LASTEXITCODE -ne 0) { + return 1 + } + + Write-Host "------" + return 0 + } catch { + return 1 + } +} + +function Invoke-WrappedCommand { + param( + [string]$OriginalCmd, + [string]$AikidoCmd, + [string[]]$Arguments + ) + + $installResult = Install-IfCommandNotFound $AikidoCmd + + if ($installResult -eq 2) { + Invoke-RealCommand $OriginalCmd $Arguments + } else { + & $AikidoCmd @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 +} \ 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 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..57d5098 100644 --- a/src/shell-integration/supported-shells/powershell.spec.js +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -69,49 +69,43 @@ 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") + content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") ); - 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"); - 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 +132,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 +165,25 @@ 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..96677a8 100644 --- a/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -69,49 +69,43 @@ 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") + content.includes(". \"$HOME\\\\.safe-chain\\\\scripts\\\\init-pwsh.ps1\" # Safe-chain PowerShell initialization script") ); - 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"); - 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 +132,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 +165,25 @@ 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"); }); }); });