Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/per-command-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"seamless-cli": minor
---

Add per-command help. Every command now answers `-h` / `--help` with usage, flags, subcommands, and
examples scoped to that command (`seamless init -h`, `seamless verify --help`), and
`seamless help <command>` prints the same thing. The help text lives in one registry
(`src/commands/helpTopics.ts`) that both the full `seamless --help` output and the per-command
output render from, so a flag is documented once and appears in both.

The help check runs before a command parses its own arguments, so `seamless init -h` prints help
instead of treating `-h` as a project name. A `--` separator ends the check, so a command can still
take a literal `-h` value (`seamless config set key -- -h`).
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ The entry point is [src/index.ts](src/index.ts), which dispatches to a command m
`logout`/`whoami`, `sessions`, `config` (system config + OAuth providers),
`users`, and `org` all talk to a running instance and are authenticated by the
stored session.
- **help** — `seamless --help`, `seamless <command> -h/--help`, and `seamless help <command>` all
render from the single registry in [src/commands/helpTopics.ts](src/commands/helpTopics.ts)
([src/commands/help.ts](src/commands/help.ts) does the formatting, and `COMMANDS` there is also
the dispatcher's known-command list). Document a new command or flag in that registry, not in the
help template. `src/index.ts` answers the help flag before a command parses its own args.
- **portal** — `login` signs in to the Seamless portal, a separate account from
any instance profile. Its session lives beside the profile map in
`config.json` and is the only one `init` uses to connect a managed
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ You’ll be guided through a short setup process where you can choose:

---

## Getting help

`seamless --help` lists every command, and every command documents itself:

```bash
seamless init --help
```

`-h` is the short form, and `seamless help <command>` is the spelled-out one, so
`seamless verify -h`, `seamless verify --help`, and `seamless help verify` all print the flags,
subcommands, and examples for `verify` only. If a command takes a value that is literally `-h`,
put it after `--` (`seamless config set key -- -h`).

---

## Connecting to a managed instance

If you are signed in to the Seamless portal (`seamless login`) and your account has at least one
Expand Down
65 changes: 64 additions & 1 deletion src/commands/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Loading index.ts first here avoids a circular-import TDZ error that occurs
// when help.ts is the first module to pull index.ts in.
import "../index.js";
import { printHelp } from "./help.js";
import { printCommandHelp, printHelp } from "./help.js";
import { COMMAND_HELP } from "./helpTopics.js";

describe("printHelp", () => {
let logSpy: ReturnType<typeof vi.spyOn>;
Expand All @@ -26,4 +27,66 @@ describe("printHelp", () => {
expect(output).toContain("seamless login");
expect(output).toContain("https://docs.seamlessauth.com");
});

it("documents every command", () => {
printHelp();

const [output] = logSpy.mock.calls[0] as [string];
for (const command of COMMAND_HELP) {
for (const usage of command.usage) {
expect(output).toContain(usage);
}
}
});
});

describe("printCommandHelp", () => {
let logSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
});

afterEach(() => {
logSpy.mockRestore();
});

it.each(COMMAND_HELP.map((c) => c.name))(
"prints usage scoped to %s",
(name) => {
expect(printCommandHelp(name)).toBe(true);

const [output] = logSpy.mock.calls[0] as [string];
expect(output).toContain(`seamless ${name} — seamless v`);
expect(output).toContain("USAGE");
expect(output).toContain("DESCRIPTION");
expect(output).toContain("https://docs.seamlessauth.com");
},
);

it("keeps each command's help to that command", () => {
printCommandHelp("check");

const [output] = logSpy.mock.calls[0] as [string];
expect(output).toContain("seamless check");
expect(output).not.toContain("seamless verify");
});

it("prints the section headings only when a command has several", () => {
printCommandHelp("sessions");
const [sessions] = logSpy.mock.calls[0] as [string];
expect(sessions).toContain("sessions revoke <id | --all>");

logSpy.mockClear();
printCommandHelp("whoami");
const [whoami] = logSpy.mock.calls[0] as [string];
expect(whoami.split("DESCRIPTION")[1].trimStart()).toMatch(
/^Show the identity/,
);
});

it("reports an unknown topic instead of printing an empty one", () => {
expect(printCommandHelp("frobnicate")).toBe(false);
expect(logSpy).not.toHaveBeenCalled();
});
});
Loading
Loading