diff --git a/jest.config.js b/jest.config.js index 60dc0bb..367b4fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,5 +4,5 @@ module.exports = { prettierPath: null, preset: "ts-jest", testEnvironment: "node", - modulePathIgnorePatterns: ["/build"], + modulePathIgnorePatterns: ["/dist"], }; diff --git a/src/commands/__tests__/__snapshots__/ls.test.ts.snap b/src/commands/__tests__/__snapshots__/ls.test.ts.snap index c8060eb..b9932b8 100644 --- a/src/commands/__tests__/__snapshots__/ls.test.ts.snap +++ b/src/commands/__tests__/__snapshots__/ls.test.ts.snap @@ -37,10 +37,10 @@ exports[`ls when valid ls command should print list response: stderr 1`] = ` exports[`ls when valid ls command should print list response: stdout 1`] = ` [ [ - "instance-1", + "instance-1 - Group / Resource 1", ], [ - "instance-2", + "instance-2 - Resource 2", ], ] `; diff --git a/src/commands/__tests__/ls.test.ts b/src/commands/__tests__/ls.test.ts index 20146e2..107293e 100644 --- a/src/commands/__tests__/ls.test.ts +++ b/src/commands/__tests__/ls.test.ts @@ -28,7 +28,7 @@ describe("ls", () => { describe("when valid ls command", () => { const command = "ls ssh destination"; - const mockItems = (items: string[]) => + const mockItems = (items: object[]) => mockFetchCommand.mockResolvedValue({ ok: true, term: "", @@ -37,7 +37,10 @@ describe("ls", () => { }); it("should print list response", async () => { - mockItems(["instance-1", "instance-2"]); + mockItems([ + { key: "instance-1", group: "Group", value: "Resource 1" }, + { key: "instance-2", value: "Resource 2" }, + ]); await lsCommand(yargs()).parse(command); expect(mockPrint1.mock.calls).toMatchSnapshot("stdout"); expect(mockPrint2.mock.calls).toMatchSnapshot("stderr"); diff --git a/src/commands/ls.ts b/src/commands/ls.ts index 0e6c00d..5407802 100644 --- a/src/commands/ls.ts +++ b/src/commands/ls.ts @@ -11,13 +11,14 @@ You should have received a copy of the GNU General Public License along with @p0 import { fetchCommand } from "../drivers/api"; import { authenticate } from "../drivers/auth"; import { guard } from "../drivers/firestore"; -import { print2, print1 } from "../drivers/stdio"; +import { print2, print1, Ansi } from "../drivers/stdio"; +import { max } from "lodash"; import pluralize from "pluralize"; import yargs from "yargs"; type LsResponse = { ok: true; - items: string[]; + items: { key: string; value: string; group?: string }[]; isTruncated: boolean; term: string; arg: string; @@ -50,6 +51,7 @@ const ls = async ( "ls", ...args.arguments, ]); + const allArguments = [...args._, ...args.arguments]; if (data && "ok" in data && data.ok) { const label = pluralize(data.arg); @@ -57,13 +59,28 @@ const ls = async ( print2(`No ${label}`); return; } - print2( - `Showing${ - data.isTruncated ? ` the first ${data.items.length}` : "" - } ${label}${data.term ? ` matching '${data.term}'` : ""}:` - ); + const truncationPart = data.isTruncated + ? ` the first ${data.items.length}` + : ""; + const postfixPart = data.term + ? ` matching '${data.term}'` + : data.isTruncated + ? ` (use \`p0 + ${allArguments.join(" ")} \` to narrow results)` + : ""; + + print2(`Showing${truncationPart} ${label}${postfixPart}:`); + const isSameValue = data.items.every((i) => !i.group && i.key === i.value); + const maxLength = max(data.items.map((i) => i.key.length)) || 0; for (const item of data.items) { - print1(item); + const tagPart = `${item.group ? `${item.group} / ` : ""}${item.value}`; + print1( + isSameValue + ? item.key + : maxLength > 30 + ? `${item.key}\n ${Ansi.Dim}${tagPart}${Ansi.Reset}` + : `${item.key.padEnd(maxLength)}${Ansi.Dim} - ${tagPart}${Ansi.Reset}` + ); } } else { throw data; diff --git a/src/drivers/stdio.ts b/src/drivers/stdio.ts index 421938f..7a6128c 100644 --- a/src/drivers/stdio.ts +++ b/src/drivers/stdio.ts @@ -8,12 +8,14 @@ This file is part of @p0security/p0cli You should have received a copy of the GNU General Public License along with @p0security/p0cli. If not, see . **/ + /** Functions to handle stdio * * These are essentially wrappers around console.foo, but allow for * - Better testing * - Later redirection / duplication */ +import { mapValues } from "lodash"; /** Used to output machine-readable text to stdout * @@ -33,3 +35,10 @@ export function print2(message: any) { // eslint-disable-next-line no-console console.error(message); } + +const AnsiCodes = { + Reset: "00", + Dim: "02", +} as const; + +export const Ansi = mapValues(AnsiCodes, (v) => `\u001b[${v}m`);