Skip to content
Closed
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
54 changes: 54 additions & 0 deletions src/commands/issue/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Command } from "@cliffy/command"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import { getIssueIdentifier } from "../../utils/linear.ts"

export const commentCommand = new Command()
.description("Add a comment to an issue.")
.option("-i, --issue <id:string>", "The ID of the issue to comment on (e.g., PROJ-123).")
.arguments("<comment:string>")
.action(async (options, comment) => {
const client = getGraphQLClient()
let issueId: string | undefined = options.issue

// If no issue ID was provided via the flag, try to get it from the branch
if (!issueId) {
issueId = await getIssueIdentifier()
}

// If we still don't have an ID, exit with an error
if (!issueId) {
console.error(
"Error: Could not determine issue ID. Please provide it using the --issue flag or run this command from a branch with the ID in its name (e.g., feature/PROJ-123-my-feature)."
)
Deno.exit(1)
}

const mutation = gql(/* GraphQL */ `
mutation CommentCreate($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
success
comment {
id
}
}
}
`)

try {
const result = await client.request(mutation, {
issueId,
body: comment,
})

if (result.commentCreate.success) {
console.log(`Comment added successfully to issue ${issueId}.`)
} else {
console.error("Failed to add comment.")
Deno.exit(1)
}
} catch (error) {
console.error("Failed to add comment:", error)
Deno.exit(1)
}
})
104 changes: 104 additions & 0 deletions src/commands/issue/issue-accept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Command } from "@cliffy/command"
import { Input } from "@cliffy/prompt"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import {
getDefaultIssueState,
getIssueIdentifier,
getIssueTeamKey,
} from "../../utils/linear.ts"

export const acceptCommand = new Command()
.name("accept")
.description("Accept an issue from triage")
.arguments("<issueId:string>")
.option(
"-c, --comment <comment:string>",
"Add a comment when accepting the issue",
)
.action(async ({ comment }, issueIdArg) => {
try {
// Get the issue ID
const issueId = await getIssueIdentifier(issueIdArg)
if (!issueId) {
console.error(
"Could not determine issue ID. Please provide an issue ID like 'ENG-123'",
)
Deno.exit(1)
}

// Get the team key from the issue
const teamKey = await getIssueTeamKey(issueId)
if (!teamKey) {
console.error(`Could not determine team for issue ${issueId}`)
Deno.exit(1)
}

// Get the team's default issue state
const defaultState = await getDefaultIssueState(teamKey)

// Prompt for comment if not provided and user wants to add one
let finalComment = comment
if (!finalComment) {
const addComment = await Input.prompt({
message: "Add a comment? (leave blank to skip)",
default: "",
})
if (addComment.trim()) {
finalComment = addComment.trim()
}
}

// Update the issue state
const updateMutation = gql(`
mutation AcceptIssue($issueId: String!, $stateId: String!) {
issueUpdate(id: $issueId, input: { stateId: $stateId }) {
success
issue {
id
identifier
title
}
}
}
`)

const client = getGraphQLClient()
const result = await client.request(updateMutation, {
issueId,
stateId: defaultState.id,
})

if (!result.issueUpdate.success) {
throw new Error("Failed to accept issue")
}

// Add comment if provided
if (finalComment) {
const commentMutation = gql(`
mutation AddComment($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
success
}
}
`)

await client.request(commentMutation, {
issueId,
body: finalComment,
})
}

const issue = result.issueUpdate.issue
console.log(
`✓ Accepted issue ${issue?.identifier}: ${issue?.title}`,
)
console.log(` Moved to state: ${defaultState.name}`)
if (finalComment) {
console.log(` Comment added`)
}
} catch (error) {
console.error("✗ Failed to accept issue:", error)
Deno.exit(1)
}
})
41 changes: 41 additions & 0 deletions src/commands/issue/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
getIssueLabelOptionsByNameForTeam,
getLabelsForTeam,
getProjectIdByName,
getProjectMilestoneIdByNameForProject,
getProjectMilestoneOptionsByNameForProject,
getProjectOptionsByName,
getTeamIdByKey,
getTeamKey,
Expand Down Expand Up @@ -480,6 +482,10 @@ export const createCommand = new Command()
"--project <project:string>",
"Name of the project with the issue",
)
.option(
"--milestone <milestone:string>",
"Name of the project milestone for the issue",
)
.option(
"-s, --state <state:string>",
"Workflow state for the issue (by name or type)",
Expand All @@ -505,6 +511,7 @@ export const createCommand = new Command()
label: labels,
team,
project,
milestone,
state,
color,
interactive,
Expand Down Expand Up @@ -715,6 +722,39 @@ export const createCommand = new Command()
}
}

