From d1c0982942613683cf4b52dee3a72a54d5be2f88 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 18 Sep 2025 17:44:42 +0200 Subject: [PATCH] Base safe-chain setupci implementation --- packages/safe-chain/bin/safe-chain.js | 8 ++ .../src/shell-integration/helpers.js | 16 +++ .../templates/unix-wrapper.template.sh | 22 ++++ .../templates/windows-wrapper.template.cmd | 24 ++++ .../src/shell-integration/setup-ci.js | 121 ++++++++++++++++++ .../safe-chain/src/shell-integration/setup.js | 4 +- .../src/shell-integration/teardown.js | 4 +- 7 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh create mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd create mode 100644 packages/safe-chain/src/shell-integration/setup-ci.js diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index a89aa6b..5a7d94b 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -4,6 +4,7 @@ import chalk from "chalk"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; +import { setupCi } from "../src/shell-integration/setup-ci.js"; if (process.argv.length < 3) { ui.writeError("No command provided. Please provide a command to execute."); @@ -23,6 +24,8 @@ if (command === "setup") { setup(); } else if (command === "teardown") { teardown(); +} else if (command === "setup-ci") { + setupCi(); } else { ui.writeError(`Unknown command: ${command}.`); ui.emptyLine(); @@ -53,5 +56,10 @@ function writeHelp() { "safe-chain teardown" )}: This will remove safe-chain aliases from your shell configuration.` ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain setup-ci" + )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` + ); ui.emptyLine(); } diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index d6c19d7..f338684 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -12,6 +12,22 @@ export const knownAikidoTools = [ // and add the documentation for the new tool in the README.md ]; +/** + * Returns a formatted string listing all supported package managers. + * Example: "npm, npx, yarn, pnpm, and pnpx commands" + */ +export function getPackageManagerList() { + const tools = knownAikidoTools.map(t => t.tool); + if (tools.length <= 1) { + return `${tools[0] || ''} commands`; + } + if (tools.length === 2) { + return `${tools[0]} and ${tools[1]} commands`; + } + const lastTool = tools.pop(); + return `${tools.join(', ')}, and ${lastTool} commands`; +} + export function doesExecutableExistOnSystem(executableName) { if (os.platform() === "win32") { const result = spawnSync("where", [executableName], { stdio: "ignore" }); diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh new file mode 100644 index 0000000..6e6d826 --- /dev/null +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain +# This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments + +# Function to remove shim from PATH (POSIX-compliant) +remove_shim_from_path() { + echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" +} + +if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then + # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops + PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@" +else + # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory) + original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}}) + if [ -n "$original_cmd" ]; then + exec "$original_cmd" "$@" + else + echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2 + exit 1 + fi +fi \ No newline at end of file diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd new file mode 100644 index 0000000..b7a65fa --- /dev/null +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd @@ -0,0 +1,24 @@ +@echo off +REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain +REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments + +REM Remove shim directory from PATH to prevent infinite loops +set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims" +call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" + +REM Check if aikido command is available with clean PATH +set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1 +if %errorlevel%==0 ( + REM Call aikido command with clean PATH + set "PATH=%CLEAN_PATH%" & {{AIKIDO_COMMAND}} %* +) else ( + REM Find the original command with clean PATH + for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do ( + "%%i" %* + goto :eof + ) + + REM If we get here, original command was not found + echo Error: Could not find original {{PACKAGE_MANAGER}} >&2 + exit /b 1 +) \ No newline at end of file diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js new file mode 100644 index 0000000..2338894 --- /dev/null +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -0,0 +1,121 @@ +import chalk from "chalk"; +import { ui } from "../environment/userInteraction.js"; +import { knownAikidoTools, getPackageManagerList } 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. + */ +export async function setupCi() { + ui.writeInformation( + chalk.bold("Setting up shell aliases.") + + ` This will wrap safe-chain around ${getPackageManagerList()}.` + ); + ui.emptyLine(); + + const shimsDir = path.join(os.homedir(), ".safe-chain", "shims"); + // Create the shims directory if it doesn't exist + if (!fs.existsSync(shimsDir)) { + fs.mkdirSync(shimsDir, { recursive: true }); + } + + createShims(shimsDir); + ui.writeInformation(`Created shims in ${shimsDir}`); + modifyPathForCi(shimsDir); + ui.writeInformation(`Added shims directory to PATH for CI environments.`); +} + +function createUnixShims(shimsDir) { + // Read the template file + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const templatePath = path.resolve( + __dirname, + "path-wrappers", + "templates", + "unix-wrapper.template.sh" + ); + + if (!fs.existsSync(templatePath)) { + ui.writeError(`Template file not found: ${templatePath}`); + return; + } + + const template = fs.readFileSync(templatePath, "utf-8"); + + // Create a shim for each tool + for (const toolInfo of knownAikidoTools) { + const shimContent = template + .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) + .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); + + const shimPath = path.join(shimsDir, toolInfo.tool); + fs.writeFileSync(shimPath, shimContent, "utf-8"); + + // Make the shim executable on Unix systems + fs.chmodSync(shimPath, 0o755); + } + + ui.writeInformation( + `Created ${knownAikidoTools.length} Unix shim(s) in ${shimsDir}` + ); +} + +function createWindowsShims(shimsDir) { + // Read the template file + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const templatePath = path.resolve( + __dirname, + "path-wrappers", + "templates", + "windows-wrapper.template.cmd" + ); + + if (!fs.existsSync(templatePath)) { + ui.writeError(`Windows template file not found: ${templatePath}`); + return; + } + + const template = fs.readFileSync(templatePath, "utf-8"); + + // Create a shim for each tool + for (const toolInfo of knownAikidoTools) { + const shimContent = template + .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) + .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); + + const shimPath = path.join(shimsDir, `${toolInfo.tool}.cmd`); + fs.writeFileSync(shimPath, shimContent, "utf-8"); + } + + ui.writeInformation( + `Created ${knownAikidoTools.length} Windows shim(s) in ${shimsDir}` + ); +} + +function createShims(shimsDir) { + if (os.platform() === "win32") { + createWindowsShims(shimsDir); + } else { + createUnixShims(shimsDir); + } +} + +function modifyPathForCi(shimsDir) { + if (process.env.GITHUB_PATH) { + // In GitHub Actions, append the shims directory to GITHUB_PATH + fs.appendFileSync(process.env.GITHUB_PATH, shimsDir + os.EOL, "utf-8"); + ui.writeInformation( + `Added shims directory to GITHUB_PATH for GitHub Actions.` + ); + } + + // detect azure pipelines + if (process.env.TF_BUILD) { + ui.writeInformation("##vso[task.prependpath]" + shimsDir); + } +} diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 792aa3c..9a37f7c 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -1,7 +1,7 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools } from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; import fs from "fs"; import os from "os"; import path from "path"; @@ -13,7 +13,7 @@ import { fileURLToPath } from "url"; export async function setup() { ui.writeInformation( chalk.bold("Setting up shell aliases.") + - " This will wrap safe-chain around npm, npx, and yarn commands." + ` This will wrap safe-chain around ${getPackageManagerList()}.` ); ui.emptyLine(); diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index 9f74300..d6b1277 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -1,12 +1,12 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools } from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; export async function teardown() { ui.writeInformation( chalk.bold("Removing shell aliases.") + - " This will remove safe-chain aliases for npm, npx, and yarn commands." + ` This will remove safe-chain aliases for ${getPackageManagerList()}.` ); ui.emptyLine();