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,