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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Type> not found: <reference>` 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

Expand Down
2 changes: 2 additions & 0 deletions skills/linear-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions skills/linear-cli/SKILL.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions skills/linear-cli/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -19,5 +22,13 @@ Options:
path)
--variables-json <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 } } }'
```
22 changes: 20 additions & 2 deletions src/commands/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:variable>",
"Variable in key=value format (coerces booleans, numbers, null; @file reads from path)",
Expand All @@ -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)
Expand Down
66 changes: 62 additions & 4 deletions test/commands/__snapshots__/api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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> - Variable in key=value format (coerces booleans, numbers, null; @file reads from
path)
--variables-json <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"}}}'
Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand Down
77 changes: 77 additions & 0 deletions test/commands/api.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -16,6 +18,81 @@ await cliffySnapshotTest({
},
})

// The `[query]` positional label used to read like a subcommand, so
// `linear api query '<document>'` 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,
Expand Down
Loading