mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix: keep identifier casing locale-independent (Turkish) Under a tr/az default locale, toLocaleLowerCase()/toLocaleUpperCase() map I to dotless ı and İ, and default-locale collation stops treating I/i as base-equal. Switch identifier matching (Linear CLI selectors, priorities, error classification, plugin/Jira/worktree/address/slash-command search) to toLowerCase()/toUpperCase(), and pin collation to 'en' where ordering feeds hashes, canonical JSON, semver precedence, remote selection, SSH Include order, or WSL distro equality. User-visible prose casing and display-list sorting stay locale-aware. * fix: order SSH Include globs bytewise and fold dashboard search ids locale-independently --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
245 lines
7.4 KiB
TypeScript
245 lines
7.4 KiB
TypeScript
import { readCommandBody } from './command-body-input'
|
|
import type {
|
|
LinearIssueInclude,
|
|
LinearIssueListRequest,
|
|
LinearIssueRequest,
|
|
LinearIssueTaskUpdateRequest,
|
|
LinearWriteTargetRequest
|
|
} from '../shared/linear/agent-access'
|
|
import {
|
|
LINEAR_CHILDREN_MAX_DEPTH,
|
|
LINEAR_WRITE_BODY_CAP,
|
|
clampLinearIssueDepth
|
|
} from '../shared/linear/agent-access'
|
|
import { isLinearUuid } from '../shared/linear/uuid'
|
|
import {
|
|
getOptionalNonNegativeIntegerFlag,
|
|
getOptionalStringFlag,
|
|
getRepeatedStringFlag,
|
|
getRequiredStringFlag
|
|
} from './flags'
|
|
import { RuntimeClientError } from './runtime-client'
|
|
|
|
const LINEAR_PRIORITY_VALUES = new Map([
|
|
['none', 0],
|
|
['urgent', 1],
|
|
['high', 2],
|
|
['medium', 3],
|
|
['low', 4]
|
|
])
|
|
|
|
export function buildAssigneeSetRequest(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
remote: boolean
|
|
): LinearIssueTaskUpdateRequest {
|
|
const me = flags.get('me') === true
|
|
const toId = getOptionalStringFlag(flags, 'to-id')
|
|
if (me === Boolean(toId)) {
|
|
throw new RuntimeClientError('invalid_argument', 'Pass exactly one of --me or --to-id')
|
|
}
|
|
return {
|
|
...buildWriteTargetRequest(flags, cwd, remote),
|
|
operation: 'assignee',
|
|
...(me ? { assigneeMe: true } : { assigneeId: toId })
|
|
}
|
|
}
|
|
|
|
export function getLinearListFilter(
|
|
flags: Map<string, string | boolean>
|
|
): LinearIssueListRequest['filter'] {
|
|
const filter = getOptionalStringFlag(flags, 'filter') ?? 'assigned'
|
|
if (['assigned', 'created', 'all', 'completed', 'open'].includes(filter)) {
|
|
return filter as LinearIssueListRequest['filter']
|
|
}
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
'--filter must be assigned, created, all, completed, or open'
|
|
)
|
|
}
|
|
|
|
export function getPriorityFlag(flags: Map<string, string | boolean>, name: string): number {
|
|
const value = getRequiredStringFlag(flags, name).toLowerCase()
|
|
const priority = LINEAR_PRIORITY_VALUES.get(value)
|
|
if (priority === undefined) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
`--${name} must be none, low, medium, high, or urgent`
|
|
)
|
|
}
|
|
return priority
|
|
}
|
|
|
|
export function getRequiredNonNegativeIntegerFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number {
|
|
const raw = getRequiredStringFlag(flags, name)
|
|
const value = Number(raw)
|
|
if (!Number.isInteger(value) || value < 0) {
|
|
throw new RuntimeClientError('invalid_argument', `--${name} must be a non-negative integer`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getDueDateFlag(flags: Map<string, string | boolean>, name: string): string {
|
|
const value = getRequiredStringFlag(flags, name)
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
throw new RuntimeClientError('invalid_argument', `--${name} must use YYYY-MM-DD`)
|
|
}
|
|
const [year, month, day] = value.split('-').map(Number)
|
|
const date = new Date(Date.UTC(year, month - 1, day))
|
|
if (
|
|
date.getUTCFullYear() !== year ||
|
|
date.getUTCMonth() !== month - 1 ||
|
|
date.getUTCDate() !== day
|
|
) {
|
|
throw new RuntimeClientError('invalid_argument', `--${name} must be a real calendar date`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getRequiredRepeatedStringFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): string[] {
|
|
const values = getRepeatedStringFlag(flags, name)
|
|
if (values.length === 0) {
|
|
throw new RuntimeClientError('invalid_argument', `Missing required --${name}`)
|
|
}
|
|
return values
|
|
}
|
|
|
|
export function buildIssueRequest(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
remote: boolean
|
|
): LinearIssueRequest {
|
|
const full = flags.get('full') === true
|
|
const includes: Record<LinearIssueInclude, boolean> = {
|
|
comments: full || flags.get('comments') === true,
|
|
children: full || flags.get('children') === true,
|
|
attachments: full || flags.get('attachments') === true,
|
|
relations: full || flags.get('relations') === true,
|
|
activity: full || flags.get('activity') === true
|
|
}
|
|
if (flags.has('depth') && !includes.children) {
|
|
throw new RuntimeClientError('invalid_argument', '--depth requires --children or --full')
|
|
}
|
|
const requestedDepth = getOptionalNonNegativeIntegerFlag(flags, 'depth')
|
|
if (requestedDepth !== undefined && requestedDepth > LINEAR_CHILDREN_MAX_DEPTH) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
`--depth must be at most ${LINEAR_CHILDREN_MAX_DEPTH}`
|
|
)
|
|
}
|
|
const workspaceId = getOptionalStringFlag(flags, 'workspace')
|
|
if (workspaceId === 'all') {
|
|
throw new RuntimeClientError(
|
|
'linear_invalid_workspace',
|
|
'--workspace all is not valid for issue'
|
|
)
|
|
}
|
|
const input = getOptionalStringFlag(flags, 'id')
|
|
return {
|
|
input,
|
|
current: input ? false : flags.get('current') === true,
|
|
workspaceId,
|
|
include: includes,
|
|
depth: clampLinearIssueDepth(requestedDepth),
|
|
context: buildLinearCurrentContext(cwd, remote)
|
|
}
|
|
}
|
|
|
|
export function buildWriteTargetRequest(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
remote: boolean
|
|
): LinearWriteTargetRequest {
|
|
rejectAllWorkspaceForWrite(flags)
|
|
const input = getOptionalStringFlag(flags, 'id')
|
|
const current = flags.get('current') === true
|
|
if (input && current) {
|
|
throw new RuntimeClientError('invalid_argument', 'Pass either <id> or --current, not both')
|
|
}
|
|
if (!input && !current) {
|
|
throw new RuntimeClientError('linear_issue_required', 'Pass a Linear issue id or --current')
|
|
}
|
|
return {
|
|
input,
|
|
current,
|
|
workspaceId: getOptionalStringFlag(flags, 'workspace'),
|
|
context: buildLinearCurrentContext(cwd, remote)
|
|
}
|
|
}
|
|
|
|
export function buildLinearCurrentContext(
|
|
cwd: string,
|
|
remote: boolean
|
|
): LinearIssueRequest['context'] {
|
|
return {
|
|
remote,
|
|
...(remote ? {} : { cwd }),
|
|
...(process.env.ORCA_WORKTREE_ID ? { worktreeId: process.env.ORCA_WORKTREE_ID } : {}),
|
|
...(process.env.ORCA_TERMINAL_HANDLE
|
|
? { terminalHandle: process.env.ORCA_TERMINAL_HANDLE }
|
|
: {})
|
|
}
|
|
}
|
|
|
|
export function rejectAllWorkspaceForWrite(flags: Map<string, string | boolean>): void {
|
|
if (getOptionalStringFlag(flags, 'workspace') === 'all') {
|
|
throw new RuntimeClientError(
|
|
'linear_invalid_workspace',
|
|
'--workspace all is not valid for Linear writes'
|
|
)
|
|
}
|
|
}
|
|
|
|
export function getOptionalWriteId(flags: Map<string, string | boolean>): string | undefined {
|
|
if (!flags.has('write-id')) {
|
|
return undefined
|
|
}
|
|
const writeId = getRequiredStringFlag(flags, 'write-id')
|
|
if (!isLinearUuid(writeId)) {
|
|
throw new RuntimeClientError('linear_invalid_write_id', '--write-id must be a UUID')
|
|
}
|
|
return writeId
|
|
}
|
|
|
|
export function getHttpUrlFlag(flags: Map<string, string | boolean>, name: string): string {
|
|
const value = getRequiredStringFlag(flags, name)
|
|
try {
|
|
const parsed = new URL(value)
|
|
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
return value
|
|
}
|
|
} catch {
|
|
// Fall through to the stable Linear error below.
|
|
}
|
|
throw new RuntimeClientError('linear_invalid_url', '--url must be an absolute http(s) URL')
|
|
}
|
|
|
|
export function readLinearBody(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
options: { required: true }
|
|
): Promise<string>
|
|
export function readLinearBody(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
options: { required: false }
|
|
): Promise<string | undefined>
|
|
export async function readLinearBody(
|
|
flags: Map<string, string | boolean>,
|
|
cwd: string,
|
|
options: { required: boolean }
|
|
): Promise<string | undefined> {
|
|
return readCommandBody(flags, cwd, {
|
|
...options,
|
|
maximumCharacters: LINEAR_WRITE_BODY_CAP,
|
|
tooLargeCode: 'linear_body_too_large',
|
|
label: 'Linear body'
|
|
})
|
|
}
|