mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #70 from AikidoSec/non-interactive-terminal-support
Support for CI/CD
This commit is contained in:
commit
cea4507559
13 changed files with 797 additions and 5 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,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" });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
123
packages/safe-chain/src/shell-integration/setup-ci.js
Normal file
123
packages/safe-chain/src/shell-integration/setup-ci.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
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.`
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.TF_BUILD) {
|
||||
// In Azure Pipelines, prepending the path is done via a logging command:
|
||||
// ##vso[task.prependpath]/path/to/add
|
||||
// Logging this to stdout will cause the Azure Pipelines agent to pick it up
|
||||
ui.writeInformation("##vso[task.prependpath]" + shimsDir);
|
||||
}
|
||||
}
|
||||
150
packages/safe-chain/src/shell-integration/setup-ci.spec.js
Normal file
150
packages/safe-chain/src/shell-integration/setup-ci.spec.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { tmpdir } from "node:os";
|
||||
import fs from "node:fs";
|
||||
import path from "path";
|
||||
|
||||
describe("Setup CI shell integration", () => {
|
||||
let mockShimsDir;
|
||||
let mockTemplateDir;
|
||||
let setupCi;
|
||||
let mockHomeDir;
|
||||
let mockPlatform;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockPlatform = "linux";
|
||||
// Create temporary directories for testing
|
||||
mockHomeDir = path.join(tmpdir(), `test-home-${Date.now()}`);
|
||||
mockShimsDir = path.join(mockHomeDir, ".safe-chain", "shims");
|
||||
mockTemplateDir = path.join(tmpdir(), `test-templates-${Date.now()}`);
|
||||
|
||||
// Create template directories and files
|
||||
fs.mkdirSync(path.join(mockTemplateDir, "path-wrappers", "templates"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(mockTemplateDir, "path-wrappers", "templates", "unix-wrapper.template.sh"),
|
||||
"#!/bin/bash\n# Template for {{PACKAGE_MANAGER}}\nexec {{AIKIDO_COMMAND}} \"$@\"\n",
|
||||
"utf-8"
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(mockTemplateDir, "path-wrappers", "templates", "windows-wrapper.template.cmd"),
|
||||
"@echo off\nREM Template for {{PACKAGE_MANAGER}}\n{{AIKIDO_COMMAND}} %*\n",
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// Mock the ui module
|
||||
mock.module("../environment/userInteraction.js", {
|
||||
namedExports: {
|
||||
ui: {
|
||||
writeInformation: () => {},
|
||||
emptyLine: () => {},
|
||||
writeError: () => {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock the helpers module
|
||||
mock.module("./helpers.js", {
|
||||
namedExports: {
|
||||
knownAikidoTools: [
|
||||
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||
],
|
||||
getPackageManagerList: () => "npm, yarn",
|
||||
},
|
||||
});
|
||||
|
||||
// Mock os module
|
||||
mock.module("os", {
|
||||
namedExports: {
|
||||
homedir: () => mockHomeDir,
|
||||
platform: () => mockPlatform,
|
||||
EOL: "\n",
|
||||
},
|
||||
});
|
||||
|
||||
// Mock path module to resolve templates correctly
|
||||
mock.module("path", {
|
||||
namedExports: {
|
||||
join: path.join,
|
||||
dirname: () => mockTemplateDir,
|
||||
resolve: (...args) => path.resolve(mockTemplateDir, ...args.slice(1)),
|
||||
},
|
||||
});
|
||||
|
||||
// Mock fileURLToPath
|
||||
mock.module("url", {
|
||||
namedExports: {
|
||||
fileURLToPath: () => path.join(mockTemplateDir, "setup-ci.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// Import setupCi module after mocking
|
||||
setupCi = (await import("./setup-ci.js")).setupCi;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directories
|
||||
if (fs.existsSync(mockShimsDir)) {
|
||||
fs.rmSync(mockShimsDir, { recursive: true, force: true });
|
||||
}
|
||||
if (fs.existsSync(mockHomeDir)) {
|
||||
fs.rmSync(mockHomeDir, { recursive: true, force: true });
|
||||
}
|
||||
if (fs.existsSync(mockTemplateDir)) {
|
||||
fs.rmSync(mockTemplateDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Reset mocks
|
||||
mock.reset();
|
||||
mockPlatform = "linux";
|
||||
});
|
||||
|
||||
describe("setupCi", () => {
|
||||
it("should create shims directory and Unix shims", async () => {
|
||||
await setupCi();
|
||||
|
||||
// Check if shims directory was created
|
||||
assert.ok(fs.existsSync(mockShimsDir), "Shims directory should exist");
|
||||
|
||||
// Check if npm shim was created
|
||||
const npmShimPath = path.join(mockShimsDir, "npm");
|
||||
assert.ok(fs.existsSync(npmShimPath), "npm shim should exist");
|
||||
|
||||
// Check if yarn shim was created
|
||||
const yarnShimPath = path.join(mockShimsDir, "yarn");
|
||||
assert.ok(fs.existsSync(yarnShimPath), "yarn shim should exist");
|
||||
|
||||
// Check content of npm shim
|
||||
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm shim should contain aikido-npm");
|
||||
assert.ok(npmShimContent.includes("#!/bin/bash"), "npm shim should have bash shebang");
|
||||
});
|
||||
|
||||
it("should create Windows .cmd shims on win32 platform", async () => {
|
||||
// Change platform for this test
|
||||
mockPlatform = "win32";
|
||||
|
||||
await setupCi();
|
||||
|
||||
// Check if shims directory was created
|
||||
assert.ok(fs.existsSync(mockShimsDir), "Shims directory should exist");
|
||||
|
||||
// Check if .cmd files were created instead of Unix scripts
|
||||
const npmShimPath = path.join(mockShimsDir, "npm.cmd");
|
||||
assert.ok(fs.existsSync(npmShimPath), "npm.cmd shim should exist");
|
||||
|
||||
const yarnShimPath = path.join(mockShimsDir, "yarn.cmd");
|
||||
assert.ok(fs.existsSync(yarnShimPath), "yarn.cmd shim should exist");
|
||||
|
||||
// Check content of npm.cmd shim
|
||||
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm.cmd should contain aikido-npm");
|
||||
assert.ok(npmShimContent.includes("@echo off"), "npm.cmd should have Windows batch header");
|
||||
assert.ok(npmShimContent.includes("%*"), "npm.cmd should use Windows argument passing");
|
||||
|
||||
// Verify Unix shims were NOT created
|
||||
const unixNpmShim = path.join(mockShimsDir, "npm");
|
||||
assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue