mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Implement pnpm and pnpx support
This commit is contained in:
parent
21cdefadde
commit
f10749923a
14 changed files with 754 additions and 145 deletions
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
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -19,6 +19,8 @@
|
||||||
"bin": {
|
"bin": {
|
||||||
"aikido-npm": "bin/aikido-npm.js",
|
"aikido-npm": "bin/aikido-npm.js",
|
||||||
"aikido-npx": "bin/aikido-npx.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",
|
"aikido-yarn": "bin/aikido-yarn.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
"aikido-npm": "bin/aikido-npm.js",
|
"aikido-npm": "bin/aikido-npm.js",
|
||||||
"aikido-npx": "bin/aikido-npx.js",
|
"aikido-npx": "bin/aikido-npx.js",
|
||||||
"aikido-yarn": "bin/aikido-yarn.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"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -40,5 +42,6 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/AikidoSec/safe-chain/issues"
|
"url": "https://github.com/AikidoSec/safe-chain/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/AikidoSec/safe-chain#readme"
|
"homepage": "https://github.com/AikidoSec/safe-chain#readme",
|
||||||
|
"packageManager": "npm@11.4.1+sha512.fcee43884166b6f9c5d04535fb95650e9708b6948a1f797eddf40e9778646778a518dfa32651b1c62ff36f4ac42becf177ca46ca27d53f24b539190c8d91802b"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { createNpmPackageManager } from "./npm/createPackageManager.js";
|
||||||
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
||||||
|
import {
|
||||||
|
createPnpmPackageManager,
|
||||||
|
createPnpxPackageManager,
|
||||||
|
} from "./pnpm/createPackageManager.js";
|
||||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
|
@ -13,6 +17,10 @@ export function initializePackageManager(packageManagerName, version) {
|
||||||
state.packageManagerName = createNpxPackageManager();
|
state.packageManagerName = createNpxPackageManager();
|
||||||
} else if (packageManagerName === "yarn") {
|
} else if (packageManagerName === "yarn") {
|
||||||
state.packageManagerName = createYarnPackageManager();
|
state.packageManagerName = createYarnPackageManager();
|
||||||
|
} else if (packageManagerName === "pnpm") {
|
||||||
|
state.packageManagerName = createPnpmPackageManager();
|
||||||
|
} else if (packageManagerName === "pnpx") {
|
||||||
|
state.packageManagerName = createPnpxPackageManager();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
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("standardYarnArgumentParser", () => {
|
||||||
|
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,6 +2,8 @@ const knownAikidoTools = [
|
||||||
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||||
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
||||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
|
{ tool: "pnpm", aikidoCommand: "aikido-pnpm" },
|
||||||
|
{ tool: "pnpx", aikidoCommand: "aikido-pnpx" },
|
||||||
// When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js)
|
// When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js)
|
||||||
// and add the documentation for the new tool in the README.md
|
// and add the documentation for the new tool in the README.md
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,45 +6,79 @@ import { getAliases } from "./helpers.js";
|
||||||
import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js";
|
import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js";
|
||||||
|
|
||||||
describe("setupShell", () => {
|
describe("setupShell", () => {
|
||||||
function runSetupTestsForEnvironment(shell, startupExtension, expectedAliases) {
|
function runSetupTestsForEnvironment(
|
||||||
|
shell,
|
||||||
|
startupExtension,
|
||||||
|
expectedAliases
|
||||||
|
) {
|
||||||
describe(`${shell} shell setup`, () => {
|
describe(`${shell} shell setup`, () => {
|
||||||
it(`should add aliases to ${shell} file`, () => {
|
it(`should add aliases to ${shell} file`, () => {
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
|
const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
|
result.addedCount,
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
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);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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`, () => {
|
it(`should not add aliases if they already exist in ${shell} file`, () => {
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases];
|
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
||||||
assert.strictEqual(result.existingCount, 3, "Should find 3 existing aliases");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
result.existingCount,
|
||||||
|
expectedAliases.length,
|
||||||
|
`Should find ${expectedAliases.length} existing aliases`
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
result.failedCount,
|
||||||
|
0,
|
||||||
|
"Should have no failed aliases"
|
||||||
|
);
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
// Count occurrences to ensure no duplicates were added
|
// Count occurrences to ensure no duplicates were added
|
||||||
for (const alias of expectedAliases) {
|
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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -54,78 +88,144 @@ describe("setupShell", () => {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test readOrCreateStartupFile function
|
// Test readOrCreateStartupFile function
|
||||||
const fileContent = readOrCreateStartupFile(filePath);
|
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");
|
assert.ok(fs.existsSync(filePath), "File should be created");
|
||||||
|
|
||||||
// Test adding aliases to the newly created file
|
// Test adding aliases to the newly created file
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
|
result.addedCount,
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
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);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should add aliases only once when called multiple times for ${shell}`, () => {
|
it(`should add aliases only once when called multiple times for ${shell}`, () => {
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, ""];
|
const lines = [`#!/usr/bin/env ${shell}`, ""];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
|
|
||||||
// First call - should add aliases
|
// First call - should add aliases
|
||||||
let fileContent = fs.readFileSync(filePath, "utf-8");
|
let fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
const result1 = appendAliasesToFile(aliases, fileContent, filePath);
|
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
|
// Second call - should detect existing aliases
|
||||||
fileContent = fs.readFileSync(filePath, "utf-8");
|
fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
const result2 = appendAliasesToFile(aliases, fileContent, filePath);
|
const result2 = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
assert.strictEqual(result2.addedCount, 0, "Second call should add 0 aliases");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result2.existingCount, 3, "Second call should find 3 existing aliases");
|
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);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should use real getAliases() for ${shell} file`, () => {
|
it(`should use real getAliases() for ${shell} file`, () => {
|
||||||
const filePath = `${tmpdir()}/test${startupExtension}`;
|
const filePath = `${tmpdir()}/test${startupExtension}`;
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
|
|
||||||
// Verify we get the expected aliases for this shell type
|
// 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++) {
|
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}`, () => {
|
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 filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 2, "Should add 2 new aliases");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.existingCount, 1, "Should find 1 existing alias");
|
result.addedCount,
|
||||||
assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
|
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);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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,65 +234,91 @@ describe("setupShell", () => {
|
||||||
runSetupTestsForEnvironment("bash", ".bashrc", [
|
runSetupTestsForEnvironment("bash", ".bashrc", [
|
||||||
"alias npm='aikido-npm'",
|
"alias npm='aikido-npm'",
|
||||||
"alias npx='aikido-npx'",
|
"alias npx='aikido-npx'",
|
||||||
"alias yarn='aikido-yarn'"
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias pnpm='aikido-pnpm'",
|
||||||
|
"alias pnpx='aikido-pnpx'",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runSetupTestsForEnvironment("zsh", ".zshrc", [
|
runSetupTestsForEnvironment("zsh", ".zshrc", [
|
||||||
"alias npm='aikido-npm'",
|
"alias npm='aikido-npm'",
|
||||||
"alias npx='aikido-npx'",
|
"alias npx='aikido-npx'",
|
||||||
"alias yarn='aikido-yarn'"
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias pnpm='aikido-pnpm'",
|
||||||
|
"alias pnpx='aikido-pnpx'",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runSetupTestsForEnvironment("fish", ".fish", [
|
runSetupTestsForEnvironment("fish", ".fish", [
|
||||||
'alias npm "aikido-npm"',
|
'alias npm "aikido-npm"',
|
||||||
'alias npx "aikido-npx"',
|
'alias npx "aikido-npx"',
|
||||||
'alias yarn "aikido-yarn"'
|
'alias yarn "aikido-yarn"',
|
||||||
|
'alias pnpm "aikido-pnpm"',
|
||||||
|
'alias pnpx "aikido-pnpx"',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runSetupTestsForEnvironment("pwsh", ".ps1", [
|
runSetupTestsForEnvironment("pwsh", ".ps1", [
|
||||||
"Set-Alias npm aikido-npm",
|
"Set-Alias npm aikido-npm",
|
||||||
"Set-Alias npx aikido-npx",
|
"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", () => {
|
describe("readOrCreateStartupFile", () => {
|
||||||
it("should read existing file content", () => {
|
it("should read existing file content", () => {
|
||||||
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
||||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
const filePath = createShellStartupScript(lines, ".bashrc");
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
const content = readOrCreateStartupFile(filePath);
|
||||||
|
|
||||||
assert.ok(content.includes("#!/usr/bin/env bash"), "Should contain shebang");
|
assert.ok(
|
||||||
assert.ok(content.includes("alias test='echo test'"), "Should contain existing aliases");
|
content.includes("#!/usr/bin/env bash"),
|
||||||
|
"Should contain shebang"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
content.includes("alias test='echo test'"),
|
||||||
|
"Should contain existing aliases"
|
||||||
|
);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create file if it doesn't exist", () => {
|
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)) {
|
if (fs.existsSync(filePath)) {
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
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");
|
assert.ok(fs.existsSync(filePath), "File should be created");
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle empty existing file", () => {
|
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");
|
fs.writeFileSync(filePath, "", "utf-8");
|
||||||
|
|
||||||
const content = readOrCreateStartupFile(filePath);
|
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");
|
assert.ok(fs.existsSync(filePath), "File should still exist");
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
@ -203,15 +329,22 @@ describe("setupShell", () => {
|
||||||
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
|
||||||
const filePath = createShellStartupScript(lines, ".bashrc");
|
const filePath = createShellStartupScript(lines, ".bashrc");
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = appendAliasesToFile([], fileContent, filePath);
|
const result = appendAliasesToFile([], fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
|
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");
|
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
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", () => {
|
it("should handle partial substring matches correctly", () => {
|
||||||
|
|
@ -219,38 +352,57 @@ describe("setupShell", () => {
|
||||||
"#!/usr/bin/env bash",
|
"#!/usr/bin/env bash",
|
||||||
"",
|
"",
|
||||||
"alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm='
|
"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 filePath = createShellStartupScript(lines, ".bashrc");
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
const aliases = ["alias npm='aikido-npm'"];
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 1, "Should add 1 alias (npm)");
|
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");
|
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "npm alias should be added");
|
assert.ok(
|
||||||
assert.ok(updatedContent.includes("alias npmx='some-other-command'"), "npmx alias should remain");
|
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", () => {
|
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}`;
|
const fileContent = `${EOL}${EOL} ${EOL}`;
|
||||||
fs.writeFileSync(filePath, fileContent, "utf-8");
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
const aliases = ["alias npm='aikido-npm'"];
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 1, "Should add 1 alias");
|
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");
|
assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
|
||||||
|
|
||||||
const updatedContent = fs.readFileSync(filePath, "utf-8");
|
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
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
@ -258,21 +410,31 @@ describe("setupShell", () => {
|
||||||
|
|
||||||
describe("appendAliasesToFile error handling", () => {
|
describe("appendAliasesToFile error handling", () => {
|
||||||
it("should handle file permission errors gracefully", () => {
|
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");
|
fs.writeFileSync(filePath, "#!/usr/bin/env bash", "utf-8");
|
||||||
|
|
||||||
// Make file read-only to simulate permission error
|
// Make file read-only to simulate permission error
|
||||||
fs.chmodSync(filePath, 0o444);
|
fs.chmodSync(filePath, 0o444);
|
||||||
|
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
const aliases = ["alias npm='aikido-npm'"];
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
const result = appendAliasesToFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.addedCount, 0, "Should add 0 aliases due to permission error");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
|
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");
|
assert.strictEqual(result.failedCount, 1, "Should have 1 failed alias");
|
||||||
|
|
||||||
// Restore permissions and cleanup
|
// Restore permissions and cleanup
|
||||||
fs.chmodSync(filePath, 0o644);
|
fs.chmodSync(filePath, 0o644);
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
|
|
@ -301,4 +463,4 @@ function countOccurrences(lines, searchString) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,41 +6,64 @@ import { getAliases } from "./helpers.js";
|
||||||
import { removeAliasesFromFile } from "./teardown.js";
|
import { removeAliasesFromFile } from "./teardown.js";
|
||||||
|
|
||||||
describe("teardown", () => {
|
describe("teardown", () => {
|
||||||
function runRemovalTestsForEnvironment(shell, startupExtension, expectedAliases) {
|
function runRemovalTestsForEnvironment(
|
||||||
|
shell,
|
||||||
|
startupExtension,
|
||||||
|
expectedAliases
|
||||||
|
) {
|
||||||
describe(`${shell} shell removal`, () => {
|
describe(`${shell} shell removal`, () => {
|
||||||
it(`should remove aliases from ${shell} file`, () => {
|
it(`should remove aliases from ${shell} file`, () => {
|
||||||
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""];
|
const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
// Test the removeAliasesFromFile function directly
|
// Test the removeAliasesFromFile function directly
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
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");
|
assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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}`, () => {
|
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 filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases");
|
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);
|
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`, () => {
|
it(`should remove duplicate aliases from ${shell} file`, () => {
|
||||||
|
|
@ -50,33 +73,51 @@ describe("teardown", () => {
|
||||||
...expectedAliases,
|
...expectedAliases,
|
||||||
"alias other='command'",
|
"alias other='command'",
|
||||||
...expectedAliases, // duplicates
|
...expectedAliases, // duplicates
|
||||||
""
|
"",
|
||||||
];
|
];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
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");
|
assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
|
||||||
|
|
||||||
const updatedContent = readAndDeleteFile(filePath);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
for (const alias of expectedAliases) {
|
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`, () => {
|
it(`should use real getAliases() for ${shell} file`, () => {
|
||||||
const filePath = `${tmpdir()}/test${startupExtension}`;
|
const filePath = `${tmpdir()}/test${startupExtension}`;
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
|
|
||||||
// Verify we get the expected aliases for this shell type
|
// 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++) {
|
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,21 +127,31 @@ describe("teardown", () => {
|
||||||
"",
|
"",
|
||||||
expectedAliases[0], // Only first alias
|
expectedAliases[0], // Only first alias
|
||||||
"alias other='command'",
|
"alias other='command'",
|
||||||
""
|
"",
|
||||||
];
|
];
|
||||||
const filePath = createShellStartupScript(lines, startupExtension);
|
const filePath = createShellStartupScript(lines, startupExtension);
|
||||||
|
|
||||||
const aliases = getAliases(filePath);
|
const aliases = getAliases(filePath);
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 1, "Should remove 1 alias");
|
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);
|
const updatedContent = readAndDeleteFile(filePath);
|
||||||
assert.ok(!updatedContent.includes(expectedAliases[0]), "First alias should be removed");
|
assert.ok(
|
||||||
assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
|
!updatedContent.includes(expectedAliases[0]),
|
||||||
|
"First alias should be removed"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
updatedContent.includes("alias other='command'"),
|
||||||
|
"Other aliases should remain"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -109,39 +160,57 @@ describe("teardown", () => {
|
||||||
runRemovalTestsForEnvironment("bash", ".bashrc", [
|
runRemovalTestsForEnvironment("bash", ".bashrc", [
|
||||||
"alias npm='aikido-npm'",
|
"alias npm='aikido-npm'",
|
||||||
"alias npx='aikido-npx'",
|
"alias npx='aikido-npx'",
|
||||||
"alias yarn='aikido-yarn'"
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias pnpm='aikido-pnpm'",
|
||||||
|
"alias pnpx='aikido-pnpx'",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("zsh", ".zshrc", [
|
runRemovalTestsForEnvironment("zsh", ".zshrc", [
|
||||||
"alias npm='aikido-npm'",
|
"alias npm='aikido-npm'",
|
||||||
"alias npx='aikido-npx'",
|
"alias npx='aikido-npx'",
|
||||||
"alias yarn='aikido-yarn'"
|
"alias yarn='aikido-yarn'",
|
||||||
|
"alias pnpm='aikido-pnpm'",
|
||||||
|
"alias pnpx='aikido-pnpx'",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("fish", ".fish", [
|
runRemovalTestsForEnvironment("fish", ".fish", [
|
||||||
'alias npm "aikido-npm"',
|
'alias npm "aikido-npm"',
|
||||||
'alias npx "aikido-npx"',
|
'alias npx "aikido-npx"',
|
||||||
'alias yarn "aikido-yarn"'
|
'alias yarn "aikido-yarn"',
|
||||||
|
'alias pnpm "aikido-pnpm"',
|
||||||
|
'alias pnpx "aikido-pnpx"',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
runRemovalTestsForEnvironment("pwsh", ".ps1", [
|
runRemovalTestsForEnvironment("pwsh", ".ps1", [
|
||||||
"Set-Alias npm aikido-npm",
|
"Set-Alias npm aikido-npm",
|
||||||
"Set-Alias npx aikido-npx",
|
"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", () => {
|
describe("removeAliasesFromFile edge cases", () => {
|
||||||
it("should handle empty file", () => {
|
it("should handle empty file", () => {
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
const aliases = ["alias npm='aikido-npm'"];
|
||||||
const fileContent = "";
|
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");
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from empty file");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found");
|
result.removedCount,
|
||||||
|
0,
|
||||||
|
"Should remove 0 aliases from empty file"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
result.notFoundCount,
|
||||||
|
1,
|
||||||
|
"Should report 1 alias not found"
|
||||||
|
);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
@ -149,14 +218,24 @@ describe("teardown", () => {
|
||||||
it("should handle file with only whitespace", () => {
|
it("should handle file with only whitespace", () => {
|
||||||
const aliases = ["alias npm='aikido-npm'"];
|
const aliases = ["alias npm='aikido-npm'"];
|
||||||
const fileContent = `${EOL}${EOL} ${EOL}`;
|
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");
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
||||||
|
|
||||||
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
const result = removeAliasesFromFile(aliases, fileContent, filePath);
|
||||||
|
|
||||||
assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from whitespace-only file");
|
assert.strictEqual(
|
||||||
assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found");
|
result.removedCount,
|
||||||
|
0,
|
||||||
|
"Should remove 0 aliases from whitespace-only file"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
result.notFoundCount,
|
||||||
|
1,
|
||||||
|
"Should report 1 alias not found"
|
||||||
|
);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
});
|
});
|
||||||
|
|
@ -174,4 +253,4 @@ function readAndDeleteFile(filePath) {
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
fs.rmSync(filePath, { force: true });
|
fs.rmSync(filePath, { force: true });
|
||||||
return fileContent.split(EOL);
|
return fileContent.split(EOL);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue