diff --git a/src/shell-integration/helpers.js b/src/shell-integration/helpers.js index 5b7c4d6..7808c7e 100644 --- a/src/shell-integration/helpers.js +++ b/src/shell-integration/helpers.js @@ -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: "npx", aikidoCommand: "aikido-npx" }, { tool: "yarn", aikidoCommand: "aikido-yarn" }, @@ -8,39 +12,33 @@ const knownAikidoTools = [ // and add the documentation for the new tool in the README.md ]; -export function getAliases(fileName) { - const fileExtension = fileName.split(".").pop().toLowerCase(); - - let createAlias = pickCreateAliasFunction(fileExtension); - - const aliases = knownAikidoTools.map(({ tool, aikidoCommand }) => - createAlias(tool, aikidoCommand) - ); - - return aliases; -} - -function pickCreateAliasFunction(fileExtension) { - let createAlias; - switch (fileExtension) { - case "ps1": - createAlias = createGeneralPowershellAlias; - break; - case "fish": - createAlias = createGeneralFishAlias; - break; - default: - createAlias = createGeneralPosixAlias; +export function doesExecutableExistOnSystem(executableName) { + if (os.platform() === "win32") { + const result = spawnSync("where", [executableName], { stdio: "ignore" }); + return result.status === 0; + } else { + const result = spawnSync("which", [executableName], { stdio: "ignore" }); + return result.status === 0; } - return createAlias; } -function createGeneralPosixAlias(tool, aikidoCommand) { - return `alias ${tool}='${aikidoCommand}'`; +export function removeLinesMatchingPattern(filePath, pattern) { + 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}`; -} -function createGeneralFishAlias(tool, aikidoCommand) { - return `alias ${tool} "${aikidoCommand}"`; + +export function addLineToFile(filePath, line) { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, "", "utf-8"); + } + + const fileContent = fs.readFileSync(filePath, "utf-8"); + const updatedContent = fileContent + os.EOL + line; + fs.writeFileSync(filePath, updatedContent, "utf-8"); } diff --git a/src/shell-integration/setup.js b/src/shell-integration/setup.js index d6a2566..e5be973 100644 --- a/src/shell-integration/setup.js +++ b/src/shell-integration/setup.js @@ -1,10 +1,11 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { getAliases } from "./helpers.js"; -import fs from "fs"; -import { EOL } from "os"; +import { knownAikidoTools } from "./helpers.js"; +/** + * Loops over the detected shells and calls the setup function for each. + */ export async function setup() { ui.writeInformation( chalk.bold("Setting up shell aliases.") + @@ -27,7 +28,7 @@ export async function setup() { let updatedCount = 0; for (const shell of shells) { - if (setupAliasesForShell(shell)) { + if (setupShell(shell)) { updatedCount++; } } @@ -45,107 +46,30 @@ export async function setup() { } /** - * This function sets up aliases for the given shell. - * 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. + * Calls the setup function for the given shell and reports the result. */ -function setupAliasesForShell(shell) { - if (!shell.startupFile) { - ui.writeError( - `- ${chalk.bold( - shell.name - )}: no startup file found. Cannot set up aliases.` +function setupShell(shell) { + let success = false; + try { + shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases + success = shell.setup(knownAikidoTools); + } 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); - - 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, - }; + return success; } diff --git a/src/shell-integration/setup.spec.js b/src/shell-integration/setup.spec.js deleted file mode 100644 index 3c11136..0000000 --- a/src/shell-integration/setup.spec.js +++ /dev/null @@ -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; -} diff --git a/src/shell-integration/shellDetection.js b/src/shell-integration/shellDetection.js index de38680..d868f6f 100644 --- a/src/shell-integration/shellDetection.js +++ b/src/shell-integration/shellDetection.js @@ -1,75 +1,26 @@ -import * as os from "os"; -import { execSync } from "child_process"; - -const shellList = { - bash: { - name: "Bash", - 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", - }, -}; +import zsh from "./supported-shells/zsh.js"; +import bash from "./supported-shells/bash.js"; +import powershell from "./supported-shells/powershell.js"; +import windowsPowershell from "./supported-shells/windowsPowershell.js"; +import fish from "./supported-shells/fish.js"; +import { ui } from "../environment/userInteraction.js"; export function detectShells() { + let possibleShells = [zsh, bash, powershell, windowsPowershell, fish]; let availableShells = []; - for (const shellName of Object.keys(shellList)) { - const shell = shellList[shellName]; - - if (isShellAvailable(shell)) { - const startupFile = getShellStartupFile(shell); - availableShells.push({ - name: shell.name, - executable: shell.executable, - startupFile: startupFile || null, - }); + try { + for (const shell of possibleShells) { + if (shell.isInstalled()) { + availableShells.push(shell); + } } + } 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; } - -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; - } -} diff --git a/src/shell-integration/supported-shells/bash.js b/src/shell-integration/supported-shells/bash.js new file mode 100644 index 0000000..66b844d --- /dev/null +++ b/src/shell-integration/supported-shells/bash.js @@ -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, +}; diff --git a/src/shell-integration/supported-shells/bash.spec.js b/src/shell-integration/supported-shells/bash.spec.js new file mode 100644 index 0000000..ce666e5 --- /dev/null +++ b/src/shell-integration/supported-shells/bash.spec.js @@ -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"); + }); + }); +}); diff --git a/src/shell-integration/supported-shells/fish.js b/src/shell-integration/supported-shells/fish.js new file mode 100644 index 0000000..fc6fc85 --- /dev/null +++ b/src/shell-integration/supported-shells/fish.js @@ -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, +}; diff --git a/src/shell-integration/supported-shells/fish.spec.js b/src/shell-integration/supported-shells/fish.spec.js new file mode 100644 index 0000000..5f1ab64 --- /dev/null +++ b/src/shell-integration/supported-shells/fish.spec.js @@ -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"); + }); + }); +}); diff --git a/src/shell-integration/supported-shells/powershell.js b/src/shell-integration/supported-shells/powershell.js new file mode 100644 index 0000000..4690bb6 --- /dev/null +++ b/src/shell-integration/supported-shells/powershell.js @@ -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, +}; diff --git a/src/shell-integration/supported-shells/powershell.spec.js b/src/shell-integration/supported-shells/powershell.spec.js new file mode 100644 index 0000000..9afade7 --- /dev/null +++ b/src/shell-integration/supported-shells/powershell.spec.js @@ -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"); + }); + }); +}); diff --git a/src/shell-integration/supported-shells/windowsPowershell.js b/src/shell-integration/supported-shells/windowsPowershell.js new file mode 100644 index 0000000..118a0b9 --- /dev/null +++ b/src/shell-integration/supported-shells/windowsPowershell.js @@ -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, +}; diff --git a/src/shell-integration/supported-shells/windowsPowershell.spec.js b/src/shell-integration/supported-shells/windowsPowershell.spec.js new file mode 100644 index 0000000..85da9f1 --- /dev/null +++ b/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -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"); + }); + }); +}); diff --git a/src/shell-integration/supported-shells/zsh.js b/src/shell-integration/supported-shells/zsh.js new file mode 100644 index 0000000..965c814 --- /dev/null +++ b/src/shell-integration/supported-shells/zsh.js @@ -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, +}; diff --git a/src/shell-integration/supported-shells/zsh.spec.js b/src/shell-integration/supported-shells/zsh.spec.js new file mode 100644 index 0000000..e284c50 --- /dev/null +++ b/src/shell-integration/supported-shells/zsh.spec.js @@ -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"); + }); + }); +}); diff --git a/src/shell-integration/teardown.js b/src/shell-integration/teardown.js index ecbee87..00a933f 100644 --- a/src/shell-integration/teardown.js +++ b/src/shell-integration/teardown.js @@ -1,9 +1,6 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { getAliases } from "./helpers.js"; -import fs from "fs"; -import { EOL } from "os"; export async function teardown() { ui.writeInformation( @@ -27,8 +24,26 @@ export async function teardown() { let updatedCount = 0; 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++; + } 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; } } - -/** - * 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, - }; -} diff --git a/src/shell-integration/teardown.spec.js b/src/shell-integration/teardown.spec.js deleted file mode 100644 index 5077a99..0000000 --- a/src/shell-integration/teardown.spec.js +++ /dev/null @@ -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); -}