diffjam
Version:
cli for diffjam.com
306 lines (258 loc) • 9.05 kB
text/typescript
/*
Performs top-level actions of the CLI. Connects the file paths found in the
`CurrentWorkingDirectory` to the worker pool.
*/
import envCi from 'env-ci';
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 { clientVersion } from "./clientVersion";
import { commentResults, postMetrics, ResultMap } from "./count";
import { ResultsMap } from './match';
import { actionPolicyModify } from './policyModify';
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 clientVers = clientVersion();
const start = new Date();
const { resultsMap, filesChecked } = await this.run();
const { breaches, successes, 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();
}
if (!this.flags.record && !this.flags.ci) {
console.log(chalk.green.bold(`Done in ${Date.now() - start.getTime()} ms.`));
return;
}
console.log(chalk.yellow("sending metrics to server..."));
verbose &&
console.log(chalk.cyan(`successes: ${JSON.stringify(successes)}`));
verbose && console.log(chalk.cyan(`breaches: ${JSON.stringify(breaches)}`));
const apiKey = process.env.DIFFJAM_API_KEY;
if (!apiKey) {
console.error(chalk.red("Missing api key! Could not post metrics."));
console.error(
chalk.red(
"You must have an api key in an environment variable named DIFFJAM_API_KEY"
)
);
process.exitCode = 1;
return;
}
const configJson = this.config.toJson();
verbose && console.log("apiKey, config, results: ", apiKey, configJson, results);
if (this.flags.record) {
await postMetrics(apiKey, configJson, results, clientVers);
}
if (this.flags.ci) {
if (!envCi().isCi) {
throw new Error(`could not detect CI environment`);
}
await commentResults(apiKey, configJson, results, clientVers);
}
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);
}
}