diff --git a/skills/linear-cli/references/cycle.md b/skills/linear-cli/references/cycle.md index c536c9ec..3b0a5188 100644 --- a/skills/linear-cli/references/cycle.md +++ b/skills/linear-cli/references/cycle.md @@ -39,7 +39,8 @@ Options: -h, --help - Show this help. -w, --workspace - Target workspace (uses credentials) - --team - Team key (defaults to current team) + --team - Team key (defaults to current team) + -j, --json - Output cycle data as JSON ``` ### view diff --git a/skills/linear-cli/references/issue.md b/skills/linear-cli/references/issue.md index 12de728d..ef305625 100644 --- a/skills/linear-cli/references/issue.md +++ b/skills/linear-cli/references/issue.md @@ -83,7 +83,8 @@ Options: --limit - Maximum number of issues to fetch (default: 50, use 0 for unlimited) (Default: 50) -w, --web - Open in web browser -a, --app - Open in Linear.app - --no-pager - Disable automatic paging for long output + --no-pager - Disable automatic paging for long output + -j, --json - Output issue data as JSON ``` ### title @@ -272,6 +273,7 @@ Options: --milestone - Name of the project milestone --cycle - Cycle name, number, or 'active' --no-use-default-template - Do not use default template for the issue + -j, --json - Output created issue data as JSON --no-interactive - Disable interactive prompts -t, --title - Title of the issue ``` diff --git a/src/commands/cycle/cycle-list.ts b/src/commands/cycle/cycle-list.ts index fc99cedf..09520759 100644 --- a/src/commands/cycle/cycle-list.ts +++ b/src/commands/cycle/cycle-list.ts @@ -56,7 +56,8 @@ export const listCommand = new Command() .name("list") .description("List cycles for a team") .option("--team <team:string>", "Team key (defaults to current team)") - .action(async ({ team }) => { + .option("-j, --json", "Output cycle data as JSON") + .action(async ({ team, json }) => { try { const teamKey = team || getTeamKey() if (!teamKey) { @@ -71,7 +72,7 @@ export const listCommand = new Command() } const { Spinner } = await import("@std/cli/unstable-spinner") - const showSpinner = shouldShowSpinner() + const showSpinner = shouldShowSpinner() && !json const spinner = showSpinner ? new Spinner() : null spinner?.start() @@ -81,6 +82,11 @@ export const listCommand = new Command() const cycles = result.team?.cycles?.nodes || [] + if (cycles.length === 0 && json) { + console.log(JSON.stringify([], null, 2)) + return + } + if (cycles.length === 0) { console.log("No cycles found for this team.") return @@ -90,6 +96,23 @@ export const listCommand = new Command() b.startsAt.localeCompare(a.startsAt) ) + if (json) { + const jsonOutput = sortedCycles.map((cycle) => ({ + id: cycle.id, + number: cycle.number, + name: cycle.name, + startsAt: cycle.startsAt, + endsAt: cycle.endsAt, + completedAt: cycle.completedAt, + isActive: cycle.isActive, + isFuture: cycle.isFuture, + isPast: cycle.isPast, + status: getCycleStatus(cycle), + })) + console.log(JSON.stringify(jsonOutput, null, 2)) + return + } + const { columns } = Deno.stdout.isTerminal() ? Deno.consoleSize() : { columns: 120 } diff --git a/src/commands/issue/issue-create.ts b/src/commands/issue/issue-create.ts index d9f37b29..3e165604 100644 --- a/src/commands/issue/issue-create.ts +++ b/src/commands/issue/issue-create.ts @@ -508,6 +508,7 @@ export const createCommand = new Command() "--no-use-default-template", "Do not use default template for the issue", ) + .option("-j, --json", "Output created issue data as JSON") .option("--no-interactive", "Disable interactive prompts") .option("-t, --title <title:string>", "Title of the issue") .action( @@ -529,6 +530,7 @@ export const createCommand = new Command() milestone, cycle, interactive, + json, title, }, ) => { @@ -840,7 +842,20 @@ export const createCommand = new Command() } const issueId = issue.id spinner?.stop() - console.log(issue.url) + if (json) { + console.log(JSON.stringify( + { + id: issue.id, + identifier: issue.identifier, + url: issue.url, + team: issue.team.key, + }, + null, + 2, + )) + } else { + console.log(issue.url) + } if (start) { await startWorkOnIssue(issueId, issue.team.key) diff --git a/src/commands/issue/issue-list.ts b/src/commands/issue/issue-list.ts index 841bfcc5..d185fb04 100644 --- a/src/commands/issue/issue-list.ts +++ b/src/commands/issue/issue-list.ts @@ -100,6 +100,7 @@ export const listCommand = new Command() .option("-w, --web", "Open in web browser") .option("-a, --app", "Open in Linear.app") .option("--no-pager", "Disable automatic paging for long output") + .option("-j, --json", "Output issue data as JSON") .action( async ( { @@ -117,6 +118,7 @@ export const listCommand = new Command() milestone, limit, pager, + json, }, ) => { const usePager = pager !== false @@ -206,7 +208,7 @@ export const listCommand = new Command() } const { Spinner } = await import("@std/cli/unstable-spinner") - const showSpinner = shouldShowSpinner() + const showSpinner = shouldShowSpinner() && !json const spinner = showSpinner ? new Spinner() : null spinner?.start() @@ -225,11 +227,34 @@ export const listCommand = new Command() spinner?.stop() const issues = result.issues?.nodes || [] + if (issues.length === 0 && json) { + console.log(JSON.stringify([], null, 2)) + return + } + if (issues.length === 0) { console.log("No issues found.") return } + if (json) { + const jsonOutput = issues.map((issue) => ({ + id: issue.id, + identifier: issue.identifier, + title: issue.title, + priority: issue.priority, + estimate: issue.estimate, + state: { id: issue.state.id, name: issue.state.name }, + labels: issue.labels.nodes.map((l) => ({ id: l.id, name: l.name })), + assignee: issue.assignee + ? { initials: issue.assignee.initials } + : null, + updatedAt: issue.updatedAt, + })) + console.log(JSON.stringify(jsonOutput, null, 2)) + return + } + const { columns } = Deno.stdout.isTerminal() ? Deno.consoleSize() : { columns: 120 } diff --git a/test/commands/cycle/__snapshots__/cycle-list.test.ts.snap b/test/commands/cycle/__snapshots__/cycle-list.test.ts.snap index 6c5b44d2..d3d236ab 100644 --- a/test/commands/cycle/__snapshots__/cycle-list.test.ts.snap +++ b/test/commands/cycle/__snapshots__/cycle-list.test.ts.snap @@ -13,6 +13,7 @@ Options: -h, --help - Show this help. --team <team> - Team key (defaults to current team) + -j, --json - Output cycle data as JSON " stderr: @@ -30,6 +31,51 @@ stderr: "" `; +snapshot[`Cycle List Command - JSON Output 1`] = ` +stdout: +'[ + { + "id": "cycle-3", + "number": 14, + "name": "Sprint 14", + "startsAt": "2026-03-10T00:00:00.000Z", + "endsAt": "2026-03-24T00:00:00.000Z", + "completedAt": null, + "isActive": false, + "isFuture": true, + "isPast": false, + "status": "Upcoming" + }, + { + "id": "cycle-2", + "number": 13, + "name": "Sprint 13", + "startsAt": "2026-02-24T00:00:00.000Z", + "endsAt": "2026-03-10T00:00:00.000Z", + "completedAt": null, + "isActive": true, + "isFuture": false, + "isPast": false, + "status": "Active" + }, + { + "id": "cycle-1", + "number": 12, + "name": "Sprint 12", + "startsAt": "2026-02-10T00:00:00.000Z", + "endsAt": "2026-02-24T00:00:00.000Z", + "completedAt": "2026-02-24T00:00:00.000Z", + "isActive": false, + "isFuture": false, + "isPast": true, + "status": "Completed" + } +] +' +stderr: +"" +`; + snapshot[`Cycle List Command - No Cycles Found 1`] = ` stdout: "No cycles found for this team. diff --git a/test/commands/cycle/cycle-list.test.ts b/test/commands/cycle/cycle-list.test.ts index 87163e80..0ced6c01 100644 --- a/test/commands/cycle/cycle-list.test.ts +++ b/test/commands/cycle/cycle-list.test.ts @@ -1,6 +1,9 @@ import { snapshotTest as cliffySnapshotTest } from "@cliffy/testing" import { listCommand } from "../../../src/commands/cycle/cycle-list.ts" -import { commonDenoArgs } from "../../utils/test-helpers.ts" +import { + commonDenoArgs, + setupMockLinearServer, +} from "../../utils/test-helpers.ts" import { MockLinearServer } from "../../utils/mock_linear_server.ts" await cliffySnapshotTest({ @@ -97,6 +100,83 @@ await cliffySnapshotTest({ }, }) +await cliffySnapshotTest({ + name: "Cycle List Command - JSON Output", + meta: import.meta, + colors: false, + args: ["--json", "--team", "ENG"], + denoArgs: commonDenoArgs, + async fn() { + const { cleanup } = await setupMockLinearServer([ + { + queryName: "GetTeamIdByKey", + response: { + data: { + teams: { + nodes: [{ id: "team-eng-id" }], + }, + }, + }, + }, + { + queryName: "GetTeamCycles", + variables: { teamId: "team-eng-id" }, + response: { + data: { + team: { + id: "team-eng-id", + name: "Engineering", + cycles: { + nodes: [ + { + id: "cycle-1", + number: 12, + name: "Sprint 12", + startsAt: "2026-02-10T00:00:00.000Z", + endsAt: "2026-02-24T00:00:00.000Z", + completedAt: "2026-02-24T00:00:00.000Z", + isActive: false, + isFuture: false, + isPast: true, + }, + { + id: "cycle-2", + number: 13, + name: "Sprint 13", + startsAt: "2026-02-24T00:00:00.000Z", + endsAt: "2026-03-10T00:00:00.000Z", + completedAt: null, + isActive: true, + isFuture: false, + isPast: false, + }, + { + id: "cycle-3", + number: 14, + name: "Sprint 14", + startsAt: "2026-03-10T00:00:00.000Z", + endsAt: "2026-03-24T00:00:00.000Z", + completedAt: null, + isActive: false, + isFuture: true, + isPast: false, + }, + ], + }, + }, + }, + }, + }, + ]) + + try { + await listCommand.parse() + } finally { + await cleanup() + } + }, +}) + await cliffySnapshotTest({ name: "Cycle List Command - No Cycles Found", meta: import.meta, diff --git a/test/commands/issue/__snapshots__/issue-create.test.ts.snap b/test/commands/issue/__snapshots__/issue-create.test.ts.snap index 729a7283..46b09c1a 100644 --- a/test/commands/issue/__snapshots__/issue-create.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-create.test.ts.snap @@ -27,6 +27,7 @@ Options: --milestone <milestone> - Name of the project milestone --cycle <cycle> - Cycle name, number, or 'active' --no-use-default-template - Do not use default template for the issue + -j, --json - Output created issue data as JSON --no-interactive - Disable interactive prompts -t, --title <title> - Title of the issue @@ -45,6 +46,21 @@ stderr: "" `; +snapshot[`Issue Create Command - JSON Output 1`] = ` +stdout: +'Creating issue in ENG + +{ + "id": "issue-json-123", + "identifier": "ENG-555", + "url": "https://linear.app/test-team/issue/ENG-555/test-json-output", + "team": "ENG" +} +' +stderr: +"" +`; + snapshot[`Issue Create Command - With Milestone 1`] = ` stdout: "Creating issue in ENG diff --git a/test/commands/issue/__snapshots__/issue-list.test.ts.snap b/test/commands/issue/__snapshots__/issue-list.test.ts.snap index 68b208a7..2b02a27c 100644 --- a/test/commands/issue/__snapshots__/issue-list.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-list.test.ts.snap @@ -27,8 +27,53 @@ Options: -w, --web - Open in web browser -a, --app - Open in Linear.app --no-pager - Disable automatic paging for long output + -j, --json - Output issue data as JSON \` stderr: "" `; + +snapshot[`Issue List Command - JSON Output 1`] = ` +stdout: +'[ + { + "id": "issue-1", + "identifier": "ENG-101", + "title": "Fix login bug", + "priority": 1, + "estimate": 3, + "state": { + "id": "state-1", + "name": "In Progress" + }, + "labels": [ + { + "id": "label-1", + "name": "bug" + } + ], + "assignee": { + "initials": "JD" + }, + "updatedAt": "2026-03-10T12:00:00.000Z" + }, + { + "id": "issue-2", + "identifier": "ENG-102", + "title": "Add dark mode", + "priority": 3, + "estimate": null, + "state": { + "id": "state-2", + "name": "Todo" + }, + "labels": [], + "assignee": null, + "updatedAt": "2026-03-09T08:30:00.000Z" + } +] +' +stderr: +"" +`; diff --git a/test/commands/issue/issue-create.test.ts b/test/commands/issue/issue-create.test.ts index 17811fb6..f9aa81dc 100644 --- a/test/commands/issue/issue-create.test.ts +++ b/test/commands/issue/issue-create.test.ts @@ -94,6 +94,62 @@ await snapshotTest({ }, }) +// Test creating an issue with --json flag +await snapshotTest({ + name: "Issue Create Command - JSON Output", + meta: import.meta, + colors: false, + args: [ + "--title", + "Test json output", + "--team", + "ENG", + "--no-interactive", + "--json", + ], + denoArgs: commonDenoArgs, + async fn() { + const { cleanup } = await setupMockLinearServer([ + { + queryName: "GetTeamIdByKey", + variables: { team: "ENG" }, + response: { + data: { + teams: { + nodes: [{ id: "team-eng-id" }], + }, + }, + }, + }, + { + queryName: "CreateIssue", + response: { + data: { + issueCreate: { + success: true, + issue: { + id: "issue-json-123", + identifier: "ENG-555", + url: + "https://linear.app/test-team/issue/ENG-555/test-json-output", + team: { + key: "ENG", + }, + }, + }, + }, + }, + }, + ], { LINEAR_TEAM_ID: "ENG" }) + + try { + await createCommand.parse() + } finally { + await cleanup() + } + }, +}) + // Test creating an issue with milestone await snapshotTest({ name: "Issue Create Command - With Milestone", diff --git a/test/commands/issue/issue-list.test.ts b/test/commands/issue/issue-list.test.ts index 277bae42..9f5c0cd5 100644 --- a/test/commands/issue/issue-list.test.ts +++ b/test/commands/issue/issue-list.test.ts @@ -1,6 +1,9 @@ import { snapshotTest } from "@cliffy/testing" import { listCommand } from "../../../src/commands/issue/issue-list.ts" -import { commonDenoArgs } from "../../utils/test-helpers.ts" +import { + commonDenoArgs, + setupMockLinearServer, +} from "../../utils/test-helpers.ts" // Test help output await snapshotTest({ @@ -13,3 +16,84 @@ await snapshotTest({ await listCommand.parse() }, }) + +// Test --json output +await snapshotTest({ + name: "Issue List Command - JSON Output", + meta: import.meta, + colors: false, + args: ["--json", "--team", "ENG", "--sort", "priority"], + denoArgs: commonDenoArgs, + async fn() { + const { cleanup } = await setupMockLinearServer([ + { + queryName: "GetTeamIdByKey", + variables: { team: "ENG" }, + response: { + data: { + teams: { + nodes: [{ id: "team-eng-id" }], + }, + }, + }, + }, + { + queryName: "GetIssuesForState", + response: { + data: { + issues: { + nodes: [ + { + id: "issue-1", + identifier: "ENG-101", + title: "Fix login bug", + priority: 1, + estimate: 3, + assignee: { initials: "JD" }, + state: { + id: "state-1", + name: "In Progress", + color: "#f2c94c", + }, + labels: { + nodes: [ + { id: "label-1", name: "bug", color: "#eb5757" }, + ], + }, + updatedAt: "2026-03-10T12:00:00.000Z", + }, + { + id: "issue-2", + identifier: "ENG-102", + title: "Add dark mode", + priority: 3, + estimate: null, + assignee: null, + state: { + id: "state-2", + name: "Todo", + color: "#bdbdbd", + }, + labels: { + nodes: [], + }, + updatedAt: "2026-03-09T08:30:00.000Z", + }, + ], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + }, + }, + }, + }, + ], { LINEAR_TEAM_ID: "ENG" }) + + try { + await listCommand.parse() + } finally { + await cleanup() + } + }, +})