Skip to content
Open
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
3 changes: 2 additions & 1 deletion skills/linear-cli/references/cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Options:

-h, --help - Show this help.
-w, --workspace <slug> - Target workspace (uses credentials)
--team <team> - Team key (defaults to current team)
--team <team> - Team key (defaults to current team)
-j, --json - Output cycle data as JSON
```

### view
Expand Down
4 changes: 3 additions & 1 deletion skills/linear-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ Options:
--limit <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
Expand Down Expand Up @@ -272,6 +273,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
```
Expand Down
27 changes: 25 additions & 2 deletions src/commands/cycle/cycle-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()

Expand All @@ -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
Expand All @@ -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 }
Expand Down
17 changes: 16 additions & 1 deletion src/commands/issue/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -529,6 +530,7 @@ export const createCommand = new Command()
milestone,
cycle,
interactive,
json,
title,
},
) => {
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 26 additions & 1 deletion src/commands/issue/issue-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
{
Expand All @@ -117,6 +118,7 @@ export const listCommand = new Command()
milestone,
limit,
pager,
json,
},
) => {
const usePager = pager !== false
Expand Down Expand Up @@ -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()

Expand All @@ -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 }
Expand Down
46 changes: 46 additions & 0 deletions test/commands/cycle/__snapshots__/cycle-list.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
82 changes: 81 additions & 1 deletion test/commands/cycle/cycle-list.test.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions test/commands/issue/__snapshots__/issue-create.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
Loading
Loading