From 35d2d66c45c3e06a1cfaeb87494fbed49b359805 Mon Sep 17 00:00:00 2001 From: Martin Piliar Date: Sun, 13 Sep 2026 08:20:36 +0200 Subject: [PATCH] feat(issue): add issue archive command --- README.md | 1 + docs/usage.md | 6 + skills/linear-cli/SKILL.md | 1 + skills/linear-cli/references/issue.md | 19 +++ src/commands/issue/issue-archive.ts | 88 ++++++++++++ src/commands/issue/issue.ts | 2 + .../__snapshots__/issue-archive.test.ts.snap | 45 ++++++ test/commands/issue/issue-archive.test.ts | 128 ++++++++++++++++++ 8 files changed, 290 insertions(+) create mode 100644 src/commands/issue/issue-archive.ts create mode 100644 test/commands/issue/__snapshots__/issue-archive.test.ts.snap create mode 100644 test/commands/issue/issue-archive.test.ts diff --git a/README.md b/README.md index 6e62c1f0..78399ef8 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ linear issue create --project "My Project" --milestone "Phase 1" # create with linear issue update # update an issue (interactive prompts) linear issue update ENG-123 --milestone "Phase 2" # set milestone on existing issue linear issue update ENG-123 --clear-due-date --clear-parent # remove values (also --clear-estimate, --clear-project, --clear-milestone, --clear-cycle, --unassign) +linear issue archive ENG-123 --confirm # archive an issue linear issue delete # delete an issue linear issue comment list # list comments on current issue linear issue comment add # add a comment to current issue diff --git a/docs/usage.md b/docs/usage.md index 2982021b..4466e5f4 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -228,6 +228,12 @@ delete an issue: linear issue delete TEAM-123 ``` +archive an issue: + +```bash +linear issue archive TEAM-123 --confirm +``` + #### issue comments ```bash diff --git a/skills/linear-cli/SKILL.md b/skills/linear-cli/SKILL.md index a85f0aa4..2a88f803 100644 --- a/skills/linear-cli/SKILL.md +++ b/skills/linear-cli/SKILL.md @@ -213,6 +213,7 @@ linear issue linear issue agent-session linear issue agent-session list linear issue agent-session view +linear issue archive linear issue attach linear issue comment linear issue comment add diff --git a/skills/linear-cli/references/issue.md b/skills/linear-cli/references/issue.md index d3453c92..7974a173 100644 --- a/skills/linear-cli/references/issue.md +++ b/skills/linear-cli/references/issue.md @@ -28,6 +28,7 @@ Commands: describe [issueId] - Print the issue title and Linear-issue trailer commits [issueId] - Show all commits for a Linear issue (jj only) pull-request, pr [issueId] - Create a GitHub pull request with issue details + archive [issueId] - Archive an issue delete, d [issueId] - Delete an issue create - Create a linear issue update [issueId] - Update a linear issue @@ -98,6 +99,24 @@ Options: -j, --json - Output as JSON ``` +### archive + +> Archive an issue + +``` +Usage: linear issue archive [issueId] + +Description: + + Archive an issue + +Options: + + -h, --help - Show this help. + --workspace - Target workspace (uses credentials) + -y, --confirm - Skip confirmation prompt +``` + ### attach > Create a sidebar link attachment on an issue (images do not render inline) diff --git a/src/commands/issue/issue-archive.ts b/src/commands/issue/issue-archive.ts new file mode 100644 index 00000000..e3acb982 --- /dev/null +++ b/src/commands/issue/issue-archive.ts @@ -0,0 +1,88 @@ +import { Command } from "@cliffy/command" +import { Confirm } from "@cliffy/prompt" +import type { GraphQLClient } from "graphql-request" +import { gql } from "../../__codegen__/gql.ts" +import { getGraphQLClient } from "../../utils/graphql.ts" +import { getIssueIdentifier } from "../../utils/linear.ts" +import { + CliError, + handleError, + NotFoundError, + ValidationError, +} from "../../utils/errors.ts" + +export const archiveCommand = new Command() + .name("archive") + .description("Archive an issue") + .arguments("[issueId:string]") + .option("-y, --confirm", "Skip confirmation prompt") + .action(async ({ confirm }, issueId) => { + try { + const client = getGraphQLClient() + await archiveIssue(client, issueId, { confirm }) + } catch (error) { + handleError(error, "Failed to archive issue") + } + }) + +async function archiveIssue( + client: GraphQLClient, + issueId: string | undefined, + options: { confirm?: boolean }, +): Promise { + const resolvedId = await getIssueIdentifier(issueId) + if (!resolvedId) { + throw new ValidationError( + "Could not determine issue ID", + { suggestion: "Please provide an issue ID like 'ENG-123'." }, + ) + } + + const detailsQuery = gql(` + query GetIssueArchiveDetails($id: String!) { + issue(id: $id) { + identifier + title + } + } + `) + + const issueDetails = await client.request(detailsQuery, { id: resolvedId }) + if (!issueDetails.issue) { + throw new NotFoundError("Issue", resolvedId) + } + + const { identifier, title } = issueDetails.issue + if (!options.confirm) { + if (!Deno.stdin.isTerminal()) { + throw new ValidationError( + "Interactive confirmation required", + { suggestion: "Use --confirm to skip." }, + ) + } + + const confirmed = await Confirm.prompt({ + message: `Are you sure you want to archive "${identifier}: ${title}"?`, + default: false, + }) + if (!confirmed) { + console.log("Archive cancelled.") + return + } + } + + const archiveMutation = gql(` + mutation ArchiveIssue($id: String!) { + issueArchive(id: $id) { + success + } + } + `) + + const result = await client.request(archiveMutation, { id: resolvedId }) + if (!result.issueArchive.success) { + throw new CliError("Failed to archive issue") + } + + console.log(`✓ Successfully archived issue: ${identifier}: ${title}`) +} diff --git a/src/commands/issue/issue.ts b/src/commands/issue/issue.ts index 5a0d3427..b551319e 100644 --- a/src/commands/issue/issue.ts +++ b/src/commands/issue/issue.ts @@ -1,5 +1,6 @@ import { Command } from "@cliffy/command" import { attachCommand } from "./issue-attach.ts" +import { archiveCommand } from "./issue-archive.ts" import { commentCommand } from "./issue-comment.ts" import { createCommand } from "./issue-create.ts" import { deleteCommand } from "./issue-delete.ts" @@ -36,6 +37,7 @@ export const issueCommand = new Command() .command("describe", describeCommand) .command("commits", commitsCommand) .command("pull-request", pullRequestCommand) + .command("archive", archiveCommand) .command("delete", deleteCommand) .command("create", createCommand) .command("update", updateCommand) diff --git a/test/commands/issue/__snapshots__/issue-archive.test.ts.snap b/test/commands/issue/__snapshots__/issue-archive.test.ts.snap new file mode 100644 index 00000000..727459d3 --- /dev/null +++ b/test/commands/issue/__snapshots__/issue-archive.test.ts.snap @@ -0,0 +1,45 @@ +export const snapshot = {}; + +snapshot[`Issue Archive Command - Help Text 1`] = ` +stdout: +" +Usage: archive [issueId] + +Description: + + Archive an issue + +Options: + + -h, --help - Show this help. + -y, --confirm - Skip confirmation prompt + +" +stderr: +"" +`; + +snapshot[`Issue Archive Command - With Confirm 1`] = ` +stdout: +"✓ Successfully archived issue: ENG-123: Archive this issue +" +stderr: +"" +`; + +snapshot[`Issue Archive Command - Requires Confirmation 1`] = ` +stdout: +"" +stderr: +"✗ Failed to archive issue: Interactive confirmation required + Use --confirm to skip. +" +`; + +snapshot[`Issue Archive Command - Issue Not Found 1`] = ` +stdout: +"" +stderr: +"✗ Failed to archive issue: Issue not found: ENG-404 +" +`; diff --git a/test/commands/issue/issue-archive.test.ts b/test/commands/issue/issue-archive.test.ts new file mode 100644 index 00000000..750bc85f --- /dev/null +++ b/test/commands/issue/issue-archive.test.ts @@ -0,0 +1,128 @@ +import { snapshotTest } from "@cliffy/testing" +import { archiveCommand } from "../../../src/commands/issue/issue-archive.ts" +import { MockLinearServer } from "../../utils/mock_linear_server.ts" +import { commonDenoArgs } from "../../utils/test-helpers.ts" + +await snapshotTest({ + name: "Issue Archive Command - Help Text", + meta: import.meta, + colors: false, + args: ["--help"], + denoArgs: commonDenoArgs, + async fn() { + await archiveCommand.parse() + }, +}) + +await snapshotTest({ + name: "Issue Archive Command - With Confirm", + meta: import.meta, + colors: false, + args: ["eng-123", "--confirm"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetIssueArchiveDetails", + variables: { id: "ENG-123" }, + response: { + data: { + issue: { + identifier: "ENG-123", + title: "Archive this issue", + }, + }, + }, + }, + { + queryName: "ArchiveIssue", + queryIncludes: "issueArchive(id: $id)", + variables: { id: "ENG-123" }, + response: { + data: { + issueArchive: { success: true }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await archiveCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +await snapshotTest({ + name: "Issue Archive Command - Requires Confirmation", + meta: import.meta, + colors: false, + canFail: true, + args: ["ENG-123"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetIssueArchiveDetails", + variables: { id: "ENG-123" }, + response: { + data: { + issue: { + identifier: "ENG-123", + title: "Archive this issue", + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await archiveCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +await snapshotTest({ + name: "Issue Archive Command - Issue Not Found", + meta: import.meta, + colors: false, + canFail: true, + args: ["ENG-404", "--confirm"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetIssueArchiveDetails", + variables: { id: "ENG-404" }, + response: { data: { issue: null } }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await archiveCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +})