cli: add `main`, `sourcecred`, and `help` (#741)
Summary: This commit includes a minimal usage of an actual CLI application. It provides the `help` command and no actual functionality. Test Plan: Unit tests added, with full coverage. To see it in action, first run `yarn backend`, then run `node bin/cli.js help`. wchargin-branch: cli-beginnings
This commit is contained in:
parent
4c433d417e
commit
ff2d4f2fd8
|
@ -29,6 +29,7 @@ module.exports = {
|
||||||
backendEntryPoints: {
|
backendEntryPoints: {
|
||||||
sourcecred: resolveApp("src/oclif/sourcecred.js"),
|
sourcecred: resolveApp("src/oclif/sourcecred.js"),
|
||||||
"commands/load": resolveApp("src/oclif/commands/load.js"),
|
"commands/load": resolveApp("src/oclif/commands/load.js"),
|
||||||
|
cli: resolveApp("src/cli/main.js"),
|
||||||
//
|
//
|
||||||
fetchAndPrintGithubRepo: resolveApp(
|
fetchAndPrintGithubRepo: resolveApp(
|
||||||
"src/plugins/github/bin/fetchAndPrintGithubRepo.js"
|
"src/plugins/github/bin/fetchAndPrintGithubRepo.js"
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// @flow
|
||||||
|
// Implementation of `sourcecred help`.
|
||||||
|
|
||||||
|
import type {Command} from "./command";
|
||||||
|
import dedent from "../util/dedent";
|
||||||
|
|
||||||
|
const help: Command = async (args, std) => {
|
||||||
|
if (args.length === 0) {
|
||||||
|
usage(std.out);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const command = args[0];
|
||||||
|
const subHelps: {[string]: Command} = {
|
||||||
|
help: metaHelp,
|
||||||
|
};
|
||||||
|
if (subHelps[command] !== undefined) {
|
||||||
|
return subHelps[command](args.slice(1), std);
|
||||||
|
} else {
|
||||||
|
usage(std.err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function usage(print: (string) => void): void {
|
||||||
|
print(
|
||||||
|
dedent`\
|
||||||
|
usage: sourcecred COMMAND [ARGS...]
|
||||||
|
sourcecred [--version] [--help]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
help show this help message
|
||||||
|
|
||||||
|
Use 'sourcecred help COMMAND' for help about an individual command.
|
||||||
|
`.trimRight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaHelp: Command = async (args, std) => {
|
||||||
|
if (args.length === 0) {
|
||||||
|
std.out(
|
||||||
|
dedent`\
|
||||||
|
usage: sourcecred help [COMMAND]
|
||||||
|
|
||||||
|
Use 'sourcecred help' for general help and a list of commands.
|
||||||
|
Use 'sourcecred help COMMAND' for help about COMMAND.
|
||||||
|
`.trimRight()
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
usage(std.err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default help;
|
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import help from "./help";
|
||||||
|
import {run} from "./testUtil";
|
||||||
|
|
||||||
|
describe("cli/help", () => {
|
||||||
|
it("prints general help with no arguments", async () => {
|
||||||
|
expect(await run(help, [])).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: expect.arrayContaining([
|
||||||
|
expect.stringMatching(/^usage: sourcecred/),
|
||||||
|
expect.stringMatching(/Commands:/),
|
||||||
|
]),
|
||||||
|
stderr: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints help about itself", async () => {
|
||||||
|
expect(await run(help, ["help"])).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: expect.arrayContaining([
|
||||||
|
expect.stringMatching(/^usage: sourcecred help/),
|
||||||
|
]),
|
||||||
|
stderr: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when given an unknown command", async () => {
|
||||||
|
expect(await run(help, ["wat"])).toEqual({
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: [],
|
||||||
|
stderr: expect.arrayContaining([
|
||||||
|
expect.stringMatching(/^usage: /),
|
||||||
|
expect.stringMatching(/Commands:/),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when given multiple arguments", async () => {
|
||||||
|
expect(await run(help, ["help", "help"])).toEqual({
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: [],
|
||||||
|
stderr: expect.arrayContaining([
|
||||||
|
expect.stringMatching(/^usage: /),
|
||||||
|
expect.stringMatching(/Commands:/),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {handlingErrors} from "./command";
|
||||||
|
import sourcecred from "./sourcecred";
|
||||||
|
|
||||||
|
require("../tools/entry");
|
||||||
|
|
||||||
|
export default function main(): Promise<void> {
|
||||||
|
return handlingErrors(sourcecred)(process.argv.slice(2), {
|
||||||
|
out: (x) => console.log(x),
|
||||||
|
err: (x) => console.error(x),
|
||||||
|
}).then((exitCode) => {
|
||||||
|
process.exitCode = exitCode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run in the Webpack bundle, not as a Node module (during tests).
|
||||||
|
/* istanbul ignore next */
|
||||||
|
/*:: declare var __webpack_require__: mixed; */
|
||||||
|
if (typeof __webpack_require__ !== "undefined") {
|
||||||
|
main();
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import main from "./main";
|
||||||
|
import sourcecred from "./sourcecred";
|
||||||
|
|
||||||
|
jest.mock("./sourcecred");
|
||||||
|
|
||||||
|
describe("cli/main", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
sourcecred.mockReset();
|
||||||
|
jest.spyOn(console, "log").mockClear();
|
||||||
|
jest.spyOn(console, "error").mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards the exit code", async () => {
|
||||||
|
process.argv = ["node", "sourcecred", "help"];
|
||||||
|
sourcecred.mockResolvedValueOnce(22);
|
||||||
|
await main();
|
||||||
|
expect(process.exitCode).toBe(22);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards arguments", async () => {
|
||||||
|
process.argv = ["node", "sourcecred", "help", "me"];
|
||||||
|
sourcecred.mockResolvedValueOnce(0);
|
||||||
|
await main();
|
||||||
|
expect(sourcecred).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sourcecred).toHaveBeenCalledWith(["help", "me"], {
|
||||||
|
out: expect.any(Function),
|
||||||
|
err: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(process.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards stdout and stderr", async () => {
|
||||||
|
process.argv = ["node", "sourcecred", "help"];
|
||||||
|
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
sourcecred.mockImplementation(async (args, std) => {
|
||||||
|
std.out("out and away");
|
||||||
|
std.err("err, what?");
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
await main();
|
||||||
|
expect(console.log.mock.calls).toEqual([["out and away"]]);
|
||||||
|
expect(console.error.mock.calls).toEqual([["err, what?"]]);
|
||||||
|
expect(process.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures an error", async () => {
|
||||||
|
process.argv = ["node", "sourcecred", "wat"];
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
sourcecred.mockImplementationOnce(() => {
|
||||||
|
throw new Error("wat");
|
||||||
|
});
|
||||||
|
await main();
|
||||||
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching("Error: wat")
|
||||||
|
);
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures a rejection", async () => {
|
||||||
|
process.argv = ["node", "sourcecred", "wat"];
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
sourcecred.mockRejectedValueOnce("wat?");
|
||||||
|
await main();
|
||||||
|
expect(console.log).not.toHaveBeenCalled();
|
||||||
|
expect(console.error).toHaveBeenCalledWith('"wat?"');
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
// @flow
|
||||||
|
// Implementation of the root `sourcecred` command.
|
||||||
|
|
||||||
|
import type {Command} from "./command";
|
||||||
|
|
||||||
|
import {VERSION_SHORT} from "../app/version";
|
||||||
|
|
||||||
|
import help from "./help";
|
||||||
|
|
||||||
|
const sourcecred: Command = async (args, std) => {
|
||||||
|
if (args.length === 0) {
|
||||||
|
help([], {out: std.err, err: std.err});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
switch (args[0]) {
|
||||||
|
case "--version":
|
||||||
|
std.out("sourcecred " + VERSION_SHORT);
|
||||||
|
return 0;
|
||||||
|
case "--help":
|
||||||
|
case "help":
|
||||||
|
return help(args.slice(1), std);
|
||||||
|
default:
|
||||||
|
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
|
||||||
|
std.err("fatal: run 'sourcecred help' for commands and usage");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sourcecred;
|
|
@ -0,0 +1,59 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {run} from "./testUtil";
|
||||||
|
import sourcecred from "./sourcecred";
|
||||||
|
|
||||||
|
function mockCommand(name) {
|
||||||
|
return jest.fn().mockImplementation(async (args, std) => {
|
||||||
|
std.out(`out(${name}): ${JSON.stringify(args)}`);
|
||||||
|
std.err(`err(${name})`);
|
||||||
|
return args.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock("./help", () => mockCommand("help"));
|
||||||
|
|
||||||
|
describe("cli/sourcecred", () => {
|
||||||
|
it("fails with usage when invoked with no arguments", async () => {
|
||||||
|
expect(await run(sourcecred, [])).toEqual({
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: [],
|
||||||
|
stderr: ["out(help): []", "err(help)"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("responds to '--version'", async () => {
|
||||||
|
expect(await run(sourcecred, ["--version"])).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: [expect.stringMatching(/^sourcecred v\d+\.\d+\.\d+$/)],
|
||||||
|
stderr: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("responds to '--help'", async () => {
|
||||||
|
expect(await run(sourcecred, ["--help"])).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: ["out(help): []"],
|
||||||
|
stderr: ["err(help)"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("responds to 'help'", async () => {
|
||||||
|
expect(await run(sourcecred, ["help"])).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: ["out(help): []"],
|
||||||
|
stderr: ["err(help)"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails given an unknown command", async () => {
|
||||||
|
expect(await run(sourcecred, ["wat"])).toEqual({
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: [],
|
||||||
|
stderr: [
|
||||||
|
'fatal: unknown command: "wat"',
|
||||||
|
"fatal: run 'sourcecred help' for commands and usage",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue