mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #2 from AikidoSec/improved-shell-integration
Refactor to allow customising shell integration per shell
This commit is contained in:
commit
c87c6491d7
16 changed files with 1397 additions and 1022 deletions
|
|
@ -1,4 +1,8 @@
|
||||||
const knownAikidoTools = [
|
import { spawnSync } from "child_process";
|
||||||
|
import * as os from "os";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export const knownAikidoTools = [
|
||||||
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||||
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
||||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
|
|
@ -8,39 +12,33 @@ const knownAikidoTools = [
|
||||||
// and add the documentation for the new tool in the README.md
|
// and add the documentation for the new tool in the README.md
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getAliases(fileName) {
|
export function doesExecutableExistOnSystem(executableName) {
|
||||||
const fileExtension = fileName.split(".").pop().toLowerCase();
|
if (os.platform() === "win32") {
|
||||||
|
const result = spawnSync("where", [executableName], { stdio: "ignore" });
|
||||||
let createAlias = pickCreateAliasFunction(fileExtension);
|
return result.status === 0;
|
||||||
|
} else {
|
||||||
const aliases = knownAikidoTools.map(({ tool, aikidoCommand }) =>
|
const result = spawnSync("which", [executableName], { stdio: "ignore" });
|
||||||
createAlias(tool, aikidoCommand)
|
return result.status === 0;
|
||||||
);
|
|
||||||
|
|
||||||
return aliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickCreateAliasFunction(fileExtension) {
|
|
||||||
let createAlias;
|
|
||||||
switch (fileExtension) {
|
|
||||||
case "ps1":
|
|
||||||
createAlias = createGeneralPowershellAlias;
|
|
||||||
break;
|
|
||||||
case "fish":
|
|
||||||
createAlias = createGeneralFishAlias;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
createAlias = createGeneralPosixAlias;
|
|
||||||
}
|
}
|
||||||
return createAlias;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGeneralPosixAlias(tool, aikidoCommand) {
|
export function removeLinesMatchingPattern(filePath, pattern) {
|
||||||
return `alias ${tool}='${aikidoCommand}'`;
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = fileContent.split(os.EOL);
|
||||||
|
const updatedLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8");
|
||||||
}
|
}
|
||||||
function createGeneralPowershellAlias(tool, aikidoCommand) {
|
|
||||||
return `Set-Alias ${tool} ${aikidoCommand}`;
|
export function addLineToFile(filePath, line) {
|
||||||
}
|
if (!fs.existsSync(filePath)) {
|
||||||
function createGeneralFishAlias(tool, aikidoCommand) {
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
return `alias ${tool} "${aikidoCommand}"`;
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const updatedContent = fileContent + os.EOL + line;
|
||||||
|
fs.writeFileSync(filePath, updatedContent, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { detectShells } from "./shellDetection.js";
|
import { detectShells } from "./shellDetection.js";
|
||||||
import { getAliases } from "./helpers.js";
|
import { knownAikidoTools } from "./helpers.js";
|
||||||
import fs from "fs";
|
|
||||||
import { EOL } from "os";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loops over the detected shells and calls the setup function for each.
|
||||||
|
*/
|
||||||
export async function setup() {
|
export async function setup() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
chalk.bold("Setting up shell aliases.") +
|
chalk.bold("Setting up shell aliases.") +
|
||||||
|
|
@ -27,7 +28,7 @@ export async function setup() {
|
||||||
|
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (setupAliasesForShell(shell)) {
|
if (setupShell(shell)) {
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,107 +46,30 @@ export async function setup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function sets up aliases for the given shell.
|
* Calls the setup function for the given shell and reports the result.
|
||||||
* It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.),
|
|
||||||
* and then appends the aliases for npm, npx, and yarn commands.
|
|
||||||
* If the aliases already exist, it will not add them again.
|
|
||||||
* If the startup file does not exist, it will create it.
|
|
||||||
*
|
|
||||||
* The shell startup script is loaded by the respective shell when it starts.
|
|
||||||
* This means that the aliases will be available in the shell after it is restarted.
|
|
||||||
*/
|
*/
|
||||||
function setupAliasesForShell(shell) {
|
function setupShell(shell) {
|
||||||
if (!shell.startupFile) {
|
let success = false;
|
||||||
ui.writeError(
|
try {
|
||||||
`- ${chalk.bold(
|
shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
|
||||||
shell.name
|
success = shell.setup(knownAikidoTools);
|
||||||
)}: no startup file found. Cannot set up aliases.`
|
} catch {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ui.writeInformation(
|
||||||
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.green(
|
||||||
|
"Setup successful"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.writeError(
|
||||||
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
||||||
|
"Setup failed"
|
||||||
|
)}. Please check your ${shell.name} configuration.`
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliases = getAliases(shell.startupFile);
|
return success;
|
||||||
|
|
||||||
if (aliases.length === 0) {
|
|
||||||
ui.writeError(`- ${chalk.bold(shell.name)}: could not generate aliases.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileContent = readOrCreateStartupFile(shell.startupFile);
|
|
||||||
const { addedCount, existingCount, failedCount } = appendAliasesToFile(
|
|
||||||
aliases,
|
|
||||||
fileContent,
|
|
||||||
shell.startupFile
|
|
||||||
);
|
|
||||||
|
|
||||||
let summary = "- " + chalk.bold(shell.name) + ": ";
|
|
||||||
|
|
||||||
if (addedCount > 0) {
|
|
||||||
summary += chalk.green(`${addedCount} aliases were added`);
|
|
||||||
}
|
|
||||||
if (existingCount > 0) {
|
|
||||||
if (addedCount > 0) {
|
|
||||||
summary += ", ";
|
|
||||||
}
|
|
||||||
summary += chalk.yellow(`${existingCount} aliases were already present`);
|
|
||||||
}
|
|
||||||
if (failedCount > 0) {
|
|
||||||
if (addedCount > 0 || existingCount > 0) {
|
|
||||||
summary += ", ";
|
|
||||||
}
|
|
||||||
summary += chalk.red(`${failedCount} aliases failed to add`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// write summary in a single line
|
|
||||||
ui.writeInformation(summary);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This reads the content of the startup file.
|
|
||||||
* If the file does not exist, it creates an empty file and returns an empty string.
|
|
||||||
* The startup file is the shell's startup script (eg: ~/.bashrc, ~/.zshrc, etc.).
|
|
||||||
* It is used to set up the shell environment when it starts.
|
|
||||||
* Some shells may not have a startup file, in which case this function will create one.
|
|
||||||
*/
|
|
||||||
export function readOrCreateStartupFile(filePath) {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
fs.writeFileSync(filePath, "", "utf-8");
|
|
||||||
ui.writeInformation(`File ${filePath} created.`);
|
|
||||||
}
|
|
||||||
return fs.readFileSync(filePath, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function appends the aliases to the startup file.
|
|
||||||
* eg: for bash it will append 'alias npm="aikido-npm"' for npm to ~/.bashrc
|
|
||||||
* @returns an object with the counts of added, existing, and failed aliases.
|
|
||||||
*/
|
|
||||||
export function appendAliasesToFile(aliases, fileContent, startupFilePath) {
|
|
||||||
let addedCount = 0;
|
|
||||||
let existingCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
for (const alias of aliases) {
|
|
||||||
try {
|
|
||||||
if (fileContent.includes(alias)) {
|
|
||||||
existingCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.appendFileSync(startupFilePath, `${EOL}${alias}`, "utf-8");
|
|
||||||
|
|
||||||
addedCount++;
|
|
||||||
} catch {
|
|
||||||
failedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
addedCount,
|
|
||||||
existingCount,
|
|
||||||
failedCount,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,466 +0,0 @@
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import { EOL, tmpdir } from "node:os";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { getAliases } from "./helpers.js";
|
|
||||||
import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js";
|
|
||||||
|
|
||||||
describe("setupShell", () => {
|
|
||||||
function runSetupTestsForEnvironment(
|
|
||||||
shell,
|
|
||||||
startupExtension,
|
|
||||||
expectedAliases
|
|
||||||
) {
|
|
||||||
describe(`${shell} shell setup`, () => {
|
|
||||||
it(`should add aliases to ${shell} file`, () => {
|
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.addedCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
`Should add ${expectedAliases.length} aliases`
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find no existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.failedCount,
|
|
||||||
0,
|
|
||||||
"Should have no failed aliases"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes(alias),
|
|
||||||
`Alias "${alias}" should be added`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias cls='clear'"),
|
|
||||||
"Original aliases should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should not add aliases if they already exist in ${shell} file`, () => {
|
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
`Should find ${expectedAliases.length} existing aliases`
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.failedCount,
|
|
||||||
0,
|
|
||||||
"Should have no failed aliases"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
// Count occurrences to ensure no duplicates were added
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.strictEqual(
|
|
||||||
countOccurrences(updatedContent, alias),
|
|
||||||
1,
|
|
||||||
`Alias "${alias}" should appear exactly once`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should create file and add aliases if file does not exist for ${shell}`, () => {
|
|
||||||
const randomName = Math.random().toString(36).substring(2, 15);
|
|
||||||
const filePath = `${tmpdir()}/nonexistent-${randomName}${startupExtension}`;
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test readOrCreateStartupFile function
|
|
||||||
const fileContent = readOrCreateStartupFile(filePath);
|
|
||||||
assert.strictEqual(
|
|
||||||
fileContent,
|
|
||||||
"",
|
|
||||||
"Should return empty string for new file"
|
|
||||||
);
|
|
||||||
assert.ok(fs.existsSync(filePath), "File should be created");
|
|
||||||
|
|
||||||
// Test adding aliases to the newly created file
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.addedCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
`Should add ${expectedAliases.length} aliases`
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find no existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.failedCount,
|
|
||||||
0,
|
|
||||||
"Should have no failed aliases"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes(alias),
|
|
||||||
`Alias "${alias}" should be added`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should add aliases only once when called multiple times for ${shell}`, () => {
|
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, ""];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
|
|
||||||
// First call - should add aliases
|
|
||||||
let fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const result1 = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
assert.strictEqual(
|
|
||||||
result1.addedCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
`First call should add ${expectedAliases.length} aliases`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Second call - should detect existing aliases
|
|
||||||
fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const result2 = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
assert.strictEqual(
|
|
||||||
result2.addedCount,
|
|
||||||
0,
|
|
||||||
"Second call should add 0 aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result2.existingCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
`Second call should find ${expectedAliases.length} existing aliases`
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.strictEqual(
|
|
||||||
countOccurrences(updatedContent, alias),
|
|
||||||
1,
|
|
||||||
`Alias "${alias}" should appear exactly once`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should use real getAliases() for ${shell} file`, () => {
|
|
||||||
const filePath = `${tmpdir()}/test${startupExtension}`;
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
|
|
||||||
// Verify we get the expected aliases for this shell type
|
|
||||||
assert.strictEqual(
|
|
||||||
aliases.length,
|
|
||||||
expectedAliases.length,
|
|
||||||
"Should get all aliases (npm, npx, yarn)"
|
|
||||||
);
|
|
||||||
for (let i = 0; i < aliases.length; i++) {
|
|
||||||
assert.strictEqual(
|
|
||||||
aliases[i],
|
|
||||||
expectedAliases[i],
|
|
||||||
`Alias ${i} should match expected format`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should handle mixed scenario - some existing, some new for ${shell}`, () => {
|
|
||||||
const lines = [
|
|
||||||
`#!/usr/bin/env ${shell}`,
|
|
||||||
"",
|
|
||||||
expectedAliases[0],
|
|
||||||
"alias other='command'",
|
|
||||||
];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.addedCount,
|
|
||||||
expectedAliases.length - 1,
|
|
||||||
`Should add ${expectedAliases.length - 1} aliases`
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
1,
|
|
||||||
"Should find 1 existing alias"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.failedCount,
|
|
||||||
0,
|
|
||||||
"Should have no failed aliases"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes(alias),
|
|
||||||
`Alias "${alias}" should be present`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias other='command'"),
|
|
||||||
"Other aliases should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for each shell type using real getAliases() output
|
|
||||||
runSetupTestsForEnvironment("bash", ".bashrc", [
|
|
||||||
"alias npm='aikido-npm'",
|
|
||||||
"alias npx='aikido-npx'",
|
|
||||||
"alias yarn='aikido-yarn'",
|
|
||||||
"alias pnpm='aikido-pnpm'",
|
|
||||||
"alias pnpx='aikido-pnpx'",
|
|
||||||
]);
|
|
||||||
|
|
||||||
runSetupTestsForEnvironment("zsh", ".zshrc", [
|
|
||||||
"alias npm='aikido-npm'",
|
|
||||||
"alias npx='aikido-npx'",
|
|
||||||
"alias yarn='aikido-yarn'",
|
|
||||||
"alias pnpm='aikido-pnpm'",
|
|
||||||
"alias pnpx='aikido-pnpx'",
|
|
||||||
]);
|
|
||||||
|
|
||||||
runSetupTestsForEnvironment("fish", ".fish", [
|
|
||||||
'alias npm "aikido-npm"',
|
|
||||||
'alias npx "aikido-npx"',
|
|
||||||
'alias yarn "aikido-yarn"',
|
|
||||||
'alias pnpm "aikido-pnpm"',
|
|
||||||
'alias pnpx "aikido-pnpx"',
|
|
||||||
]);
|
|
||||||
|
|
||||||
runSetupTestsForEnvironment("pwsh", ".ps1", [
|
|
||||||
"Set-Alias npm aikido-npm",
|
|
||||||
"Set-Alias npx aikido-npx",
|
|
||||||
"Set-Alias yarn aikido-yarn",
|
|
||||||
"Set-Alias pnpm aikido-pnpm",
|
|
||||||
"Set-Alias pnpx aikido-pnpx",
|
|
||||||
]);
|
|
||||||
|
|
||||||
describe("readOrCreateStartupFile", () => {
|
|
||||||
it("should read existing file content", () => {
|
|
||||||
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
|
||||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
content.includes("#!/usr/bin/env bash"),
|
|
||||||
"Should contain shebang"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
content.includes("alias test='echo test'"),
|
|
||||||
"Should contain existing aliases"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create file if it doesn't exist", () => {
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
content,
|
|
||||||
"",
|
|
||||||
"Should return empty string for new file"
|
|
||||||
);
|
|
||||||
assert.ok(fs.existsSync(filePath), "File should be created");
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty existing file", () => {
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
fs.writeFileSync(filePath, "", "utf-8");
|
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
content,
|
|
||||||
"",
|
|
||||||
"Should return empty string for empty file"
|
|
||||||
);
|
|
||||||
assert.ok(fs.existsSync(filePath), "File should still exist");
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("appendAliasesToFile edge cases", () => {
|
|
||||||
it("should handle empty aliases array", () => {
|
|
||||||
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
|
||||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = appendAliasesToFile([], fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find 0 existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias test='echo test'"),
|
|
||||||
"Original content should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle partial substring matches correctly", () => {
|
|
||||||
const lines = [
|
|
||||||
"#!/usr/bin/env bash",
|
|
||||||
"",
|
|
||||||
"alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm='
|
|
||||||
"alias test='echo test'",
|
|
||||||
];
|
|
||||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 1, "Should add 1 alias (npm)");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find 0 existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias npm='aikido-npm'"),
|
|
||||||
"npm alias should be added"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias npmx='some-other-command'"),
|
|
||||||
"npmx alias should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle file with only whitespace", () => {
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
const fileContent = `${EOL}${EOL} ${EOL}`;
|
|
||||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 1, "Should add 1 alias");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find 0 existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
|
||||||
|
|
||||||
const updatedContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias npm='aikido-npm'"),
|
|
||||||
"Alias should be added"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("appendAliasesToFile error handling", () => {
|
|
||||||
it("should handle file permission errors gracefully", () => {
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
fs.writeFileSync(filePath, "#!/usr/bin/env bash", "utf-8");
|
|
||||||
|
|
||||||
// Make file read-only to simulate permission error
|
|
||||||
fs.chmodSync(filePath, 0o444);
|
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.addedCount,
|
|
||||||
0,
|
|
||||||
"Should add 0 aliases due to permission error"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.existingCount,
|
|
||||||
0,
|
|
||||||
"Should find 0 existing aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.failedCount, 1, "Should have 1 failed alias");
|
|
||||||
|
|
||||||
// Restore permissions and cleanup
|
|
||||||
fs.chmodSync(filePath, 0o644);
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createShellStartupScript(lines, fileExtension) {
|
|
||||||
const randomFileName = Math.random().toString(36).substring(2, 15);
|
|
||||||
const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
|
|
||||||
fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAndDeleteFile(filePath) {
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
return fileContent.split(EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
function countOccurrences(lines, searchString) {
|
|
||||||
let count = 0;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes(searchString)) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +1,26 @@
|
||||||
import * as os from "os";
|
import zsh from "./supported-shells/zsh.js";
|
||||||
import { execSync } from "child_process";
|
import bash from "./supported-shells/bash.js";
|
||||||
|
import powershell from "./supported-shells/powershell.js";
|
||||||
const shellList = {
|
import windowsPowershell from "./supported-shells/windowsPowershell.js";
|
||||||
bash: {
|
import fish from "./supported-shells/fish.js";
|
||||||
name: "Bash",
|
import { ui } from "../environment/userInteraction.js";
|
||||||
executable: "bash",
|
|
||||||
getStartupFileCommand: "echo ~/.bashrc",
|
|
||||||
},
|
|
||||||
zsh: {
|
|
||||||
name: "Zsh",
|
|
||||||
executable: "zsh",
|
|
||||||
getStartupFileCommand: "echo ${ZDOTDIR:-$HOME}/.zshrc",
|
|
||||||
},
|
|
||||||
fish: {
|
|
||||||
name: "Fish",
|
|
||||||
executable: "fish",
|
|
||||||
getStartupFileCommand: "echo ~/.config/fish/config.fish",
|
|
||||||
},
|
|
||||||
powershell: {
|
|
||||||
name: "PowerShell Core",
|
|
||||||
executable: "pwsh",
|
|
||||||
getStartupFileCommand: "echo $PROFILE",
|
|
||||||
},
|
|
||||||
windowsPowerShell: {
|
|
||||||
name: "Windows PowerShell",
|
|
||||||
executable: "powershell",
|
|
||||||
getStartupFileCommand: "echo $PROFILE",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function detectShells() {
|
export function detectShells() {
|
||||||
|
let possibleShells = [zsh, bash, powershell, windowsPowershell, fish];
|
||||||
let availableShells = [];
|
let availableShells = [];
|
||||||
|
|
||||||
for (const shellName of Object.keys(shellList)) {
|
try {
|
||||||
const shell = shellList[shellName];
|
for (const shell of possibleShells) {
|
||||||
|
if (shell.isInstalled()) {
|
||||||
if (isShellAvailable(shell)) {
|
availableShells.push(shell);
|
||||||
const startupFile = getShellStartupFile(shell);
|
}
|
||||||
availableShells.push({
|
|
||||||
name: shell.name,
|
|
||||||
executable: shell.executable,
|
|
||||||
startupFile: startupFile || null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ui.writeError(
|
||||||
|
`We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableShells;
|
return availableShells;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isShellAvailable(shell) {
|
|
||||||
try {
|
|
||||||
if (os.platform() === "win32") {
|
|
||||||
execSync(`where ${shell.executable}`, { stdio: "ignore" });
|
|
||||||
} else {
|
|
||||||
execSync(`which ${shell.executable}`, { stdio: "ignore" });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShellStartupFile(shell) {
|
|
||||||
try {
|
|
||||||
const command = shell.getStartupFileCommand;
|
|
||||||
const output = execSync(command, {
|
|
||||||
encoding: "utf8",
|
|
||||||
shell: shell.executable,
|
|
||||||
}).trim();
|
|
||||||
return output;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
58
src/shell-integration/supported-shells/bash.js
Normal file
58
src/shell-integration/supported-shells/bash.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import {
|
||||||
|
addLineToFile,
|
||||||
|
doesExecutableExistOnSystem,
|
||||||
|
removeLinesMatchingPattern,
|
||||||
|
} from "../helpers.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const shellName = "Bash";
|
||||||
|
const executableName = "bash";
|
||||||
|
const startupFileCommand = "echo ~/.bashrc";
|
||||||
|
|
||||||
|
function isInstalled() {
|
||||||
|
return doesExecutableExistOnSystem(executableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool } of tools) {
|
||||||
|
// Remove any existing alias for the tool
|
||||||
|
removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool, aikidoCommand } of tools) {
|
||||||
|
addLineToFile(
|
||||||
|
startupFile,
|
||||||
|
`alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartupFile() {
|
||||||
|
try {
|
||||||
|
return execSync(startupFileCommand, {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
}).trim();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: shellName,
|
||||||
|
isInstalled,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
};
|
||||||
199
src/shell-integration/supported-shells/bash.spec.js
Normal file
199
src/shell-integration/supported-shells/bash.spec.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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";
|
||||||
|
import { knownAikidoTools } from "../helpers.js";
|
||||||
|
|
||||||
|
describe("Bash shell integration", () => {
|
||||||
|
let mockStartupFile;
|
||||||
|
let bash;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary startup file for testing
|
||||||
|
mockStartupFile = path.join(tmpdir(), `test-bashrc-${Date.now()}`);
|
||||||
|
|
||||||
|
// Mock the helpers module
|
||||||
|
mock.module("../helpers.js", {
|
||||||
|
namedExports: {
|
||||||
|
doesExecutableExistOnSystem: () => true,
|
||||||
|
addLineToFile: (filePath, line) => {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
||||||
|
},
|
||||||
|
removeLinesMatchingPattern: (filePath, pattern) => {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock child_process execSync
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
execSync: () => mockStartupFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import bash module after mocking
|
||||||
|
bash = (await import("./bash.js")).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isInstalled", () => {
|
||||||
|
it("should return true when bash is installed", () => {
|
||||||
|
assert.strictEqual(bash.isInstalled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call doesExecutableExistOnSystem with correct parameter", () => {
|
||||||
|
// Test that the method calls the helper with the right executable name
|
||||||
|
assert.strictEqual(bash.isInstalled(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = bash.setup(tools);
|
||||||
|
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')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty tools array", () => {
|
||||||
|
const result = bash.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", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/bin/bash",
|
||||||
|
"alias npm='aikido-npm'",
|
||||||
|
"alias npx='aikido-npx'",
|
||||||
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias ls='ls --color=auto'",
|
||||||
|
"alias grep='grep --color=auto'",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = bash.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm="));
|
||||||
|
assert.ok(!content.includes("alias npx="));
|
||||||
|
assert.ok(!content.includes("alias yarn="));
|
||||||
|
assert.ok(content.includes("alias ls="));
|
||||||
|
assert.ok(content.includes("alias grep="));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file that doesn't exist", () => {
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = bash.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file with no relevant aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/bin/bash",
|
||||||
|
"alias ls='ls --color=auto'",
|
||||||
|
"export PATH=$PATH:~/bin",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = bash.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes("alias ls="));
|
||||||
|
assert.ok(content.includes("export PATH="));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shell properties", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
assert.strictEqual(bash.name, "Bash");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose all required methods", () => {
|
||||||
|
assert.ok(typeof bash.isInstalled === "function");
|
||||||
|
assert.ok(typeof bash.setup === "function");
|
||||||
|
assert.ok(typeof bash.teardown === "function");
|
||||||
|
assert.ok(typeof bash.name === "string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration tests", () => {
|
||||||
|
it("should handle complete setup and teardown cycle", () => {
|
||||||
|
const tools = [
|
||||||
|
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||||
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
bash.setup(tools);
|
||||||
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes('alias npm="aikido-npm"'));
|
||||||
|
assert.ok(content.includes('alias yarn="aikido-yarn"'));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
bash.teardown(tools);
|
||||||
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm="));
|
||||||
|
assert.ok(!content.includes("alias yarn="));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple setup calls", () => {
|
||||||
|
const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
|
||||||
|
|
||||||
|
bash.setup(tools);
|
||||||
|
bash.teardown(tools);
|
||||||
|
bash.setup(tools);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
const npmMatches = (content.match(/alias npm="/g) || []).length;
|
||||||
|
assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
61
src/shell-integration/supported-shells/fish.js
Normal file
61
src/shell-integration/supported-shells/fish.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
addLineToFile,
|
||||||
|
doesExecutableExistOnSystem,
|
||||||
|
removeLinesMatchingPattern,
|
||||||
|
} from "../helpers.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const shellName = "Fish";
|
||||||
|
const executableName = "fish";
|
||||||
|
const startupFileCommand = "echo ~/.config/fish/config.fish";
|
||||||
|
|
||||||
|
function isInstalled() {
|
||||||
|
return doesExecutableExistOnSystem(executableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool } of tools) {
|
||||||
|
// Remove any existing alias for the tool
|
||||||
|
removeLinesMatchingPattern(
|
||||||
|
startupFile,
|
||||||
|
new RegExp(`^alias\\s+${tool}\\s+`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool, aikidoCommand } of tools) {
|
||||||
|
addLineToFile(
|
||||||
|
startupFile,
|
||||||
|
`alias ${tool} "${aikidoCommand}" # Safe-chain alias for ${tool}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartupFile() {
|
||||||
|
try {
|
||||||
|
return execSync(startupFileCommand, {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
}).trim();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: shellName,
|
||||||
|
isInstalled,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
};
|
||||||
199
src/shell-integration/supported-shells/fish.spec.js
Normal file
199
src/shell-integration/supported-shells/fish.spec.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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";
|
||||||
|
import { knownAikidoTools } from "../helpers.js";
|
||||||
|
|
||||||
|
describe("Fish shell integration", () => {
|
||||||
|
let mockStartupFile;
|
||||||
|
let fish;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary startup file for testing
|
||||||
|
mockStartupFile = path.join(tmpdir(), `test-fish-config-${Date.now()}`);
|
||||||
|
|
||||||
|
// Mock the helpers module
|
||||||
|
mock.module("../helpers.js", {
|
||||||
|
namedExports: {
|
||||||
|
doesExecutableExistOnSystem: () => true,
|
||||||
|
addLineToFile: (filePath, line) => {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
||||||
|
},
|
||||||
|
removeLinesMatchingPattern: (filePath, pattern) => {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock child_process execSync
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
execSync: () => mockStartupFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import fish module after mocking
|
||||||
|
fish = (await import("./fish.js")).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isInstalled", () => {
|
||||||
|
it("should return true when fish is installed", () => {
|
||||||
|
assert.strictEqual(fish.isInstalled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call doesExecutableExistOnSystem with correct parameter", () => {
|
||||||
|
// Test that the method calls the helper with the right executable name
|
||||||
|
assert.strictEqual(fish.isInstalled(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = fish.setup(tools);
|
||||||
|
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')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty tools array", () => {
|
||||||
|
const result = fish.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", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/usr/bin/env fish",
|
||||||
|
"alias npm 'aikido-npm'",
|
||||||
|
"alias npx 'aikido-npx'",
|
||||||
|
"alias yarn 'aikido-yarn'",
|
||||||
|
"alias ls 'ls --color=auto'",
|
||||||
|
"alias grep 'grep --color=auto'",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = fish.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm "));
|
||||||
|
assert.ok(!content.includes("alias npx "));
|
||||||
|
assert.ok(!content.includes("alias yarn "));
|
||||||
|
assert.ok(content.includes("alias ls "));
|
||||||
|
assert.ok(content.includes("alias grep "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file that doesn't exist", () => {
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = fish.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file with no relevant aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/usr/bin/env fish",
|
||||||
|
"alias ls 'ls --color=auto'",
|
||||||
|
"set PATH $PATH ~/bin",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = fish.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes("alias ls "));
|
||||||
|
assert.ok(content.includes("set PATH "));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shell properties", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
assert.strictEqual(fish.name, "Fish");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose all required methods", () => {
|
||||||
|
assert.ok(typeof fish.isInstalled === "function");
|
||||||
|
assert.ok(typeof fish.setup === "function");
|
||||||
|
assert.ok(typeof fish.teardown === "function");
|
||||||
|
assert.ok(typeof fish.name === "string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration tests", () => {
|
||||||
|
it("should handle complete setup and teardown cycle", () => {
|
||||||
|
const tools = [
|
||||||
|
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||||
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
fish.setup(tools);
|
||||||
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes('alias npm "aikido-npm"'));
|
||||||
|
assert.ok(content.includes('alias yarn "aikido-yarn"'));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
fish.teardown(tools);
|
||||||
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm "));
|
||||||
|
assert.ok(!content.includes("alias yarn "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple setup calls", () => {
|
||||||
|
const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
|
||||||
|
|
||||||
|
fish.setup(tools);
|
||||||
|
fish.teardown(tools);
|
||||||
|
fish.setup(tools);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
const npmMatches = (content.match(/alias npm "/g) || []).length;
|
||||||
|
assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
61
src/shell-integration/supported-shells/powershell.js
Normal file
61
src/shell-integration/supported-shells/powershell.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
addLineToFile,
|
||||||
|
doesExecutableExistOnSystem,
|
||||||
|
removeLinesMatchingPattern,
|
||||||
|
} from "../helpers.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const shellName = "PowerShell Core";
|
||||||
|
const executableName = "pwsh";
|
||||||
|
const startupFileCommand = "echo $PROFILE";
|
||||||
|
|
||||||
|
function isInstalled() {
|
||||||
|
return doesExecutableExistOnSystem(executableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool } of tools) {
|
||||||
|
// Remove any existing alias for the tool
|
||||||
|
removeLinesMatchingPattern(
|
||||||
|
startupFile,
|
||||||
|
new RegExp(`^Set-Alias\\s+${tool}\\s+`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool, aikidoCommand } of tools) {
|
||||||
|
addLineToFile(
|
||||||
|
startupFile,
|
||||||
|
`Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartupFile() {
|
||||||
|
try {
|
||||||
|
return execSync(startupFileCommand, {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
}).trim();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: shellName,
|
||||||
|
isInstalled,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
};
|
||||||
204
src/shell-integration/supported-shells/powershell.spec.js
Normal file
204
src/shell-integration/supported-shells/powershell.spec.js
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
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";
|
||||||
|
import { knownAikidoTools } from "../helpers.js";
|
||||||
|
|
||||||
|
describe("PowerShell Core shell integration", () => {
|
||||||
|
let mockStartupFile;
|
||||||
|
let powershell;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary startup file for testing
|
||||||
|
mockStartupFile = path.join(
|
||||||
|
tmpdir(),
|
||||||
|
`test-powershell-profile-${Date.now()}.ps1`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the helpers module
|
||||||
|
mock.module("../helpers.js", {
|
||||||
|
namedExports: {
|
||||||
|
doesExecutableExistOnSystem: () => true,
|
||||||
|
addLineToFile: (filePath, line) => {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
||||||
|
},
|
||||||
|
removeLinesMatchingPattern: (filePath, pattern) => {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock child_process execSync
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
execSync: () => mockStartupFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import powershell module after mocking
|
||||||
|
powershell = (await import("./powershell.js")).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isInstalled", () => {
|
||||||
|
it("should return true when powershell is installed", () => {
|
||||||
|
assert.strictEqual(powershell.isInstalled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call doesExecutableExistOnSystem with correct parameter", () => {
|
||||||
|
// Test that the method calls the helper with the right executable name
|
||||||
|
assert.strictEqual(powershell.isInstalled(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
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", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"# PowerShell profile",
|
||||||
|
"Set-Alias npm aikido-npm",
|
||||||
|
"Set-Alias npx aikido-npx",
|
||||||
|
"Set-Alias yarn aikido-yarn",
|
||||||
|
"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("Set-Alias npm "));
|
||||||
|
assert.ok(!content.includes("Set-Alias npx "));
|
||||||
|
assert.ok(!content.includes("Set-Alias yarn "));
|
||||||
|
assert.ok(content.includes("Set-Alias ls "));
|
||||||
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file that doesn't exist", () => {
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = powershell.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file with no relevant aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"# PowerShell profile",
|
||||||
|
"Set-Alias ls Get-ChildItem",
|
||||||
|
"$env:PATH += ';C:\\Tools'",
|
||||||
|
].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("Set-Alias ls "));
|
||||||
|
assert.ok(content.includes("$env:PATH "));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shell properties", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
assert.strictEqual(powershell.name, "PowerShell Core");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose all required methods", () => {
|
||||||
|
assert.ok(typeof powershell.isInstalled === "function");
|
||||||
|
assert.ok(typeof powershell.setup === "function");
|
||||||
|
assert.ok(typeof powershell.teardown === "function");
|
||||||
|
assert.ok(typeof powershell.name === "string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
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"));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
powershell.teardown(tools);
|
||||||
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("Set-Alias npm "));
|
||||||
|
assert.ok(!content.includes("Set-Alias yarn "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple setup calls", () => {
|
||||||
|
const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
|
||||||
|
|
||||||
|
powershell.setup(tools);
|
||||||
|
powershell.teardown(tools);
|
||||||
|
powershell.setup(tools);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
const npmMatches = (content.match(/Set-Alias npm /g) || []).length;
|
||||||
|
assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
61
src/shell-integration/supported-shells/windowsPowershell.js
Normal file
61
src/shell-integration/supported-shells/windowsPowershell.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
addLineToFile,
|
||||||
|
doesExecutableExistOnSystem,
|
||||||
|
removeLinesMatchingPattern,
|
||||||
|
} from "../helpers.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const shellName = "Windows PowerShell";
|
||||||
|
const executableName = "powershell";
|
||||||
|
const startupFileCommand = "echo $PROFILE";
|
||||||
|
|
||||||
|
function isInstalled() {
|
||||||
|
return doesExecutableExistOnSystem(executableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool } of tools) {
|
||||||
|
// Remove any existing alias for the tool
|
||||||
|
removeLinesMatchingPattern(
|
||||||
|
startupFile,
|
||||||
|
new RegExp(`^Set-Alias\\s+${tool}\\s+`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool, aikidoCommand } of tools) {
|
||||||
|
addLineToFile(
|
||||||
|
startupFile,
|
||||||
|
`Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartupFile() {
|
||||||
|
try {
|
||||||
|
return execSync(startupFileCommand, {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
}).trim();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: shellName,
|
||||||
|
isInstalled,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
};
|
||||||
204
src/shell-integration/supported-shells/windowsPowershell.spec.js
Normal file
204
src/shell-integration/supported-shells/windowsPowershell.spec.js
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
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";
|
||||||
|
import { knownAikidoTools } from "../helpers.js";
|
||||||
|
|
||||||
|
describe("Windows PowerShell shell integration", () => {
|
||||||
|
let mockStartupFile;
|
||||||
|
let windowsPowershell;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary startup file for testing
|
||||||
|
mockStartupFile = path.join(
|
||||||
|
tmpdir(),
|
||||||
|
`test-windows-powershell-profile-${Date.now()}.ps1`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the helpers module
|
||||||
|
mock.module("../helpers.js", {
|
||||||
|
namedExports: {
|
||||||
|
doesExecutableExistOnSystem: () => true,
|
||||||
|
addLineToFile: (filePath, line) => {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
||||||
|
},
|
||||||
|
removeLinesMatchingPattern: (filePath, pattern) => {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock child_process execSync
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
execSync: () => mockStartupFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import windowsPowershell module after mocking
|
||||||
|
windowsPowershell = (await import("./windowsPowershell.js")).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isInstalled", () => {
|
||||||
|
it("should return true when windows powershell is installed", () => {
|
||||||
|
assert.strictEqual(windowsPowershell.isInstalled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call doesExecutableExistOnSystem with correct parameter", () => {
|
||||||
|
// Test that the method calls the helper with the right executable name
|
||||||
|
assert.strictEqual(windowsPowershell.isInstalled(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
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", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"# Windows PowerShell profile",
|
||||||
|
"Set-Alias npm aikido-npm",
|
||||||
|
"Set-Alias npx aikido-npx",
|
||||||
|
"Set-Alias yarn aikido-yarn",
|
||||||
|
"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("Set-Alias npm "));
|
||||||
|
assert.ok(!content.includes("Set-Alias npx "));
|
||||||
|
assert.ok(!content.includes("Set-Alias yarn "));
|
||||||
|
assert.ok(content.includes("Set-Alias ls "));
|
||||||
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file that doesn't exist", () => {
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = windowsPowershell.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file with no relevant aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"# Windows PowerShell profile",
|
||||||
|
"Set-Alias ls Get-ChildItem",
|
||||||
|
"$env:PATH += ';C:\\Tools'",
|
||||||
|
].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("Set-Alias ls "));
|
||||||
|
assert.ok(content.includes("$env:PATH "));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shell properties", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
assert.strictEqual(windowsPowershell.name, "Windows PowerShell");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose all required methods", () => {
|
||||||
|
assert.ok(typeof windowsPowershell.isInstalled === "function");
|
||||||
|
assert.ok(typeof windowsPowershell.setup === "function");
|
||||||
|
assert.ok(typeof windowsPowershell.teardown === "function");
|
||||||
|
assert.ok(typeof windowsPowershell.name === "string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
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"));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
windowsPowershell.teardown(tools);
|
||||||
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("Set-Alias npm "));
|
||||||
|
assert.ok(!content.includes("Set-Alias yarn "));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple setup calls", () => {
|
||||||
|
const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
|
||||||
|
|
||||||
|
windowsPowershell.setup(tools);
|
||||||
|
windowsPowershell.teardown(tools);
|
||||||
|
windowsPowershell.setup(tools);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
const npmMatches = (content.match(/Set-Alias npm /g) || []).length;
|
||||||
|
assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/shell-integration/supported-shells/zsh.js
Normal file
58
src/shell-integration/supported-shells/zsh.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import {
|
||||||
|
addLineToFile,
|
||||||
|
doesExecutableExistOnSystem,
|
||||||
|
removeLinesMatchingPattern,
|
||||||
|
} from "../helpers.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const shellName = "Zsh";
|
||||||
|
const executableName = "zsh";
|
||||||
|
const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc";
|
||||||
|
|
||||||
|
function isInstalled() {
|
||||||
|
return doesExecutableExistOnSystem(executableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool } of tools) {
|
||||||
|
// Remove any existing alias for the tool
|
||||||
|
removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(tools) {
|
||||||
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
|
for (const { tool, aikidoCommand } of tools) {
|
||||||
|
addLineToFile(
|
||||||
|
startupFile,
|
||||||
|
`alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStartupFile() {
|
||||||
|
try {
|
||||||
|
return execSync(startupFileCommand, {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
}).trim();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: shellName,
|
||||||
|
isInstalled,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
};
|
||||||
199
src/shell-integration/supported-shells/zsh.spec.js
Normal file
199
src/shell-integration/supported-shells/zsh.spec.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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";
|
||||||
|
import { knownAikidoTools } from "../helpers.js";
|
||||||
|
|
||||||
|
describe("Zsh shell integration", () => {
|
||||||
|
let mockStartupFile;
|
||||||
|
let zsh;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary startup file for testing
|
||||||
|
mockStartupFile = path.join(tmpdir(), `test-zshrc-${Date.now()}`);
|
||||||
|
|
||||||
|
// Mock the helpers module
|
||||||
|
mock.module("../helpers.js", {
|
||||||
|
namedExports: {
|
||||||
|
doesExecutableExistOnSystem: () => true,
|
||||||
|
addLineToFile: (filePath, line) => {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, line + "\n", "utf-8");
|
||||||
|
},
|
||||||
|
removeLinesMatchingPattern: (filePath, pattern) => {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock child_process execSync
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
execSync: () => mockStartupFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import zsh module after mocking
|
||||||
|
zsh = (await import("./zsh.js")).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mocks
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isInstalled", () => {
|
||||||
|
it("should return true when zsh is installed", () => {
|
||||||
|
assert.strictEqual(zsh.isInstalled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call doesExecutableExistOnSystem with correct parameter", () => {
|
||||||
|
// Test that the method calls the helper with the right executable name
|
||||||
|
assert.strictEqual(zsh.isInstalled(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = zsh.setup(tools);
|
||||||
|
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')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty tools array", () => {
|
||||||
|
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(), "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("teardown", () => {
|
||||||
|
it("should remove npm, npx, and yarn aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/bin/zsh",
|
||||||
|
"alias npm='aikido-npm'",
|
||||||
|
"alias npx='aikido-npx'",
|
||||||
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias ls='ls --color=auto'",
|
||||||
|
"alias grep='grep --color=auto'",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = zsh.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm="));
|
||||||
|
assert.ok(!content.includes("alias npx="));
|
||||||
|
assert.ok(!content.includes("alias yarn="));
|
||||||
|
assert.ok(content.includes("alias ls="));
|
||||||
|
assert.ok(content.includes("alias grep="));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file that doesn't exist", () => {
|
||||||
|
if (fs.existsSync(mockStartupFile)) {
|
||||||
|
fs.unlinkSync(mockStartupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = zsh.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file with no relevant aliases", () => {
|
||||||
|
const initialContent = [
|
||||||
|
"#!/bin/zsh",
|
||||||
|
"alias ls='ls --color=auto'",
|
||||||
|
"export PATH=$PATH:~/bin",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
||||||
|
|
||||||
|
const result = zsh.teardown(knownAikidoTools);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes("alias ls="));
|
||||||
|
assert.ok(content.includes("export PATH="));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shell properties", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
assert.strictEqual(zsh.name, "Zsh");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose all required methods", () => {
|
||||||
|
assert.ok(typeof zsh.isInstalled === "function");
|
||||||
|
assert.ok(typeof zsh.setup === "function");
|
||||||
|
assert.ok(typeof zsh.teardown === "function");
|
||||||
|
assert.ok(typeof zsh.name === "string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration tests", () => {
|
||||||
|
it("should handle complete setup and teardown cycle", () => {
|
||||||
|
const tools = [
|
||||||
|
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||||
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
zsh.setup(tools);
|
||||||
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(content.includes('alias npm="aikido-npm"'));
|
||||||
|
assert.ok(content.includes('alias yarn="aikido-yarn"'));
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
zsh.teardown(tools);
|
||||||
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
assert.ok(!content.includes("alias npm="));
|
||||||
|
assert.ok(!content.includes("alias yarn="));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple setup calls", () => {
|
||||||
|
const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
|
||||||
|
|
||||||
|
zsh.setup(tools);
|
||||||
|
zsh.teardown(tools);
|
||||||
|
zsh.setup(tools);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
|
const npmMatches = (content.match(/alias npm="/g) || []).length;
|
||||||
|
assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { detectShells } from "./shellDetection.js";
|
import { detectShells } from "./shellDetection.js";
|
||||||
import { getAliases } from "./helpers.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import { EOL } from "os";
|
|
||||||
|
|
||||||
export async function teardown() {
|
export async function teardown() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
|
|
@ -27,8 +24,26 @@ export async function teardown() {
|
||||||
|
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (removeAliasesForShell(shell)) {
|
let success = false;
|
||||||
|
try {
|
||||||
|
success = shell.teardown();
|
||||||
|
} catch {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ui.writeInformation(
|
||||||
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.green(
|
||||||
|
"Teardown successful"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
|
} else {
|
||||||
|
ui.writeError(
|
||||||
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
||||||
|
"Teardown failed"
|
||||||
|
)}. Please check your ${shell.name} configuration.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,98 +58,3 @@ export async function teardown() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function removes aliases for the given shell.
|
|
||||||
* It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.),
|
|
||||||
* and then removes the aliases for npm, npx, and yarn commands.
|
|
||||||
* If the aliases don't exist, it will report that they were not found.
|
|
||||||
* If the startup file does not exist, it will report that no aliases need to be removed.
|
|
||||||
*
|
|
||||||
* The shell startup script is loaded by the respective shell when it starts.
|
|
||||||
* This means that the aliases will be removed from the shell after it is restarted.
|
|
||||||
*/
|
|
||||||
function removeAliasesForShell(shell) {
|
|
||||||
if (!shell.startupFile) {
|
|
||||||
ui.writeError(
|
|
||||||
`- ${chalk.bold(
|
|
||||||
shell.name
|
|
||||||
)}: no startup file found. Cannot remove aliases.`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(shell.startupFile)) {
|
|
||||||
ui.writeInformation(
|
|
||||||
`- ${chalk.bold(
|
|
||||||
shell.name
|
|
||||||
)}: startup file does not exist. No aliases to remove.`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aliases = getAliases(shell.startupFile);
|
|
||||||
const fileContent = fs.readFileSync(shell.startupFile, "utf-8");
|
|
||||||
const { removedCount, notFoundCount } = removeAliasesFromFile(
|
|
||||||
aliases,
|
|
||||||
fileContent,
|
|
||||||
shell.startupFile
|
|
||||||
);
|
|
||||||
|
|
||||||
let summary = "- " + chalk.bold(shell.name) + ": ";
|
|
||||||
|
|
||||||
if (removedCount > 0) {
|
|
||||||
summary += chalk.green(`${removedCount} aliases were removed`);
|
|
||||||
}
|
|
||||||
if (notFoundCount > 0) {
|
|
||||||
if (removedCount > 0) {
|
|
||||||
summary += ", ";
|
|
||||||
}
|
|
||||||
summary += chalk.yellow(`${notFoundCount} aliases were not found`);
|
|
||||||
}
|
|
||||||
if (removedCount === 0 && notFoundCount === 0) {
|
|
||||||
summary += chalk.yellow("no aliases found to remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.writeInformation(summary);
|
|
||||||
return removedCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function removes the aliases from the startup file.
|
|
||||||
* It searches for exact matches of each alias line and removes them.
|
|
||||||
* eg: for bash it will remove 'alias npm="aikido-npm"' for npm from ~/.bashrc
|
|
||||||
* @returns an object with the counts of removed and not found aliases.
|
|
||||||
*/
|
|
||||||
export function removeAliasesFromFile(aliases, fileContent, startupFilePath) {
|
|
||||||
let removedCount = 0;
|
|
||||||
let notFoundCount = 0;
|
|
||||||
let updatedContent = fileContent;
|
|
||||||
|
|
||||||
for (const alias of aliases) {
|
|
||||||
const lines = updatedContent.split(EOL);
|
|
||||||
let aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
|
|
||||||
|
|
||||||
if (aliasLineIndex !== -1) {
|
|
||||||
removedCount++;
|
|
||||||
|
|
||||||
// Remove all occurrences of the alias line, in case it appears multiple times
|
|
||||||
while (aliasLineIndex !== -1) {
|
|
||||||
lines.splice(aliasLineIndex, 1);
|
|
||||||
aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
|
|
||||||
}
|
|
||||||
updatedContent = lines.join(EOL);
|
|
||||||
} else {
|
|
||||||
notFoundCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedCount > 0) {
|
|
||||||
fs.writeFileSync(startupFilePath, updatedContent, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
removedCount,
|
|
||||||
notFoundCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import { EOL, tmpdir } from "node:os";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { getAliases } from "./helpers.js";
|
|
||||||
import { removeAliasesFromFile } from "./teardown.js";
|
|
||||||
|
|
||||||
describe("teardown", () => {
|
|
||||||
function runRemovalTestsForEnvironment(
|
|
||||||
shell,
|
|
||||||
startupExtension,
|
|
||||||
expectedAliases
|
|
||||||
) {
|
|
||||||
describe(`${shell} shell removal`, () => {
|
|
||||||
it(`should remove aliases from ${shell} file`, () => {
|
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
// Test the removeAliasesFromFile function directly
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.removedCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
"Should remove all aliases"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.ok(
|
|
||||||
!updatedContent.includes(alias),
|
|
||||||
`Alias "${alias}" should be removed`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should handle file with no aliases for ${shell}`, () => {
|
|
||||||
const lines = [
|
|
||||||
`#!/usr/bin/env ${shell}`,
|
|
||||||
"",
|
|
||||||
"alias other='command'",
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.notFoundCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
"Should report all aliases not found"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias other='command'"),
|
|
||||||
"Other aliases should remain unchanged"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should remove duplicate aliases from ${shell} file`, () => {
|
|
||||||
const lines = [
|
|
||||||
`#!/usr/bin/env ${shell}`,
|
|
||||||
"",
|
|
||||||
...expectedAliases,
|
|
||||||
"alias other='command'",
|
|
||||||
...expectedAliases, // duplicates
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.removedCount,
|
|
||||||
expectedAliases.length,
|
|
||||||
"Should remove all aliases (counting duplicates as single removal)"
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
for (const alias of expectedAliases) {
|
|
||||||
assert.ok(
|
|
||||||
!updatedContent.includes(alias),
|
|
||||||
`Alias "${alias}" should be completely removed`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias other='command'"),
|
|
||||||
"Other aliases should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should use real getAliases() for ${shell} file`, () => {
|
|
||||||
const filePath = `${tmpdir()}/test${startupExtension}`;
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
|
|
||||||
// Verify we get the expected aliases for this shell type
|
|
||||||
assert.strictEqual(
|
|
||||||
aliases.length,
|
|
||||||
expectedAliases.length,
|
|
||||||
"Should get all aliases (npm, npx, yarn)"
|
|
||||||
);
|
|
||||||
for (let i = 0; i < aliases.length; i++) {
|
|
||||||
assert.strictEqual(
|
|
||||||
aliases[i],
|
|
||||||
expectedAliases[i],
|
|
||||||
`Alias ${i} should match expected format`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should handle partial alias matches for ${shell}`, () => {
|
|
||||||
const lines = [
|
|
||||||
`#!/usr/bin/env ${shell}`,
|
|
||||||
"",
|
|
||||||
expectedAliases[0], // Only first alias
|
|
||||||
"alias other='command'",
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 1, "Should remove 1 alias");
|
|
||||||
assert.strictEqual(
|
|
||||||
result.notFoundCount,
|
|
||||||
expectedAliases.length - 1,
|
|
||||||
"Should report all aliases not found"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
|
||||||
assert.ok(
|
|
||||||
!updatedContent.includes(expectedAliases[0]),
|
|
||||||
"First alias should be removed"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
updatedContent.includes("alias other='command'"),
|
|
||||||
"Other aliases should remain"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for each shell type using real getAliases() output
|
|
||||||
runRemovalTestsForEnvironment("bash", ".bashrc", [
|
|
||||||
"alias npm='aikido-npm'",
|
|
||||||
"alias npx='aikido-npx'",
|
|
||||||
"alias yarn='aikido-yarn'",
|
|
||||||
"alias pnpm='aikido-pnpm'",
|
|
||||||
"alias pnpx='aikido-pnpx'",
|
|
||||||
]);
|
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("zsh", ".zshrc", [
|
|
||||||
"alias npm='aikido-npm'",
|
|
||||||
"alias npx='aikido-npx'",
|
|
||||||
"alias yarn='aikido-yarn'",
|
|
||||||
"alias pnpm='aikido-pnpm'",
|
|
||||||
"alias pnpx='aikido-pnpx'",
|
|
||||||
]);
|
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("fish", ".fish", [
|
|
||||||
'alias npm "aikido-npm"',
|
|
||||||
'alias npx "aikido-npx"',
|
|
||||||
'alias yarn "aikido-yarn"',
|
|
||||||
'alias pnpm "aikido-pnpm"',
|
|
||||||
'alias pnpx "aikido-pnpx"',
|
|
||||||
]);
|
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("pwsh", ".ps1", [
|
|
||||||
"Set-Alias npm aikido-npm",
|
|
||||||
"Set-Alias npx aikido-npx",
|
|
||||||
"Set-Alias yarn aikido-yarn",
|
|
||||||
"Set-Alias pnpm aikido-pnpm",
|
|
||||||
"Set-Alias pnpx aikido-pnpx",
|
|
||||||
]);
|
|
||||||
|
|
||||||
describe("removeAliasesFromFile edge cases", () => {
|
|
||||||
it("should handle empty file", () => {
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
|
||||||
const fileContent = "";
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.removedCount,
|
|
||||||
0,
|
|
||||||
"Should remove 0 aliases from empty file"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.notFoundCount,
|
|
||||||
1,
|
|
||||||
"Should report 1 alias not found"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle file with only whitespace", () => {
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
|
||||||
const fileContent = `${EOL}${EOL} ${EOL}`;
|
|
||||||
const filePath = `${tmpdir()}/test-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 15)}.bashrc`;
|
|
||||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
result.removedCount,
|
|
||||||
0,
|
|
||||||
"Should remove 0 aliases from whitespace-only file"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
result.notFoundCount,
|
|
||||||
1,
|
|
||||||
"Should report 1 alias not found"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createShellStartupScript(lines, fileExtension) {
|
|
||||||
const randomFileName = Math.random().toString(36).substring(2, 15);
|
|
||||||
const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
|
|
||||||
fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAndDeleteFile(filePath) {
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
||||||
fs.rmSync(filePath, { force: true });
|
|
||||||
return fileContent.split(EOL);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue