Something went wrong. Try again.
CLI for Linear via GraphQL API
Something went wrong. Try again.
15 kB · 601 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602import { graphql } from "../api.ts";import { pad, printTable, requireToken, resolveAssignee, resolveDelegate, useJson,} from "./shared.ts";
export interface ListIssuesOptions { team?: string; assignee?: string; delegate?: string; agent?: string; state?: string; label?: string; limit?: number; json?: boolean;}
export async function listIssues(options: ListIssuesOptions): Promise<void> { const token = await requireToken(); const json = await useJson(options.json); const limit = options.limit ?? 50;
const filter: Record<string, unknown> = {}; if (options.team) filter.team = { key: { eq: options.team } };
const assigneeRaw = options.assignee; if (assigneeRaw) { if (assigneeRaw.toLowerCase() === "me") { filter.assignee = { isMe: { eq: true } }; } else if (assigneeRaw.toLowerCase() === "none") { filter.assignee = { null: true }; } else { try { const assigneeId = await resolveAssignee(token, assigneeRaw); if (assigneeId) { filter.assignee = { id: { eq: assigneeId } }; } else { filter.assignee = { null: true }; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } }
const delegateRaw = options.delegate ?? options.agent; if (delegateRaw) { if (delegateRaw.toLowerCase() === "none") { filter.delegate = { null: true }; } else { try { const delegateId = await resolveDelegate(token, delegateRaw); if (delegateId) { filter.delegate = { id: { eq: delegateId } }; } else { filter.delegate = { null: true }; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } }
if (options.state) filter.state = { name: { eqIgnoreCase: options.state } }; if (options.label) filter.labels = { name: { eqIgnoreCase: options.label } };
const query = ` query Issues($first: Int, $filter: IssueFilter) { issues(first: $first, filter: $filter) { nodes { id identifier title priority priorityLabel state { name type } assignee { name email } delegate { name email app } team { key name } labels { nodes { name } } createdAt updatedAt } } } `;
const data = await graphql<{ issues: { nodes: Array<{ id: string; identifier: string; title: string; priority: number; priorityLabel: string; state: { name: string; type: string }; assignee: { name: string; email: string } | null; delegate: { name: string; email: string; app: boolean } | null; team: { key: string; name: string }; labels: { nodes: Array<{ name: string }> }; createdAt: string; updatedAt: string; }>; }; }>(token, query, { first: limit, filter });
const issues = data.issues.nodes;
if (json) { console.log(JSON.stringify(issues, null, 2)); return; }
if (issues.length === 0) { console.log("No issues found."); return; }
const headers = [ "IDENTIFIER", "TITLE", "STATE", "PRIORITY", "ASSIGNEE", "AGENT", ]; const rows = issues.map((issue) => [ issue.identifier, issue.title, issue.state.name, issue.priorityLabel, issue.assignee?.name ?? "", issue.delegate?.name ?? "", ]);
printTable(headers, rows);}
export interface GetIssueOptions { identifier: string; json?: boolean;}
export async function getIssue(options: GetIssueOptions): Promise<void> { const token = await requireToken(); const json = await useJson(options.json);
const query = ` query Issue($id: String!) { issue(id: $id) { id identifier title description priority priorityLabel state { name type } assignee { name email } delegate { name email app } team { key name } project { name } cycle { number name } labels { nodes { name } } estimate dueDate createdAt updatedAt url } } `;
const data = await graphql<{ issue: { id: string; identifier: string; title: string; description: string | null; priority: number; priorityLabel: string; state: { name: string; type: string }; assignee: { name: string; email: string } | null; delegate: { name: string; email: string; app: boolean } | null; team: { key: string; name: string }; project: { name: string } | null; cycle: { number: number; name: string } | null; labels: { nodes: Array<{ name: string }> }; estimate: number | null; dueDate: string | null; createdAt: string; updatedAt: string; url: string; }; }>(token, query, { id: options.identifier });
const issue = data.issue;
if (json) { console.log(JSON.stringify(issue, null, 2)); return; }
const description = issue.description ? issue.description.length > 200 ? `${issue.description.slice(0, 200)}...` : issue.description : "";
const fields: Array<[string, string]> = [ ["Identifier", issue.identifier], ["Title", issue.title], ["State", `${issue.state.name} (${issue.state.type})`], ["Priority", issue.priorityLabel], ["Team", `${issue.team.name} (${issue.team.key})`], [ "Assignee", issue.assignee ? `${issue.assignee.name} <${issue.assignee.email}>` : "", ], [ "Agent", issue.delegate ? `${issue.delegate.name} <${issue.delegate.email}>${issue.delegate.app ? " (app)" : ""}` : "", ], ["Project", issue.project?.name ?? ""], ["Cycle", issue.cycle ? `#${issue.cycle.number} ${issue.cycle.name}` : ""], ["Labels", issue.labels.nodes.map((l) => l.name).join(", ")], ["Estimate", issue.estimate != null ? String(issue.estimate) : ""], ["Due Date", issue.dueDate ?? ""], ["Description", description], ["Created", issue.createdAt], ["Updated", issue.updatedAt], ["URL", issue.url], ];
const maxKey = Math.max(...fields.map(([k]) => k.length)); for (const [key, value] of fields) { if (value) { console.log(`${pad(key, maxKey)} ${value}`); } }}
export interface CreateIssueOptions { team: string; title: string; description?: string; priority?: number; assignee?: string; delegate?: string; agent?: string; state?: string; label?: string; project?: string; estimate?: number; json?: boolean;}
export async function createIssue(options: CreateIssueOptions): Promise<void> { const token = await requireToken(); const json = await useJson(options.json);
const teamQuery = ` query TeamByKey($filter: TeamFilter!) { teams(filter: $filter, first: 1) { nodes { id key } } } `;
const teamData = await graphql<{ teams: { nodes: Array<{ id: string; key: string }> }; }>(token, teamQuery, { filter: { key: { eq: options.team } } });
const team = teamData.teams.nodes[0]; if (!team) { console.error(`Error: Team with key "${options.team}" not found.`); process.exit(1); }
const input: Record<string, unknown> = { teamId: team.id, title: options.title, };
if (options.description) input.description = options.description; if (options.priority != null) input.priority = options.priority;
if (options.assignee) { try { const assigneeId = await resolveAssignee(token, options.assignee); if (assigneeId) { input.assigneeId = assigneeId; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } }
const delegateRaw = options.delegate ?? options.agent; if (delegateRaw) { try { const delegateId = await resolveDelegate(token, delegateRaw); if (delegateId) { input.delegateId = delegateId; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } }
if (options.state) input.stateId = options.state; if (options.label) input.labelIds = options.label.split(","); if (options.project) input.projectId = options.project; if (options.estimate != null) input.estimate = options.estimate;
const mutation = ` mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title state { name } team { key } assignee { name email } delegate { name email app } url } } } `;
const data = await graphql<{ issueCreate: { success: boolean; issue: { id: string; identifier: string; title: string; state: { name: string }; team: { key: string }; assignee: { name: string; email: string } | null; delegate: { name: string; email: string; app: boolean } | null; url: string; }; }; }>(token, mutation, { input });
if (!data.issueCreate.success) { console.error("Error: Failed to create issue."); process.exit(1); }
const issue = data.issueCreate.issue;
if (json) { console.log(JSON.stringify(issue, null, 2)); return; }
console.log(`Created ${issue.identifier}: ${issue.title}`); console.log(`State: ${issue.state.name}`); if (issue.assignee) { console.log(`Assignee: ${issue.assignee.name} <${issue.assignee.email}>`); } if (issue.delegate) { console.log(`Agent: ${issue.delegate.name} <${issue.delegate.email}>`); } console.log(`URL: ${issue.url}`);}
export interface UpdateIssueOptions { identifier: string; title?: string; description?: string; priority?: number; state?: string; assignee?: string; delegate?: string; agent?: string; label?: string; project?: string; estimate?: number; json?: boolean;}
export async function updateIssue(options: UpdateIssueOptions): Promise<void> { const token = await requireToken(); const json = await useJson(options.json);
const input: Record<string, unknown> = {};
if (options.title) input.title = options.title; if (options.description) input.description = options.description; if (options.priority != null) input.priority = options.priority; if (options.state) input.stateId = options.state;
if (options.assignee) { try { const assigneeId = await resolveAssignee(token, options.assignee); if (assigneeId) { input.assigneeId = assigneeId; } else { input.assigneeId = null; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } }
const delegateRaw = options.delegate ?? options.agent; if (delegateRaw) { try { const delegateId = await resolveDelegate(token, delegateRaw); if (delegateId) { input.delegateId = delegateId; } else { input.delegateId = null; } } catch (err) { console.error( `Error: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } }
if (options.label) input.labelIds = options.label.split(","); if (options.project) input.projectId = options.project; if (options.estimate != null) input.estimate = options.estimate;
if (Object.keys(input).length === 0) { console.error("Error: No update flags provided."); process.exit(1); }
const mutation = ` mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier title state { name } assignee { name email } delegate { name email app } url } } } `;
const data = await graphql<{ issueUpdate: { success: boolean; issue: { id: string; identifier: string; title: string; state: { name: string }; assignee: { name: string; email: string } | null; delegate: { name: string; email: string; app: boolean } | null; url: string; }; }; }>(token, mutation, { id: options.identifier, input });
if (!data.issueUpdate.success) { console.error("Error: Failed to update issue."); process.exit(1); }
const issue = data.issueUpdate.issue;
if (json) { console.log(JSON.stringify(issue, null, 2)); return; }
console.log(`Updated ${issue.identifier}: ${issue.title}`); console.log(`State: ${issue.state.name}`); if (issue.assignee) { console.log(`Assignee: ${issue.assignee.name} <${issue.assignee.email}>`); } if (issue.delegate) { console.log(`Agent: ${issue.delegate.name} <${issue.delegate.email}>`); } console.log(`URL: ${issue.url}`);}
export interface CloseIssueOptions { identifier: string; json?: boolean;}
export async function closeIssue(options: CloseIssueOptions): Promise<void> { const token = await requireToken(); const json = await useJson(options.json);
const issueQuery = ` query Issue($id: String!) { issue(id: $id) { id identifier team { id states { nodes { id name type } } } } } `;
const issueData = await graphql<{ issue: { id: string; identifier: string; team: { id: string; states: { nodes: Array<{ id: string; name: string; type: string }>; }; }; }; }>(token, issueQuery, { id: options.identifier });
const issue = issueData.issue; const completedState = issue.team.states.nodes.find( (s) => s.type === "completed", );
if (!completedState) { console.error("Error: No completed state found for this team."); process.exit(1); }
const mutation = ` mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier title state { name } url } } } `;
const data = await graphql<{ issueUpdate: { success: boolean; issue: { id: string; identifier: string; title: string; state: { name: string }; url: string; }; }; }>(token, mutation, { id: issue.id, input: { stateId: completedState.id } });
if (!data.issueUpdate.success) { console.error("Error: Failed to close issue."); process.exit(1); }
const updated = data.issueUpdate.issue;
if (json) { console.log(JSON.stringify(updated, null, 2)); return; }
console.log(`Closed ${updated.identifier}: ${updated.title}`); console.log(`State: ${updated.state.name}`); console.log(`URL: ${updated.url}`);}