From 72b05c3c59f5523485b17291ca73e2b77e5cd64a Mon Sep 17 00:00:00 2001 From: Peter Schilling Date: Tue, 15 Sep 2026 09:43:56 -0700 Subject: [PATCH] Stop presenting linear api's positional as a subcommand in help `linear --help` listed `api [query]` and `linear api --help` said `Usage: linear api [query]`, styled exactly like the real subcommands, so `linear api query ''` read as the natural invocation. It fails with "Too many arguments: ", which looks like the query was parsed and rejected, and `query` is a genuine subcommand elsewhere (`issue query`). This is a help-text fix only; parsing, options, exit codes, and output are unchanged. The positional is now labelled `[graphqlDocument]` (camelCase like every other multiword positional, and unlike the kebab-case subcommands it was being mistaken for), the description says the document is the only argument and that api has no subcommands, and an Examples section covers the inline, named-query-with-variables, stdin, file, and --paginate forms. Because cliffy prints the command help above the "Too many arguments" error, the mistaken invocation now explains itself. The skill template carries the same note and the generated skill docs are regenerated. The reporter also suggested rejecting unknown subcommands so that `linear api query --help` stops exiting 0 with api's help. That is cliffy's generic --help precedence (`linear issue view FOO --help` behaves the same) and changing it would alter behaviour, so it is deliberately left alone. Github-Issue: Fixes #286 Github-Issue-Url: https://github.com/schpet/linear-cli/issues/286 Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub --- CHANGELOG.md | 1 + skills/linear-cli/SKILL.md | 2 + skills/linear-cli/SKILL.template.md | 2 + skills/linear-cli/references/api.md | 17 ++++- src/commands/api.ts | 22 +++++- test/commands/__snapshots__/api.test.ts.snap | 66 ++++++++++++++++- test/commands/api.test.ts | 77 ++++++++++++++++++++ 7 files changed, 178 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a145a11..3dcb769d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixed +- `linear api` help now labels its positional `[graphqlDocument]` instead of `[query]`, which read like a subcommand and invited `linear api query '...'` (rejected with "Too many arguments"). The description states that the document is the only argument and that `api` has no subcommands, and an `Examples:` section covers inline, stdin, file, variable, and `--paginate` forms. No parsing change ([#286](https://github.com/schpet/linear-cli/issues/286)) - an unknown document, project, initiative, or issue passed to `document view` or any `comment` command is reported as ` not found: ` instead of Linear's raw "Could not find referenced …" wording, and `document view` no longer exits with a stack trace for an unknown slug (its not-found branch re-threw instead of reporting, and was unreachable until the not-found detection was fixed) - `cycle list` and `milestone list` now paginate instead of taking Linear's default page, so a team with more than 50 cycles or a project with more than 50 milestones is no longer silently truncated diff --git a/skills/linear-cli/SKILL.md b/skills/linear-cli/SKILL.md index 89fa9f8c..2872a2f5 100644 --- a/skills/linear-cli/SKILL.md +++ b/skills/linear-cli/SKILL.md @@ -353,6 +353,8 @@ grep -A 30 "^type Issue " "${TMPDIR:-/tmp}/linear-schema.graphql" ### Make a GraphQL request +`linear api` takes the GraphQL document as its only positional argument and has no subcommands. Put a leading `query` or `mutation` keyword inside that quoted document: use `linear api 'query { ... }'`, never `linear api query '...'`. `linear issue query` is a separate, real subcommand for searching issues. + **Important:** GraphQL queries containing non-null type markers (e.g. `String` followed by an exclamation mark) must be passed via heredoc stdin to avoid escaping issues. Simple queries without those markers can be passed inline. ```bash diff --git a/skills/linear-cli/SKILL.template.md b/skills/linear-cli/SKILL.template.md index abc65972..98400153 100644 --- a/skills/linear-cli/SKILL.template.md +++ b/skills/linear-cli/SKILL.template.md @@ -218,6 +218,8 @@ grep -A 30 "^type Issue " "${TMPDIR:-/tmp}/linear-schema.graphql" ### Make a GraphQL request +`linear api` takes the GraphQL document as its only positional argument and has no subcommands. Put a leading `query` or `mutation` keyword inside that quoted document: use `linear api 'query { ... }'`, never `linear api query '...'`. `linear issue query` is a separate, real subcommand for searching issues. + **Important:** GraphQL queries containing non-null type markers (e.g. `String` followed by an exclamation mark) must be passed via heredoc stdin to avoid escaping issues. Simple queries without those markers can be passed inline. ```bash diff --git a/skills/linear-cli/references/api.md b/skills/linear-cli/references/api.md index 3b308aff..e311e821 100644 --- a/skills/linear-cli/references/api.md +++ b/skills/linear-cli/references/api.md @@ -5,11 +5,14 @@ ## Usage ``` -Usage: linear api [query] +Usage: linear api [graphqlDocument] Description: - Make a raw GraphQL API request + Make a raw GraphQL API request + + Pass the GraphQL document as one quoted argument or on stdin. The api command has no subcommands: a leading query or mutation keyword + belongs inside that document. Options: @@ -19,5 +22,13 @@ Options: path) --variables-json - JSON object of variables (merged with --variable, which takes precedence) --paginate - Auto-paginate a single connection field using cursor pagination - --silent - Suppress response output (exit code still reflects errors) + --silent - Suppress response output (exit code still reflects errors) + +Examples: + + Run an inline document linear api '{ viewer { id name } }' + Run a named query with variables linear api 'query RecentIssues($first: Int) { issues(first: $first) { nodes { identifier title } } }' --variable first=5 + Pipe a document from stdin echo '{ viewer { id } }' | linear api + Read a document from a file linear api - < issues.graphql + Auto-paginate a connection linear api --paginate 'query($after: String) { issues(first: 50, after: $after) { nodes { identifier } pageInfo { hasNextPage endCursor } } }' ``` diff --git a/src/commands/api.ts b/src/commands/api.ts index 3d6f2eae..d5822205 100644 --- a/src/commands/api.ts +++ b/src/commands/api.ts @@ -26,9 +26,13 @@ class VariableType extends Type<[string, string]> { export const apiCommand = new Command() .name("api") - .description("Make a raw GraphQL API request") + .description( + `Make a raw GraphQL API request + +Pass the GraphQL document as one quoted argument or on stdin. The api command has no subcommands: a leading query or mutation keyword belongs inside that document.`, + ) .type("variable", new VariableType()) - .arguments("[query:string]") + .arguments("[graphqlDocument:string]") .option( "--variable ", "Variable in key=value format (coerces booleans, numbers, null; @file reads from path)", @@ -46,6 +50,20 @@ export const apiCommand = new Command() "--silent", "Suppress response output (exit code still reflects errors)", ) + .example("Run an inline document", "linear api '{ viewer { id name } }'") + .example( + "Run a named query with variables", + "linear api 'query RecentIssues($first: Int) { issues(first: $first) { nodes { identifier title } } }' --variable first=5", + ) + .example( + "Pipe a document from stdin", + "echo '{ viewer { id } }' | linear api", + ) + .example("Read a document from a file", "linear api - < issues.graphql") + .example( + "Auto-paginate a connection", + "linear api --paginate 'query($after: String) { issues(first: 50, after: $after) { nodes { identifier } pageInfo { hasNextPage endCursor } } }'", + ) .action(async (options, query?: string) => { try { const resolvedQuery = await resolveQuery(query) diff --git a/test/commands/__snapshots__/api.test.ts.snap b/test/commands/__snapshots__/api.test.ts.snap index 82213c26..85323cab 100644 --- a/test/commands/__snapshots__/api.test.ts.snap +++ b/test/commands/__snapshots__/api.test.ts.snap @@ -3,11 +3,14 @@ export const snapshot = {}; snapshot[`API Command - Help Text 1`] = ` stdout: " -Usage: api [query] +Usage: api [graphqlDocument] Description: - Make a raw GraphQL API request + Make a raw GraphQL API request + + Pass the GraphQL document as one quoted argument or on stdin. The api command has no subcommands: a leading query or mutation keyword + belongs inside that document. Options: @@ -18,11 +21,55 @@ Options: --paginate - Auto-paginate a single connection field using cursor pagination --silent - Suppress response output (exit code still reflects errors) +Examples: + + Run an inline document linear api '{ viewer { id name } }' + Run a named query with variables linear api 'query RecentIssues(\$first: Int) { issues(first: \$first) { nodes { identifier title } } }' --variable first=5 + Pipe a document from stdin echo '{ viewer { id } }' | linear api + Read a document from a file linear api - < issues.graphql + Auto-paginate a connection linear api --paginate 'query(\$after: String) { issues(first: 50, after: \$after) { nodes { identifier } pageInfo { hasNextPage endCursor } } }' + " stderr: "" `; +snapshot[`API Command - Query Keyword Mistaken For Subcommand 1`] = ` +stdout: +" +Usage: api [graphqlDocument] + +Description: + + Make a raw GraphQL API request + + Pass the GraphQL document as one quoted argument or on stdin. The api command has no subcommands: a leading query or mutation keyword + belongs inside that document. + +Options: + + -h, --help - Show this help. + --variable - Variable in key=value format (coerces booleans, numbers, null; @file reads from + path) + --variables-json - JSON object of variables (merged with --variable, which takes precedence) + --paginate - Auto-paginate a single connection field using cursor pagination + --silent - Suppress response output (exit code still reflects errors) + +Examples: + + Run an inline document linear api '{ viewer { id name } }' + Run a named query with variables linear api 'query RecentIssues(\$first: Int) { issues(first: \$first) { nodes { identifier title } } }' --variable first=5 + Pipe a document from stdin echo '{ viewer { id } }' | linear api + Read a document from a file linear api - < issues.graphql + Auto-paginate a connection linear api --paginate 'query(\$after: String) { issues(first: 50, after: \$after) { nodes { identifier } pageInfo { hasNextPage endCursor } } }' + +" +stderr: +" error: Too many arguments: query { viewer { id } } + +" +`; + snapshot[`API Command - Basic Query 1`] = ` stdout: '{"data":{"viewer":{"id":"user-1","name":"Test User"}}}' @@ -57,11 +104,14 @@ stderr: snapshot[`API Command - Invalid Variable Format 1`] = ` stdout: " -Usage: api [query] +Usage: api [graphqlDocument] Description: - Make a raw GraphQL API request + Make a raw GraphQL API request + + Pass the GraphQL document as one quoted argument or on stdin. The api command has no subcommands: a leading query or mutation keyword + belongs inside that document. Options: @@ -72,6 +122,14 @@ Options: --paginate - Auto-paginate a single connection field using cursor pagination --silent - Suppress response output (exit code still reflects errors) +Examples: + + Run an inline document linear api '{ viewer { id name } }' + Run a named query with variables linear api 'query RecentIssues(\$first: Int) { issues(first: \$first) { nodes { identifier title } } }' --variable first=5 + Pipe a document from stdin echo '{ viewer { id } }' | linear api + Read a document from a file linear api - < issues.graphql + Auto-paginate a connection linear api --paginate 'query(\$after: String) { issues(first: 50, after: \$after) { nodes { identifier } pageInfo { hasNextPage endCursor } } }' + " stderr: " error: Invalid variable format: badformat. Variables must be in key=value format, e.g. --variable teamId=abc diff --git a/test/commands/api.test.ts b/test/commands/api.test.ts index 245b5eed..12874906 100644 --- a/test/commands/api.test.ts +++ b/test/commands/api.test.ts @@ -1,4 +1,6 @@ import { snapshotTest as cliffySnapshotTest } from "@cliffy/testing" +import { assertEquals, assertStringIncludes } from "@std/assert" +import { fromFileUrl, join } from "@std/path" import { apiCommand } from "../../src/commands/api.ts" import { loadCredentials } from "../../src/credentials.ts" import { MockLinearServer } from "../utils/mock_linear_server.ts" @@ -16,6 +18,81 @@ await cliffySnapshotTest({ }, }) +// The `[query]` positional label used to read like a subcommand, so +// `linear api query ''` was a natural mistake (#286). Parsing is +// unchanged; the snapshot pins the help that cliffy prints above the error, +// which now names the positional and explains that api has no subcommands. +await cliffySnapshotTest({ + name: "API Command - Query Keyword Mistaken For Subcommand", + meta: import.meta, + colors: false, + args: ["query", "query { viewer { id } }"], + denoArgs, + canFail: true, + async fn() { + await apiCommand.parse() + }, +}) + +// Runs the real entry point so the root command's help row and the exit code +// are covered; the cliffy snapshot helper imports apiCommand directly and, with +// canFail, accepts any non-zero status. +async function runMain( + args: string[], +): Promise<{ code: number; stdout: string; stderr: string }> { + const mainPath = fromFileUrl(new URL("../../src/main.ts", import.meta.url)) + const denoJsonPath = fromFileUrl(new URL("../../deno.json", import.meta.url)) + const homeDir = Deno.env.get("HOME") + const denoDir = Deno.env.get("DENO_DIR") ?? + (homeDir == null ? undefined : join(homeDir, ".cache", "deno")) + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--allow-all", + "--quiet", + `--config=${denoJsonPath}`, + mainPath, + ...args, + ], + clearEnv: true, + env: { + PATH: Deno.env.get("PATH") ?? "", + HOME: homeDir ?? "", + ...(denoDir == null ? {} : { DENO_DIR: denoDir }), + ...(Deno.build.os === "windows" + ? { SystemRoot: Deno.env.get("SystemRoot") ?? "" } + : {}), + LINEAR_API_KEY: "Bearer test-token", + NO_COLOR: "true", + }, + stdout: "piped", + stderr: "piped", + }) + const { code, stdout, stderr } = await command.output() + return { + code, + stdout: new TextDecoder().decode(stdout), + stderr: new TextDecoder().decode(stderr), + } +} + +Deno.test("API Command - Root Help Names The Positional", async () => { + const { code, stdout } = await runMain(["--help"]) + assertEquals(code, 0) + const apiRow = stdout.split("\n").find((line) => /^\s+api\s/.test(line)) + assertStringIncludes(apiRow ?? "", "[graphqlDocument]") +}) + +Deno.test("API Command - Query Keyword Still Exits 2", async () => { + const { code, stderr } = await runMain([ + "api", + "query", + "query { viewer { id } }", + ]) + assertEquals(code, 2) + assertStringIncludes(stderr, "Too many arguments: query { viewer { id } }") +}) + await cliffySnapshotTest({ name: "API Command - Basic Query", meta: import.meta,