diffjam
Version:
cli for diffjam.com
308 lines (264 loc) • 8.04 kB
text/typescript
/*
Performs top-level actions of the CLI. Connects the file paths found in the
`CurrentWorkingDirectory` to the worker pool.
*/
import chalk from "chalk";
import { equal } from "node:assert";
import { Config } from "./Config";
import { CurrentWorkingDirectory } from "./CurrentWorkingDirectory";
import { Flags } from "./cli";
import { Policy } from "./Policy";
import {
GREEN_CHECK,
logAllResultDetails,
logCheckFailedError,
logResults,
} from "./log";
import { ResultsMap } from "./match";
import { actionPolicyModify } from "./policyModify";
type ResultMap = { [key: string]: { measurement: number } };
interface WorkerPool {
resultsMap: ResultsMap;
filesChecked: string[];
processFile(filePath: string): void;
onFilesDone(): void;
onResults(): void;
}
export class Runner {
private policies: Policy[];
private ran = false;
constructor(
public config: Config,
private flags: Flags,
private cwd: CurrentWorkingDirectory,
private workerPool: WorkerPool
) {
this.policies = Object.values(config.policyMap);
for (const policyName in config.policyMap) {
this.workerPool.resultsMap[policyName] = {
policy: config.policyMap[policyName],
matches: [],
};
}
}
private run(): Promise<{ resultsMap: ResultsMap; filesChecked: string[] }> {
equal(this.ran, false, "Runner.run() should only be called once");
equal(
this.policies.length > 0,
true,
"No policies specified.\nPlease check your config file at " +
this.config.filePath
);
return new Promise((resolve) => {
this.workerPool.onResults = () => {
this.ran = true;
resolve({
resultsMap: this.workerPool.resultsMap,
filesChecked: this.workerPool.filesChecked,
});
};
this.cwd.allNonGitIgnoredFiles(
this.processFile.bind(this),
this.workerPool.onFilesDone.bind(this.workerPool)
);
});
}
async check() {
const { resultsMap, filesChecked } = await this.run();
const { breaches } = logResults(resultsMap, filesChecked);
if (breaches.length) {
logCheckFailedError();
process.exitCode = 1;
} else {
console.log(
`\n${GREEN_CHECK} ${chalk.bold(
`All policies passed with ${filesChecked.length} matching files checked`
)}`
);
}
}
async count() {
const start = new Date();
const { resultsMap, filesChecked } = await this.run();
const { breaches, all } = logResults(resultsMap, filesChecked);
const results: ResultMap = {};
for (const result of all) {
results[result.policy.name] = {
measurement: result.matches.length,
};
}
const verbose = Boolean(this.flags.verbose);
if (breaches.length) {
logCheckFailedError();
}
console.log(
chalk.green.bold(`Done in ${Date.now() - start.getTime()} ms.`)
);
}
async cinch() {
const { resultsMap, filesChecked } = await this.run();
const { breaches, successes } = logResults(resultsMap, filesChecked);
console.log();
if (breaches.length > 0) {
console.error(
chalk.bold(
"Cannot cinch a policy that doesn't even meet the baseline. \n"
)
);
process.exitCode = 1;
return;
}
let anyCinched = false;
for (const success of successes) {
if (success.policy.isCountCinchable(success.matches)) {
anyCinched = true;
const before = success.policy.baseline;
success.policy.baseline = success.matches.length;
console.log(
`${GREEN_CHECK} cinching ${chalk.bold(
success.policy.name
)} from ${before} to ${chalk.bold(
success.matches.length.toString()
)}!`
);
}
}
if (anyCinched) {
this.config.write();
} else {
console.log(
`${GREEN_CHECK} ${chalk.bold(
"All policies are already exactly at their baseline, so none were cinched"
)}`
);
}
}
async bump() {
const { resultsMap, filesChecked } = await this.run();
const { breaches } = logResults(resultsMap, filesChecked);
console.log();
for (const breach of breaches) {
const before = breach.policy.baseline;
breach.policy.baseline = breach.matches.length;
console.log(
`🎚 bumping ${chalk.bold(
breach.policy.name
)} from ${before} to ${chalk.bold(breach.matches.length.toString())}!`
);
}
if (breaches.length) {
this.config.write();
} else {
console.log(
`${GREEN_CHECK} ${chalk.bold(
"All policies are already exactly at their baseline, so none were bumped"
)}`
);
}
}
async addPolicy() {
// Requiring ui inline as it adds a fair bit of time to startup
const ui = require("./ui");
const name = await ui.textInput("Enter a name for this policy: ");
const isRegex = await ui.confirm("Is this a regex search?");
let search: string;
const negativeSearchTerms = [];
if (isRegex) {
const regex = await ui.textInput("Enter the regex to search for: ");
search = `regex:${regex}`;
} else {
search = await ui.textInput("Enter the string to match : ");
while (true) {
const negativeSearchTerm = await ui.textInput(
"Enter any string to negate (or leave blank to continue): "
);
if (negativeSearchTerm.trim()) {
negativeSearchTerms.push(negativeSearchTerm);
} else {
break;
}
}
}
const filePattern = await ui.textInput(
"Enter the filePattern to search for this policy: "
);
const ignoreFilePatterns = [];
while (true) {
const ignoreFilePattern = await ui.textInput(
"Enter any filePatterns to ignore (or leave blank to continue): "
);
if (ignoreFilePattern.trim()) {
ignoreFilePatterns.push(ignoreFilePattern);
} else {
break;
}
}
const description = await ui.textInput(
"Give a description for this policy: "
);
const policy = new Policy(
name,
description,
filePattern,
[search, ...negativeSearchTerms],
0,
ignoreFilePatterns
);
this.config.setPolicy(policy);
this.policies = [policy];
this.workerPool.resultsMap[policy.name] = {
policy,
matches: [],
};
const { resultsMap, filesChecked } = await this.run();
const { matches } = resultsMap[policy.name];
policy.baseline = matches.length;
if (
await ui.confirm(
`There are currently ${policy.baseline} matches for that configuration after checking ${filesChecked.length} files. Save it?`
)
) {
this.config.write();
console.log("Saved!");
} else {
console.log("Cancelled save.");
}
}
async runSinglePolicy(policyName: string) {
const policy = this.config.getPolicy(policyName);
if (!policy) {
throw new Error(`Could not find policy ${policyName}`);
}
const { resultsMap } = await this.run();
return resultsMap[policyName];
}
async removePolicy() {
const ui = require("./ui");
const policy = await ui.select(
"Select a policy to remove: ",
this.config.policyMap
);
await this.config.deletePolicy(policy.name);
await this.config.write();
console.log(`Removed ${policy.name}`);
}
modifyPolicy() {
return actionPolicyModify(this);
}
async checkBreachesForPolicy() {
const ui = require("./ui");
const policy = await ui.select(
"Select a policy to check for breaches: ",
this.config.policyMap
);
const result = await this.runSinglePolicy(policy.name);
logAllResultDetails(result);
}
private processFile(filePath: string) {
const isUnderPolicy = this.policies.some((policy) =>
policy.isFileUnderPolicy(filePath)
);
if (!isUnderPolicy) return;
this.workerPool.processFile(filePath);
}
}