mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #1 from AikidoSec/pnpm-support
Add pnpm and pnpx support
This commit is contained in:
commit
fdef99931e
16 changed files with 766 additions and 156 deletions
16
README.md
16
README.md
|
|
@ -1,8 +1,8 @@
|
|||
# Aikido Safe Chain
|
||||
|
||||
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, or yarn.
|
||||
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx.
|
||||
|
||||
The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), and [yarn](https://yarnpkg.com/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, or yarn from downloading or running the malware.
|
||||
The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware.
|
||||
|
||||

|
||||
|
||||
|
|
@ -11,7 +11,9 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi
|
|||
- ✅ **npm**
|
||||
- ✅ **npx**
|
||||
- ✅ **yarn**
|
||||
- 🚧 **pnpm** Coming soon
|
||||
- ✅ **pnpm**
|
||||
- ✅ **pnpx**
|
||||
- 🚧 **bun** Coming soon
|
||||
|
||||
# Usage
|
||||
|
||||
|
|
@ -28,20 +30,20 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
|||
safe-chain setup
|
||||
```
|
||||
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
||||
- This step is crucial as it ensures that the shell aliases for npm, npx, and yarn are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm and pnpx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||
4. **Verify the installation** by running:
|
||||
```shell
|
||||
npm install eslint-js
|
||||
```
|
||||
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
|
||||
|
||||
When running `npm`, `npx`, or `yarn` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
|
||||
When running `npm`, `npx`, `yarn`, `pnpm` or `pnpx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
|
||||
|
||||
## How it works
|
||||
|
||||
The Aikido Safe Chain works by intercepting the npm, npx, and yarn commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**.
|
||||
The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm and pnpx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**.
|
||||
|
||||
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, and yarn commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support:
|
||||
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm and pnpx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support:
|
||||
|
||||
- ✅ **Bash**
|
||||
- ✅ **Zsh**
|
||||
|
|
|
|||
8
bin/aikido-pnpm.js
Executable file
8
bin/aikido-pnpm.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { main } from "../src/main.js";
|
||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||
|
||||
const packageManagerName = "pnpm";
|
||||
initializePackageManager(packageManagerName, process.versions.node);
|
||||
await main(process.argv.slice(2));
|
||||
8
bin/aikido-pnpx.js
Executable file
8
bin/aikido-pnpx.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { main } from "../src/main.js";
|
||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||
|
||||
const packageManagerName = "pnpx";
|
||||
initializePackageManager(packageManagerName, process.versions.node);
|
||||
await main(process.argv.slice(2));
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
|
||||
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
|
||||
|
||||
## Supported Shells
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ safe-chain setup
|
|||
This command:
|
||||
|
||||
- Detects all supported shells on your system
|
||||
- Adds aliases for `npm`, `npx`, and `yarn` to each shell's startup file
|
||||
- Adds aliases for `npm`, `npx`, `yarn`, `pnpm` and `pnpx` to each shell's startup file
|
||||
|
||||
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the aliases are loaded correctly.
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ The system modifies the following files based on your shell configuration:
|
|||
This means the aliases are working but the Aikido commands aren't installed or available in your PATH:
|
||||
|
||||
- Make sure Aikido Safe Chain is properly installed on your system
|
||||
- Verify the `aikido-npm`, `aikido-npx`, and `aikido-yarn` commands exist
|
||||
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist
|
||||
- Check that these commands are in your system's PATH
|
||||
|
||||
### Manual Verification
|
||||
|
|
@ -105,4 +105,4 @@ To verify the integration is working, follow these steps:
|
|||
|
||||
3. **If you need to remove aliases manually:**
|
||||
|
||||
Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, or `aikido-yarn`.
|
||||
Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` or `aikido-pnpx`.
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -19,6 +19,8 @@
|
|||
"bin": {
|
||||
"aikido-npm": "bin/aikido-npm.js",
|
||||
"aikido-npx": "bin/aikido-npx.js",
|
||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||
"aikido-yarn": "bin/aikido-yarn.js",
|
||||
"safe-chain": "bin/safe-chain.js"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"aikido-npm": "bin/aikido-npm.js",
|
||||
"aikido-npx": "bin/aikido-npx.js",
|
||||
"aikido-yarn": "bin/aikido-yarn.js",
|
||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||
"safe-chain": "bin/safe-chain.js"
|
||||
},
|
||||
"type": "module",
|
||||
|
|
|
|||
13
src/packagemanager/_shared/matchesCommand.js
Normal file
13
src/packagemanager/_shared/matchesCommand.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export function matchesCommand(args, ...commandArgs) {
|
||||
if (args.length < commandArgs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < commandArgs.length; i++) {
|
||||
if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { createNpmPackageManager } from "./npm/createPackageManager.js";
|
||||
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
||||
import {
|
||||
createPnpmPackageManager,
|
||||
createPnpxPackageManager,
|
||||
} from "./pnpm/createPackageManager.js";
|
||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||
|
||||
const state = {
|
||||
|
|
@ -13,6 +17,10 @@ export function initializePackageManager(packageManagerName, version) {
|
|||
state.packageManagerName = createNpxPackageManager();
|
||||
} else if (packageManagerName === "yarn") {
|
||||
state.packageManagerName = createYarnPackageManager();
|
||||
} else if (packageManagerName === "pnpm") {
|
||||
state.packageManagerName = createPnpmPackageManager();
|
||||
} else if (packageManagerName === "pnpx") {
|
||||
state.packageManagerName = createPnpxPackageManager();
|
||||
} else {
|
||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||
}
|
||||
|
|
|
|||
46
src/packagemanager/pnpm/createPackageManager.js
Normal file
46
src/packagemanager/pnpm/createPackageManager.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { matchesCommand } from "../_shared/matchesCommand.js";
|
||||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||
import { runPnpmCommand } from "./runPnpmCommand.js";
|
||||
|
||||
const scanner = commandArgumentScanner();
|
||||
|
||||
export function createPnpmPackageManager() {
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
||||
isSupportedCommand: (args) =>
|
||||
matchesCommand(args, "add") ||
|
||||
matchesCommand(args, "update") ||
|
||||
matchesCommand(args, "upgrade") ||
|
||||
matchesCommand(args, "up") ||
|
||||
// dlx does not always come in the first position
|
||||
// eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
|
||||
// documentation: https://pnpm.io/cli/dlx#--package-name
|
||||
args.includes("dlx"),
|
||||
getDependencyUpdatesForCommand: (args) =>
|
||||
getDependencyUpdatesForCommand(args, false),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPnpxPackageManager() {
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
||||
isSupportedCommand: () => true,
|
||||
getDependencyUpdatesForCommand: (args) =>
|
||||
getDependencyUpdatesForCommand(args, true),
|
||||
};
|
||||
}
|
||||
|
||||
function getDependencyUpdatesForCommand(args, isPnpx) {
|
||||
if (isPnpx) {
|
||||
return scanner.scan(args);
|
||||
}
|
||||
if (args.includes("dlx")) {
|
||||
// dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
|
||||
// so we need to filter it out instead of slicing the array
|
||||
// documentation: https://pnpm.io/cli/dlx#--package-name
|
||||
return scanner.scan(args.filter((arg) => arg !== "dlx"));
|
||||
}
|
||||
return scanner.scan(args.slice(1));
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||
|
||||
export function commandArgumentScanner() {
|
||||
return {
|
||||
scan: (args) => scanDependencies(args),
|
||||
shouldScan: () => true, // There's no dry run for pnpm, so we always scan
|
||||
};
|
||||
}
|
||||
|
||||
async function scanDependencies(args) {
|
||||
const changes = [];
|
||||
const packageUpdates = parsePackagesFromArguments(args);
|
||||
|
||||
for (const packageUpdate of packageUpdates) {
|
||||
var exactVersion = await resolvePackageVersion(
|
||||
packageUpdate.name,
|
||||
packageUpdate.version
|
||||
);
|
||||
if (exactVersion) {
|
||||
packageUpdate.version = exactVersion;
|
||||
}
|
||||
|
||||
changes.push({ ...packageUpdate, type: "add" });
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
export function parsePackagesFromArguments(args) {
|
||||
const changes = [];
|
||||
let defaultTag = "latest";
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const option = getOption(arg);
|
||||
|
||||
if (option) {
|
||||
// If the option has a parameter, skip the next argument as well
|
||||
i += option.numberOfParameters;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDetails = parsePackagename(arg, defaultTag);
|
||||
if (packageDetails) {
|
||||
changes.push(packageDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function getOption(arg) {
|
||||
if (isOptionWithParameter(arg)) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Arguments starting with "-" or "--" are considered options
|
||||
// except for "--package=" which contains the package name
|
||||
if (arg.startsWith("-") && !arg.startsWith("--package=")) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOptionWithParameter(arg) {
|
||||
const optionsWithParameters = ["--C", "--dir"];
|
||||
|
||||
return optionsWithParameters.includes(arg);
|
||||
}
|
||||
|
||||
function parsePackagename(arg, defaultTag) {
|
||||
// format can be --package=name@version
|
||||
// in that case, we need to remove the --package= part
|
||||
if (arg.startsWith("--package=")) {
|
||||
arg = arg.slice(10);
|
||||
}
|
||||
|
||||
arg = removeAlias(arg);
|
||||
|
||||
// Split at the last "@" to separate the package name and version
|
||||
const lastAtIndex = arg.lastIndexOf("@");
|
||||
|
||||
let name, version;
|
||||
// The index of the last "@" should be greater than 0
|
||||
// If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name")
|
||||
if (lastAtIndex > 0) {
|
||||
name = arg.slice(0, lastAtIndex);
|
||||
version = arg.slice(lastAtIndex + 1);
|
||||
} else {
|
||||
name = arg;
|
||||
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function removeAlias(arg) {
|
||||
// removes the alias.
|
||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||
const aliasIndex = arg.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return arg.slice(aliasIndex + 5);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
|
||||
|
||||
describe("standardPnpmArgumentParser", () => {
|
||||
it("should return an empty array for no changes", () => {
|
||||
const args = [];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should return an array of changes for one package", () => {
|
||||
const args = ["axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if absent", () => {
|
||||
const args = ["axios"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if the version is absent and package starts with @", () => {
|
||||
const args = ["@aikidosec/package-name"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "@aikidosec/package-name", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return the package with the specified tag if the package starts with @ and includes the version", () => {
|
||||
const args = ["@aikidosec/package-name@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "@aikidosec/package-name", version: "1.0.0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should only return all packages", () => {
|
||||
const args = ["axios", "jest"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "axios", version: "latest" },
|
||||
{ name: "jest", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore options with parameters and return an array of changes", () => {
|
||||
const args = ["--C", "/Users/johnsmith/dev/project", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse version even for aliased packages", () => {
|
||||
const args = ["server@npm:axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse scoped packages", () => {
|
||||
const args = ["@scope/package@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with version ranges", () => {
|
||||
const args = ["axios@^1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse package folders", () => {
|
||||
const args = ["./local-package"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should parse tarballs", () => {
|
||||
const args = ["file:./local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "file:./local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse tarball URLs", () => {
|
||||
const args = ["https://example.com/local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse git URLs", () => {
|
||||
const args = ["git://github.com/http-party/http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "git://github.com/http-party/http-server", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse packages with --package={packageName}", () => {
|
||||
const args = ["--package=axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
});
|
||||
24
src/packagemanager/pnpm/runPnpmCommand.js
Normal file
24
src/packagemanager/pnpm/runPnpmCommand.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { spawnSync } from "child_process";
|
||||
import { ui } from "../../environment/userInteraction.js";
|
||||
|
||||
export function runPnpmCommand(args, toolName = "pnpm") {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (toolName === "pnpm") {
|
||||
result = spawnSync("pnpm", args, { stdio: "inherit" });
|
||||
} else if (toolName === "pnpx") {
|
||||
result = spawnSync("pnpx", args, { stdio: "inherit" });
|
||||
} else {
|
||||
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
||||
}
|
||||
|
||||
if (result.status !== null) {
|
||||
return { status: result.status };
|
||||
}
|
||||
} catch (error) {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
return { status: 0 };
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ const knownAikidoTools = [
|
|||
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||
// When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js)
|
||||
{ tool: "pnpm", aikidoCommand: "aikido-pnpm" },
|
||||
{ tool: "pnpx", aikidoCommand: "aikido-pnpx" },
|
||||
// When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js)
|
||||
// and add the documentation for the new tool in the README.md
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { getAliases } from "./helpers.js";
|
|||
import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js";
|
||||
|
||||
describe("setupShell", () => {
|
||||
function runSetupTestsForEnvironment(shell, startupExtension, expectedAliases) {
|
||||
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'"];
|
||||
|
|
@ -17,15 +21,33 @@ describe("setupShell", () => {
|
|||
|
||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
|
||||
assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
|
||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
||||
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),
|
||||
`Alias "${alias}" should be added`
|
||||
);
|
||||
}
|
||||
assert.ok(updatedContent.includes("alias cls='clear'"), "Original aliases should remain");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias cls='clear'"),
|
||||
"Original aliases should remain"
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not add aliases if they already exist in ${shell} file`, () => {
|
||||
|
|
@ -38,13 +60,25 @@ describe("setupShell", () => {
|
|||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
||||
assert.strictEqual(result.existingCount, 3, "Should find 3 existing aliases");
|
||||
assert.strictEqual(result.failedCount, 0, "Should have no failed 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`);
|
||||
assert.strictEqual(
|
||||
countOccurrences(updatedContent, alias),
|
||||
1,
|
||||
`Alias "${alias}" should appear exactly once`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -57,20 +91,39 @@ describe("setupShell", () => {
|
|||
|
||||
// Test readOrCreateStartupFile function
|
||||
const fileContent = readOrCreateStartupFile(filePath);
|
||||
assert.strictEqual(fileContent, "", "Should return empty string for new file");
|
||||
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, 3, "Should add 3 aliases");
|
||||
assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
|
||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
||||
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),
|
||||
`Alias "${alias}" should be added`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -83,17 +136,33 @@ describe("setupShell", () => {
|
|||
// First call - should add aliases
|
||||
let fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
const result1 = appendAliasesToFile(aliases, fileContent, filePath);
|
||||
assert.strictEqual(result1.addedCount, 3, "First call should add 3 aliases");
|
||||
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, 3, "Second call should find 3 existing aliases");
|
||||
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`);
|
||||
assert.strictEqual(
|
||||
countOccurrences(updatedContent, alias),
|
||||
1,
|
||||
`Alias "${alias}" should appear exactly once`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -102,14 +171,27 @@ describe("setupShell", () => {
|
|||
const aliases = getAliases(filePath);
|
||||
|
||||
// Verify we get the expected aliases for this shell type
|
||||
assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)");
|
||||
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`);
|
||||
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 lines = [
|
||||
`#!/usr/bin/env ${shell}`,
|
||||
"",
|
||||
expectedAliases[0],
|
||||
"alias other='command'",
|
||||
];
|
||||
const filePath = createShellStartupScript(lines, startupExtension);
|
||||
|
||||
const aliases = getAliases(filePath);
|
||||
|
|
@ -117,15 +199,33 @@ describe("setupShell", () => {
|
|||
|
||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.addedCount, 2, "Should add 2 new aliases");
|
||||
assert.strictEqual(result.existingCount, 1, "Should find 1 existing alias");
|
||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
||||
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),
|
||||
`Alias "${alias}" should be present`
|
||||
);
|
||||
}
|
||||
assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias other='command'"),
|
||||
"Other aliases should remain"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -134,25 +234,33 @@ describe("setupShell", () => {
|
|||
runSetupTestsForEnvironment("bash", ".bashrc", [
|
||||
"alias npm='aikido-npm'",
|
||||
"alias npx='aikido-npx'",
|
||||
"alias yarn='aikido-yarn'"
|
||||
"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 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 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 yarn aikido-yarn",
|
||||
"Set-Alias pnpm aikido-pnpm",
|
||||
"Set-Alias pnpx aikido-pnpx",
|
||||
]);
|
||||
|
||||
describe("readOrCreateStartupFile", () => {
|
||||
|
|
@ -162,22 +270,34 @@ describe("setupShell", () => {
|
|||
|
||||
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");
|
||||
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`;
|
||||
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.strictEqual(
|
||||
content,
|
||||
"",
|
||||
"Should return empty string for new file"
|
||||
);
|
||||
assert.ok(fs.existsSync(filePath), "File should be created");
|
||||
|
||||
// Cleanup
|
||||
|
|
@ -185,12 +305,18 @@ describe("setupShell", () => {
|
|||
});
|
||||
|
||||
it("should handle empty existing file", () => {
|
||||
const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
|
||||
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.strictEqual(
|
||||
content,
|
||||
"",
|
||||
"Should return empty string for empty file"
|
||||
);
|
||||
assert.ok(fs.existsSync(filePath), "File should still exist");
|
||||
|
||||
// Cleanup
|
||||
|
|
@ -207,11 +333,18 @@ describe("setupShell", () => {
|
|||
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.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");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias test='echo test'"),
|
||||
"Original content should remain"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle partial substring matches correctly", () => {
|
||||
|
|
@ -219,7 +352,7 @@ describe("setupShell", () => {
|
|||
"#!/usr/bin/env bash",
|
||||
"",
|
||||
"alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm='
|
||||
"alias test='echo test'"
|
||||
"alias test='echo test'",
|
||||
];
|
||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
|
|
@ -228,16 +361,28 @@ describe("setupShell", () => {
|
|||
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.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");
|
||||
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 filePath = `${tmpdir()}/test-${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}.bashrc`;
|
||||
const fileContent = `${EOL}${EOL} ${EOL}`;
|
||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||
|
||||
|
|
@ -245,11 +390,18 @@ describe("setupShell", () => {
|
|||
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.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");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias npm='aikido-npm'"),
|
||||
"Alias should be added"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
fs.rmSync(filePath, { force: true });
|
||||
|
|
@ -258,7 +410,9 @@ describe("setupShell", () => {
|
|||
|
||||
describe("appendAliasesToFile error handling", () => {
|
||||
it("should handle file permission errors gracefully", () => {
|
||||
const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
|
||||
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
|
||||
|
|
@ -269,8 +423,16 @@ describe("setupShell", () => {
|
|||
|
||||
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.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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { getAliases } from "./helpers.js";
|
|||
import { removeAliasesFromFile } from "./teardown.js";
|
||||
|
||||
describe("teardown", () => {
|
||||
function runRemovalTestsForEnvironment(shell, startupExtension, expectedAliases) {
|
||||
function runRemovalTestsForEnvironment(
|
||||
shell,
|
||||
startupExtension,
|
||||
expectedAliases
|
||||
) {
|
||||
describe(`${shell} shell removal`, () => {
|
||||
it(`should remove aliases from ${shell} file`, () => {
|
||||
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""];
|
||||
|
|
@ -18,17 +22,29 @@ describe("teardown", () => {
|
|||
|
||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases");
|
||||
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`);
|
||||
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 lines = [
|
||||
`#!/usr/bin/env ${shell}`,
|
||||
"",
|
||||
"alias other='command'",
|
||||
"",
|
||||
];
|
||||
const filePath = createShellStartupScript(lines, startupExtension);
|
||||
|
||||
const aliases = getAliases(filePath);
|
||||
|
|
@ -37,10 +53,17 @@ describe("teardown", () => {
|
|||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases");
|
||||
assert.strictEqual(result.notFoundCount, 3, "Should report 3 aliases not found");
|
||||
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");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias other='command'"),
|
||||
"Other aliases should remain unchanged"
|
||||
);
|
||||
});
|
||||
|
||||
it(`should remove duplicate aliases from ${shell} file`, () => {
|
||||
|
|
@ -50,7 +73,7 @@ describe("teardown", () => {
|
|||
...expectedAliases,
|
||||
"alias other='command'",
|
||||
...expectedAliases, // duplicates
|
||||
""
|
||||
"",
|
||||
];
|
||||
const filePath = createShellStartupScript(lines, startupExtension);
|
||||
|
||||
|
|
@ -59,14 +82,24 @@ describe("teardown", () => {
|
|||
|
||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases (counting duplicates as single removal)");
|
||||
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),
|
||||
`Alias "${alias}" should be completely removed`
|
||||
);
|
||||
}
|
||||
assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
|
||||
assert.ok(
|
||||
updatedContent.includes("alias other='command'"),
|
||||
"Other aliases should remain"
|
||||
);
|
||||
});
|
||||
|
||||
it(`should use real getAliases() for ${shell} file`, () => {
|
||||
|
|
@ -74,9 +107,17 @@ describe("teardown", () => {
|
|||
const aliases = getAliases(filePath);
|
||||
|
||||
// Verify we get the expected aliases for this shell type
|
||||
assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)");
|
||||
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`);
|
||||
assert.strictEqual(
|
||||
aliases[i],
|
||||
expectedAliases[i],
|
||||
`Alias ${i} should match expected format`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +127,7 @@ describe("teardown", () => {
|
|||
"",
|
||||
expectedAliases[0], // Only first alias
|
||||
"alias other='command'",
|
||||
""
|
||||
"",
|
||||
];
|
||||
const filePath = createShellStartupScript(lines, startupExtension);
|
||||
|
||||
|
|
@ -96,11 +137,21 @@ describe("teardown", () => {
|
|||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||
|
||||
assert.strictEqual(result.removedCount, 1, "Should remove 1 alias");
|
||||
assert.strictEqual(result.notFoundCount, 2, "Should report 2 aliases not found");
|
||||
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");
|
||||
assert.ok(
|
||||
!updatedContent.includes(expectedAliases[0]),
|
||||
"First alias should be removed"
|
||||
);
|
||||
assert.ok(
|
||||
updatedContent.includes("alias other='command'"),
|
||||
"Other aliases should remain"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -109,38 +160,56 @@ describe("teardown", () => {
|
|||
runRemovalTestsForEnvironment("bash", ".bashrc", [
|
||||
"alias npm='aikido-npm'",
|
||||
"alias npx='aikido-npx'",
|
||||
"alias yarn='aikido-yarn'"
|
||||
"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 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 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 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`;
|
||||
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");
|
||||
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 });
|
||||
|
|
@ -149,13 +218,23 @@ describe("teardown", () => {
|
|||
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`;
|
||||
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");
|
||||
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 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue