mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
Linear-provided titles, states, assignees, projects, labels, workspace names and error messages were printed verbatim, so remote text could emit OSC/CSI/C1 sequences or forge extra output lines. Move terminalSafe into src/shared/terminal-safe-text.ts, add terminalSafeLine (collapses CR/LF), and apply it to every Linear text field in the human-readable formatters. --json output is unchanged. Fixes #20478 Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
281 lines
9.8 KiB
TypeScript
281 lines
9.8 KiB
TypeScript
import type {
|
|
LinearAttachResult,
|
|
LinearCommentAddResult,
|
|
LinearCommentDeleteResult,
|
|
LinearCommentEditResult,
|
|
LinearCreateResult,
|
|
LinearIssueListResult,
|
|
LinearMcpIssueListResult,
|
|
LinearIssueContextResult,
|
|
LinearIssueTaskUpdateResult,
|
|
LinearIssueRelationWriteResult,
|
|
LinearSaveIssueResult,
|
|
LinearProjectListResult,
|
|
LinearSearchIssueSummary,
|
|
LinearSearchResult,
|
|
LinearTeamLabelsResult,
|
|
LinearTeamListResult,
|
|
LinearTeamMembersResult,
|
|
LinearTeamStatesResult,
|
|
LinearStatusSetResult
|
|
} from '../shared/linear/agent-access'
|
|
import { appendLinearListTruncation } from '../shared/linear/list-truncation-format'
|
|
import { linearPriorityLabel } from '../shared/linear/priority-label'
|
|
import {
|
|
formatLinearProjectListRows,
|
|
linearProjectListWarningLines
|
|
} from '../shared/linear/project-list-format'
|
|
import { terminalSafeLine as safe } from '../shared/terminal-safe-text'
|
|
|
|
export function formatLinearIssue(result: LinearIssueContextResult): string {
|
|
const issue = result.issue
|
|
const lines = [
|
|
`${safe(issue.identifier)} ${safe(issue.title)}`,
|
|
`URL: ${safe(issue.url)}`,
|
|
`State: ${safe(issue.state?.name ?? 'unknown')}`,
|
|
`Assignee: ${safe(issue.assignee?.displayName ?? 'unassigned')}`,
|
|
`Project: ${safe(issue.project?.name ?? 'none')}`
|
|
]
|
|
lines.push(`Priority: ${formatPriority(issue.priority)}`)
|
|
lines.push(`Estimate: ${issue.estimate ?? 'none'}`)
|
|
if (issue.labels.length > 0) {
|
|
lines.push(
|
|
`Labels: ${issue.labels
|
|
.map((label) => safe(label.name ?? ''))
|
|
.filter(Boolean)
|
|
.join(', ')}`
|
|
)
|
|
}
|
|
if (issue.dueDate) {
|
|
lines.push(`Due: ${safe(issue.dueDate)}`)
|
|
}
|
|
const sections = result.meta.sections
|
|
if (sections.comments) {
|
|
lines.push(`Comments: ${sections.comments.returned}`)
|
|
}
|
|
if (sections.children) {
|
|
lines.push(`Children: ${sections.children.returned}`)
|
|
}
|
|
if (sections.attachments) {
|
|
lines.push(`Attachments: ${sections.attachments.returned}`)
|
|
}
|
|
if (sections.relations) {
|
|
lines.push(`Relations: ${sections.relations.returned}`)
|
|
}
|
|
if (sections.activity) {
|
|
lines.push(`Activity: ${sections.activity.returned}`)
|
|
}
|
|
if (result.inlineMedia?.length) {
|
|
lines.push(`Inline media: ${result.inlineMedia.length} (use --json for URLs)`)
|
|
}
|
|
return lines.join('\n')
|
|
}
|
|
|
|
export function formatLinearSearch(result: LinearSearchResult): string {
|
|
if (result.issues.length === 0) {
|
|
return 'No Linear issues found.'
|
|
}
|
|
return appendLinearListTruncation(
|
|
result.issues.map(formatSearchRow).join('\n'),
|
|
result.issues.length,
|
|
result.truncated ?? result.meta.limitReached
|
|
)
|
|
}
|
|
|
|
export function formatLinearTeamList(result: LinearTeamListResult): string {
|
|
if (result.teams.length === 0) {
|
|
return 'No Linear teams found.'
|
|
}
|
|
return result.teams
|
|
.map((team) => {
|
|
const workspace = team.workspace ? ` ${safe(team.workspace.name)}` : ''
|
|
return `${safe(team.key).padEnd(10)} ${safe(team.name)}${workspace}`
|
|
})
|
|
.join('\n')
|
|
}
|
|
|
|
export function formatLinearTeamMembers(result: LinearTeamMembersResult): string {
|
|
if (result.members.length === 0) {
|
|
return `No Linear members found for ${safe(result.team.key)}.`
|
|
}
|
|
return result.members
|
|
.map((member) => `${safe(member.displayName ?? 'unknown').padEnd(24)} ${safe(member.id ?? '')}`)
|
|
.join('\n')
|
|
}
|
|
|
|
export function formatLinearTeamStates(result: LinearTeamStatesResult): string {
|
|
if (result.states.length === 0) {
|
|
return `No Linear workflow states found for ${safe(result.team.key)}.`
|
|
}
|
|
return result.states
|
|
.map(
|
|
(state) =>
|
|
`${safe(state.name).padEnd(24)} ${safe(state.type ?? '').padEnd(12)} ${safe(state.id)}`
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
export function formatLinearTeamLabels(result: LinearTeamLabelsResult): string {
|
|
if (result.labels.length === 0) {
|
|
return `No Linear labels found for ${safe(result.team.key)}.`
|
|
}
|
|
return result.labels.map((label) => `${safe(label.name).padEnd(24)} ${safe(label.id)}`).join('\n')
|
|
}
|
|
|
|
export function formatLinearIssueList(result: LinearIssueListResult): string {
|
|
if (result.issues.length === 0) {
|
|
return 'No Linear issues found.'
|
|
}
|
|
return appendLinearListTruncation(
|
|
result.issues.map(formatSearchRow).join('\n'),
|
|
result.issues.length,
|
|
result.truncated ?? result.meta.hasMore
|
|
)
|
|
}
|
|
|
|
export function formatLinearMcpIssueList(result: LinearMcpIssueListResult): string {
|
|
if (result.issues.length === 0) {
|
|
return 'No Linear issues found.'
|
|
}
|
|
return appendLinearListTruncation(
|
|
result.issues.map(formatSearchRow).join('\n'),
|
|
result.issues.length,
|
|
result.truncated ?? result.meta.hasMore
|
|
)
|
|
}
|
|
|
|
export function printLinearMcpIssueListWarnings(result: LinearMcpIssueListResult): void {
|
|
if (result.meta.hasMore) {
|
|
const workspaceHint =
|
|
result.meta.nextCursor && result.meta.workspaceId !== 'all' && result.meta.workspaceId
|
|
? `; continue with --workspace ${safe(result.meta.workspaceId)}`
|
|
: ''
|
|
console.error(
|
|
`warning: more results available; next cursor: ${safe(result.meta.nextCursor ?? 'n/a')}${workspaceHint}`
|
|
)
|
|
}
|
|
for (const error of result.meta.workspaceErrors) {
|
|
console.error(
|
|
`warning: ${safe(error.workspace.name)} unavailable for Linear: ${safe(error.message)}`
|
|
)
|
|
}
|
|
}
|
|
|
|
export function formatLinearProjectList(result: LinearProjectListResult): string {
|
|
return formatLinearProjectListRows(result)
|
|
}
|
|
|
|
export function formatLinearStatusSet(result: LinearStatusSetResult): string {
|
|
const suffix = result.meta.alreadyInState ? ' (already set)' : ''
|
|
return `Set ${safe(result.issue.identifier)} to ${safe(result.state.name)}${suffix}.`
|
|
}
|
|
|
|
export function formatLinearCommentAdd(result: LinearCommentAddResult): string {
|
|
const suffix = result.meta.deduplicated ? ' (already posted)' : ''
|
|
return `Added comment ${safe(result.comment.id)} to ${safe(result.issue.identifier)}${suffix}.`
|
|
}
|
|
|
|
export function formatLinearCommentEdit(result: LinearCommentEditResult): string {
|
|
return `Edited comment ${safe(result.comment.id)} on ${safe(result.issue.identifier)}.`
|
|
}
|
|
|
|
export function formatLinearCommentDelete(result: LinearCommentDeleteResult): string {
|
|
return `Deleted comment ${safe(result.comment.id)} from ${safe(result.issue.identifier)}.`
|
|
}
|
|
|
|
export function formatLinearAttach(result: LinearAttachResult): string {
|
|
const suffix = result.meta.deduplicated ? ' (already attached)' : ''
|
|
return `Attached ${safe(result.attachment.title)} to ${safe(result.issue.identifier)}${suffix}.`
|
|
}
|
|
|
|
export function formatLinearCreate(result: LinearCreateResult | LinearSaveIssueResult): string {
|
|
const parent = result.issue.parent ? ` under ${safe(result.issue.parent.identifier)}` : ''
|
|
const project = result.issue.project?.name ? ` in ${safe(result.issue.project.name)}` : ''
|
|
const suffix = result.meta.deduplicated ? ' (already created)' : ''
|
|
return `Created ${safe(result.issue.identifier)}${parent}${project}: ${safe(result.issue.title)}${suffix}.`
|
|
}
|
|
|
|
export function formatLinearSaveIssue(result: LinearSaveIssueResult): string {
|
|
if (result.meta.created) {
|
|
return formatLinearCreate(result)
|
|
}
|
|
return `Saved ${safe(result.issue.identifier)}: ${safe(result.issue.title)}.`
|
|
}
|
|
|
|
export function formatLinearTaskUpdate(result: LinearIssueTaskUpdateResult): string {
|
|
const suffix = result.meta.alreadySet ? ' (already set)' : ''
|
|
return `Updated ${safe(result.issue.identifier)} ${taskOperationLabel(result.operation)}${suffix}.`
|
|
}
|
|
|
|
export function formatLinearRelationWrite(result: LinearIssueRelationWriteResult): string {
|
|
const verb = result.operation === 'add' ? 'Added' : 'Removed'
|
|
const suffix = result.meta.alreadySet
|
|
? result.operation === 'add'
|
|
? ' (already present)'
|
|
: ' (already absent)'
|
|
: ''
|
|
return `${verb} ${safe(result.issue.identifier)} ${result.relation.relationship} ${safe(result.relatedIssue.identifier)}${suffix}.`
|
|
}
|
|
|
|
export function printLinearIssueWarnings(result: LinearIssueContextResult): void {
|
|
for (const error of result.meta.includeErrors) {
|
|
console.error(`warning: ${error.include} unavailable: ${safe(error.message)}`)
|
|
}
|
|
for (const [name, meta] of Object.entries(result.meta.sections)) {
|
|
if (meta?.capReached) {
|
|
console.error(`warning: ${name} capped at ${meta.returned}/${meta.cap}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function printLinearSearchWarnings(result: LinearSearchResult): void {
|
|
if (result.meta.limitReached) {
|
|
console.error(`warning: showing first ${result.meta.returned} Linear issues`)
|
|
}
|
|
for (const error of result.meta.workspaceErrors ?? []) {
|
|
console.error(
|
|
`warning: ${safe(error.workspace.name)} unavailable for Linear search: ${safe(error.message)}`
|
|
)
|
|
}
|
|
}
|
|
|
|
export function printLinearListWarnings(
|
|
result: LinearSearchResult | LinearIssueListResult | LinearTeamListResult
|
|
): void {
|
|
const meta = result.meta
|
|
if ('hasMore' in meta && meta.hasMore) {
|
|
console.error(`warning: showing first ${meta.returned} Linear issues`)
|
|
}
|
|
if ('limitReached' in meta && meta.limitReached) {
|
|
console.error(`warning: showing first ${meta.returned} Linear issues`)
|
|
}
|
|
for (const error of meta.workspaceErrors ?? []) {
|
|
console.error(
|
|
`warning: ${safe(error.workspace.name)} unavailable for Linear: ${safe(error.message)}`
|
|
)
|
|
}
|
|
}
|
|
|
|
export function printLinearProjectListWarnings(result: LinearProjectListResult): void {
|
|
for (const warning of linearProjectListWarningLines(result)) {
|
|
console.error(warning)
|
|
}
|
|
}
|
|
|
|
function formatSearchRow(issue: LinearSearchIssueSummary): string {
|
|
const state = safe(issue.state?.name ?? 'unknown')
|
|
const assignee = safe(issue.assignee?.displayName ?? 'unassigned')
|
|
return `${safe(issue.identifier).padEnd(10)} ${state.padEnd(14)} ${assignee.padEnd(18)} ${safe(issue.title)}`
|
|
}
|
|
|
|
function formatPriority(priority: number | null | undefined): string {
|
|
return linearPriorityLabel(priority)
|
|
}
|
|
|
|
function taskOperationLabel(operation: LinearIssueTaskUpdateResult['operation']): string {
|
|
if (operation === 'dueDate') {
|
|
return 'due date'
|
|
}
|
|
return operation
|
|
}
|