📦 deps(thirdparty): update snapshots
This commit is contained in:
+218
@@ -0,0 +1,218 @@
|
||||
# CLI Command Template
|
||||
|
||||
Commander.js command that wraps a handler.
|
||||
|
||||
## Template
|
||||
|
||||
```typescript
|
||||
import { command, output, exitWithError } from "@outfitter/cli";
|
||||
import { createContext } from "@outfitter/contracts";
|
||||
import { myHandler } from "../handlers/my-handler.js";
|
||||
|
||||
export const myCommand = command("my-command")
|
||||
// ========================================================================
|
||||
// Metadata
|
||||
// ========================================================================
|
||||
.description("Brief description of what this command does")
|
||||
|
||||
// ========================================================================
|
||||
// Arguments (positional)
|
||||
// ========================================================================
|
||||
.argument("<id>", "Required resource ID")
|
||||
.argument("[name]", "Optional name")
|
||||
|
||||
// ========================================================================
|
||||
// Options (flags)
|
||||
// ========================================================================
|
||||
.option("-l, --limit <n>", "Maximum number of results", parseInt)
|
||||
.option("-v, --verbose", "Enable verbose output")
|
||||
.option("-t, --tags <tags...>", "Filter by tags (multiple allowed)")
|
||||
.option("--include-deleted", "Include deleted items")
|
||||
.option("-o, --output <format>", "Output format", "table")
|
||||
|
||||
// ========================================================================
|
||||
// Action
|
||||
// ========================================================================
|
||||
.action(async ({ args, flags }) => {
|
||||
// Create context
|
||||
const ctx = createContext({});
|
||||
|
||||
// Call handler
|
||||
const result = await myHandler(
|
||||
{
|
||||
id: args.id,
|
||||
name: args.name,
|
||||
limit: flags.limit,
|
||||
tags: flags.tags,
|
||||
includeDeleted: flags.includeDeleted,
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
// Handle error
|
||||
if (result.isErr()) {
|
||||
exitWithError(result.error);
|
||||
}
|
||||
|
||||
// Output success
|
||||
await output(result.value);
|
||||
})
|
||||
|
||||
// ========================================================================
|
||||
// Build
|
||||
// ========================================================================
|
||||
.build();
|
||||
```
|
||||
|
||||
## Registration
|
||||
|
||||
```typescript
|
||||
import { createCLI } from "@outfitter/cli";
|
||||
import { myCommand } from "./commands/my-command.js";
|
||||
import { otherCommand } from "./commands/other-command.js";
|
||||
|
||||
const cli = createCLI({
|
||||
name: "myapp",
|
||||
version: "1.0.0",
|
||||
description: "My CLI application",
|
||||
});
|
||||
|
||||
// Register commands
|
||||
cli.program.addCommand(myCommand);
|
||||
cli.program.addCommand(otherCommand);
|
||||
|
||||
// Parse and execute
|
||||
cli.program.parse();
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Description is clear and concise
|
||||
- [ ] Arguments use `<required>` and `[optional]` syntax
|
||||
- [ ] Options have short and long forms where appropriate
|
||||
- [ ] Numeric options use `parseInt` or `parseFloat`
|
||||
- [ ] Handler is called with structured input
|
||||
- [ ] Errors use `exitWithError()` for correct exit codes
|
||||
- [ ] Success uses `await output()` for format detection
|
||||
|
||||
## Patterns
|
||||
|
||||
### Pagination Support
|
||||
|
||||
```typescript
|
||||
import { loadCursor, saveCursor, clearCursor } from "@outfitter/cli";
|
||||
|
||||
export const listCommand = command("list")
|
||||
.option("-n, --next", "Continue from previous position")
|
||||
.option("--reset", "Reset pagination cursor")
|
||||
.option("-l, --limit <n>", "Results per page", parseInt, 20)
|
||||
.action(async ({ flags }) => {
|
||||
const paginationOpts = { command: "list", toolName: "myapp" };
|
||||
|
||||
if (flags.reset) {
|
||||
clearCursor(paginationOpts);
|
||||
console.log("Cursor reset");
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = flags.next ? loadCursor(paginationOpts)?.cursor : undefined;
|
||||
const ctx = createContext({});
|
||||
const result = await listHandler({ cursor, limit: flags.limit }, ctx);
|
||||
|
||||
if (result.isErr()) {
|
||||
exitWithError(result.error);
|
||||
}
|
||||
|
||||
await output(result.value.items);
|
||||
|
||||
if (result.value.nextCursor) {
|
||||
saveCursor(result.value.nextCursor, paginationOpts);
|
||||
console.log("\nUse --next for more results");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
### Subcommands
|
||||
|
||||
```typescript
|
||||
import { Command } from "commander";
|
||||
|
||||
const userCommand = new Command("user")
|
||||
.description("User management commands");
|
||||
|
||||
userCommand.addCommand(
|
||||
command("create")
|
||||
.argument("<email>", "User email")
|
||||
.action(async ({ args }) => { /* ... */ })
|
||||
.build()
|
||||
);
|
||||
|
||||
userCommand.addCommand(
|
||||
command("delete")
|
||||
.argument("<id>", "User ID")
|
||||
.option("--force", "Skip confirmation")
|
||||
.action(async ({ args, flags }) => { /* ... */ })
|
||||
.build()
|
||||
);
|
||||
|
||||
cli.program.addCommand(userCommand);
|
||||
```
|
||||
|
||||
### Interactive Prompts
|
||||
|
||||
```typescript
|
||||
import { confirm, text, select } from "@clack/prompts";
|
||||
|
||||
export const deleteCommand = command("delete")
|
||||
.argument("<id>", "Resource ID")
|
||||
.option("--force", "Skip confirmation")
|
||||
.action(async ({ args, flags }) => {
|
||||
if (!flags.force) {
|
||||
const confirmed = await confirm({
|
||||
message: `Delete resource ${args.id}?`,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
console.log("Cancelled");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with deletion
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
## Test Template
|
||||
|
||||
```typescript
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { createCliHarness } from "@outfitter/testing";
|
||||
import { myCommand } from "../commands/my-command.js";
|
||||
|
||||
const harness = createCliHarness(myCommand);
|
||||
|
||||
describe("my-command", () => {
|
||||
test("outputs JSON with --json flag", async () => {
|
||||
const result = await harness.run(["test-id", "--json"]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({ id: "test-id" });
|
||||
});
|
||||
|
||||
test("exits with error for missing resource", async () => {
|
||||
const result = await harness.run(["missing-id"]);
|
||||
|
||||
expect(result.exitCode).toBe(2); // not_found
|
||||
expect(result.stderr).toContain("not found");
|
||||
});
|
||||
|
||||
test("validates required arguments", async () => {
|
||||
const result = await harness.run([]);
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr).toContain("required");
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user