Base safe-chain setupci implementation

This commit is contained in:
Sander Declerck 2025-09-18 17:44:42 +02:00
parent f7589160af
commit d1c0982942
No known key found for this signature in database
7 changed files with 195 additions and 4 deletions

View file

@ -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();
}

View file

@ -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" });

View file

@ -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

View file

@ -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
)

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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();