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: {
|
||||
sourcecred: resolveApp("src/oclif/sourcecred.js"),
|
||||
"commands/load": resolveApp("src/oclif/commands/load.js"),
|
||||
cli: resolveApp("src/cli/main.js"),
|
||||
//
|
||||
fetchAndPrintGithubRepo: resolveApp(
|
||||
"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