Something went wrong. Try again.
CLI for Linear via GraphQL API
Something went wrong. Try again.
3.8 kB · 135 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136/** * Linear GraphQL API client. */
import type { ResolvedAuth } from "./config.ts";import { LINEAR_API_URL } from "./constants.ts";import { CliError } from "./errors.ts";
export type { ResolvedAuth };
export interface GraphQLResponse<T = unknown> { data?: T; errors?: Array<{ message: string; extensions?: Record<string, unknown> }>;}
/** * Resolve an auth value (string or ResolvedAuth) into a header string. */function authHeader(auth: string | ResolvedAuth): string { return typeof auth === "string" ? auth : auth.header;}
/** * Execute a GraphQL request and return the raw Response. */async function doGraphQL( authValue: string, query: string, variables?: Record<string, unknown>,): Promise<Response> { return fetch(LINEAR_API_URL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: authValue, }, body: JSON.stringify({ query, variables }), });}
function truncateDetail(value: string, maxLength = 200): string { return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;}
/** * Execute a GraphQL request against the Linear API and return the raw GraphQL response. */export async function graphqlRequest<T = unknown>( auth: string | ResolvedAuth, query: string, variables?: Record<string, unknown>,): Promise<GraphQLResponse<T>> { const currentHeader = authHeader(auth); const response = await doGraphQL(currentHeader, query, variables);
if (!response.ok) { if (response.status === 401) { throw new CliError("Invalid API token", { suggestion: "Run 'linear auth login' to save a valid token.", }); }
const body = await response.text(); const detail = truncateDetail(body.trim()); const message = detail ? `Linear API request failed with ${response.status} ${response.statusText}: ${detail}` : `Linear API request failed with ${response.status} ${response.statusText}`;
throw new CliError(message); }
return (await response.json()) as GraphQLResponse<T>;}
/** * Execute a GraphQL query against the Linear API. */export async function graphql<T = unknown>( auth: string | ResolvedAuth, query: string, variables?: Record<string, unknown>,): Promise<T> { const result = await graphqlRequest<T>(auth, query, variables);
if (result.errors?.length) { const firstMessage = result.errors[0]?.message ?? "Unknown Linear API error"; const entityNotFoundMatch = /^Entity not found: (.+)$/.exec(firstMessage);
if (entityNotFoundMatch) { const entityName = entityNotFoundMatch[1] ?? "Entity"; const identifier = typeof variables?.id === "string" ? variables.id.trim() : "";
if (identifier) { throw new CliError(`${entityName} not found: ${identifier}`, { suggestion: `Use a valid ${entityName.toLowerCase()} identifier or UUID.`, }); } }
throw new CliError(result.errors.map((e) => e.message).join(", ")); }
if (!result.data) { throw new CliError("No data returned from Linear API"); }
return result.data;}
/** * Fetch the authenticated user (viewer) to validate token. */export async function fetchViewer( auth: string | ResolvedAuth,): Promise<{ id: string; name: string; email: string }> { const data = await graphql<{ viewer: { id: string; name: string; email: string }; }>(auth, `query { viewer { id name email } }`);
return data.viewer;}
export async function fetchOrganization( auth: string | ResolvedAuth,): Promise<{ id: string; name: string; urlKey: string }> { const data = await graphql<{ organization: { id: string; name: string; urlKey: string }; }>(auth, `query { organization { id name urlKey } }`);
return data.organization;}