@log4brains/cli
Version:
Log4brains architecture knowledge base CLI
236 lines (195 loc) • 7.49 kB
JavaScript
import commander from 'commander';
import terminalLink from 'terminal-link';
import { Log4brainsError, Log4brains } from '@log4brains/core';
import { FailureExit } from '@log4brains/cli-common';
import path from 'path';
import fs, { promises } from 'fs';
import execa from 'execa';
class ListCommand {
constructor({
l4bInstance,
appConsole
}) {
this.l4bInstance = l4bInstance;
this.console = appConsole;
}
async execute(opts) {
const filters = {};
if (opts.statuses) {
filters.statuses = opts.statuses.split(",");
}
const adrs = await this.l4bInstance.searchAdrs(filters);
const table = this.console.createTable({
head: ["Slug", "Status", "Package", "Title"]
});
adrs.forEach(adr => {
table.push([adr.slug, adr.status.toUpperCase(), adr.package || "", adr.title || "Untitled"]);
});
this.console.printTable(table, opts.raw);
}
}
async function previewAdr(slug) {
var _subprocess$stdout, _subprocess$stderr;
const subprocess = execa("log4brains", ["preview", slug], {
stdio: "inherit"
});
(_subprocess$stdout = subprocess.stdout) == null ? void 0 : _subprocess$stdout.pipe(process.stdout);
(_subprocess$stderr = subprocess.stderr) == null ? void 0 : _subprocess$stderr.pipe(process.stderr);
await subprocess;
}
/* eslint-disable no-await-in-loop */
class NewCommand {
constructor({
l4bInstance,
appConsole
}) {
this.l4bInstance = l4bInstance;
this.console = appConsole;
}
detectCurrentPackageFromCwd() {
const {
packages
} = this.l4bInstance.config.project;
if (!packages) {
return undefined;
}
const cwd = path.resolve(".");
const match = packages.filter(pkg => cwd.includes(pkg.path)).sort((a, b) => a.path.length - b.path.length).pop(); // returns the most precise path (ie. longest)
return match == null ? void 0 : match.name;
} // eslint-disable-next-line sonarjs/cognitive-complexity
async execute(opts, titleArg) {
const {
packages
} = this.l4bInstance.config.project;
let pkg = opts.package;
if (!opts.quiet && !pkg && packages && packages.length > 0) {
const currentPackage = this.detectCurrentPackageFromCwd();
const packageChoices = [{
name: `Global`,
value: ""
}, ...packages.map(p => ({
name: `Package: ${p.name}`,
value: p.name
}))];
pkg = (await this.console.askListQuestion("For which package do you want to create this new ADR?", packageChoices, currentPackage)) || undefined;
}
if (opts.quiet && !titleArg) {
throw new Log4brainsError("<title> is required when using --quiet");
}
let title;
do {
title = titleArg || (await this.console.askInputQuestion("Title of the solved problem and its solution?"));
if (!title.trim()) {
this.console.warn("Please enter a title");
}
} while (!title.trim()); // const slug = await this.console.askInputQuestion(
// "We pre-generated a slug to identify this ADR. Press [ENTER] or enter another one.",
// await this.l4bInstance.generateAdrSlug(title, pkg)
// );
const slug = await this.l4bInstance.generateAdrSlug(title, pkg);
const adrDto = await this.l4bInstance.createAdrFromTemplate(slug, title); // --from option (used by init-log4brains to create the starter ADRs)
// Since this is a private use case, we don't include it in CORE for now
if (opts.from) {
if (!fs.existsSync(opts.from)) {
throw new Log4brainsError("The given file does not exist", opts.from);
} // TODO: use streams
await promises.writeFile(adrDto.file.absolutePath, await promises.readFile(opts.from, "utf-8"), "utf-8");
}
if (opts.quiet) {
this.console.println(adrDto.slug);
process.exit(0);
}
const activeAdrs = await this.l4bInstance.searchAdrs({
statuses: ["accepted"]
});
if (activeAdrs.length > 0) {
const supersedeChoices = [{
name: "No",
value: ""
}, ...activeAdrs.map(a => ({
name: a.title || "Untitled",
value: a.slug
}))];
const supersededSlug = await this.console.askListQuestion("Does this ADR supersede a previous one?", supersedeChoices, "");
if (supersededSlug !== "") {
await this.l4bInstance.supersedeAdr(supersededSlug, slug);
this.console.debug(`${supersededSlug} was marked as superseded by ${slug}`);
}
}
this.console.println();
this.console.success(`New ADR created: ${adrDto.file.relativePath}`);
this.console.println();
const actionChoices = [{
name: "Edit and preview",
value: "edit-and-preview"
}, {
name: "Edit",
value: "edit"
}, {
name: "Later",
value: "close"
}];
const action = await this.console.askListQuestion("How would you like to edit it?", actionChoices, "edit-and-preview");
if (action === "edit-and-preview" || action === "edit") {
await this.l4bInstance.openAdrInEditor(slug, () => {
this.console.warn("We were not able to detect your preferred editor :(");
this.console.warn("You can define it by setting your $VISUAL or $EDITOR environment variable in ~/.zshenv or ~/.bashrc");
});
if (action === "edit-and-preview") {
await previewAdr(slug);
}
}
process.exit(0);
}
}
const templateExampleUrl = "https://raw.githubusercontent.com/thomvaill/log4brains/stable/packages/init/assets/template.md";
let l4bInstance;
function getL4bInstance() {
if (!l4bInstance) {
l4bInstance = Log4brains.createFromCwd(process.env.LOG4BRAINS_CWD || ".");
}
return l4bInstance;
}
function execWithErrorHandler(promise, appConsole) {
promise.catch(err => {
if (err instanceof Log4brainsError && err.name === "The template.md file does not exist") {
appConsole.error(err);
appConsole.printlnErr(`You can use this ${terminalLink("template", templateExampleUrl)} as an example`);
throw new FailureExit();
}
});
return promise;
}
function createCli({
appConsole
}) {
const program = new commander.Command();
const adr = program.command("adr").description("Group of commands to manage your ADRs...");
adr.command("new [title]").description("Create an ADR", {
title: "The title of the ADR. Required if --quiet is passed"
}).option("-q, --quiet", "Disable interactive mode", false).option("-p, --package <package>", "To create the ADR for a specific package").option("--from <file>", "Copy <file> contents into the ADR instead of using the default template").action((title, opts) => {
const cmd = new NewCommand({
l4bInstance: getL4bInstance(),
appConsole
});
return execWithErrorHandler(cmd.execute(opts, title), appConsole);
}); // adr
// .command("quick")
// .description("Create a one-sentence ADR (Y-Statement)")
// .action(
// (): Promise<void> => {
// // TODO
// }
// );
adr.command("list").option("-s, --statuses <statuses>", "Filter on the given statuses, comma-separated") // TODO: list available statuses
.option("-r, --raw", "Use a raw format instead of a table", false).description("List ADRs").action(opts => {
const cmd = new ListCommand({
l4bInstance: getL4bInstance(),
appConsole
});
return execWithErrorHandler(cmd.execute(opts), appConsole);
});
return program;
}
export { createCli };
//# sourceMappingURL=index.module.js.map