let projectMilestoneId: string | undefined = undefined
if (milestone !== undefined) {
if (projectId === undefined) {
console.error(
"Cannot set milestone without a project. Use --project to specify the project.",
)
Deno.exit(1)
}
projectMilestoneId = await getProjectMilestoneIdByNameForProject(
milestone,
projectId,
)
if (projectMilestoneId === undefined && interactive) {
const milestoneIds = await getProjectMilestoneOptionsByNameForProject(
milestone,
projectId,
)
spinner?.stop()
projectMilestoneId = await selectOption(
"Project milestone",
milestone,
milestoneIds,
)
spinner?.start()
}
if (projectMilestoneId === undefined) {
console.error(
`Could not determine ID for project milestone ${milestone}`,
)
Deno.exit(1)
}
}

// Date validation done at graphql level

// Convert parent identifier if provided and fetch parent data
Expand Down Expand Up @@ -756,6 +796,7 @@ export const createCommand = new Command()
labelIds,
teamId: teamId,
projectId: projectId || parentData?.projectId,
projectMilestoneId,
stateId,
useDefaultTemplate,
description,
Expand Down
104 changes: 104 additions & 0 deletions src/commands/issue/issue-decline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Command } from "@cliffy/command"
import { Input } from "@cliffy/prompt"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import {
getCanceledState,
getIssueIdentifier,
getIssueTeamKey,
} from "../../utils/linear.ts"

export const declineCommand = new Command()
.name("decline")
.description("Decline an issue from triage")
.arguments("<issueId:string>")
.option(
"-c, --comment <comment:string>",
"Add a comment explaining why the issue is declined",
)
.action(async ({ comment }, issueIdArg) => {
try {
// Get the issue ID
const issueId = await getIssueIdentifier(issueIdArg)
if (!issueId) {
console.error(
"Could not determine issue ID. Please provide an issue ID like 'ENG-123'",
)
Deno.exit(1)
}

// Get the team key from the issue
const teamKey = await getIssueTeamKey(issueId)
if (!teamKey) {
console.error(`Could not determine team for issue ${issueId}`)
Deno.exit(1)
}

// Get the team's canceled state
const canceledState = await getCanceledState(teamKey)

// Prompt for comment if not provided
let finalComment = comment
if (!finalComment) {
const addComment = await Input.prompt({
message: "Add a comment explaining why? (leave blank to skip)",
default: "",
})
if (addComment.trim()) {
finalComment = addComment.trim()
}
}

// Update the issue state to canceled
const updateMutation = gql(`
mutation DeclineIssue($issueId: String!, $stateId: String!) {
issueUpdate(id: $issueId, input: { stateId: $stateId }) {
success
issue {
id
identifier
title
}
}
}
`)

const client = getGraphQLClient()
const result = await client.request(updateMutation, {
issueId,
stateId: canceledState.id,
})

if (!result.issueUpdate.success) {
throw new Error("Failed to decline issue")
}

// Add comment if provided
if (finalComment) {
const commentMutation = gql(`
mutation AddComment($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
success
}
}
`)

await client.request(commentMutation, {
issueId,
body: finalComment,
})
}

const issue = result.issueUpdate.issue
console.log(
`✓ Declined issue ${issue?.identifier}: ${issue?.title}`,
)
console.log(` Moved to state: ${canceledState.name}`)
if (finalComment) {
console.log(` Comment added`)
}
} catch (error) {
console.error("✗ Failed to decline issue:", error)
Deno.exit(1)
}
})
Loading