export type JsonRecord = Record<string, unknown>
export type LinearAuthMode = 'oauth' | 'api-key'
export type LinearAuthInput = {
/** Saved OAuth integration name. Defaults to `linear`. */
integrationName?: string
/** Alias of `integrationName` (Notion-style). */
integration?: string
/**
* Short account alias. `work` → `linear-work`. Values that already start
* with `linear-` are used as-is. `default` / `linear` / omitted → `linear`.
*/
account?: string
/** User secret name for a personal API key. Defaults to `linearApiKey`. */
secretName?: string
/** Force OAuth or API-key auth when both exist. */
auth?: LinearAuthMode
}
export type LinearPageInfo = {
hasNextPage: boolean
endCursor: string | null
}
export type LinearNamedRef = {
id: string
name: string
key?: string
type?: string
}
export type LinearIssueSummary = {
id: string
identifier: string
title: string
url: string | null
priority: number | null
createdAt: string | null
updatedAt: string | null
state: LinearNamedRef | null
team: LinearNamedRef | null
assignee: LinearNamedRef | null
project: LinearNamedRef | null
}
export type LinearCommentSummary = {
id: string
body: string | null
url: string | null
createdAt: string | null
updatedAt: string | null
user: LinearNamedRef | null
}
export type LinearProjectSummary = {
id: string
name: string
url: string | null
description: string | null
state: string | null
startDate: string | null
targetDate: string | null
progress: number | null
}
export type LinearTeamSummary = {
id: string
name: string
key: string
description: string | null
}
export type LinearWorkflowStateSummary = {
id: string
name: string
type: string
team: LinearNamedRef | null
}
export type DryRunResult<T> = {
dryRun: true
wouldCall: T
}
export function requireRecord(value: unknown, label: string): JsonRecord {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be an object.`)
}
return value as JsonRecord
}
export function requireString(
value: unknown,
label: string,
options: { allowEmpty?: boolean } = {},
): string {
if (typeof value !== 'string' || (!options.allowEmpty && value.trim().length === 0)) {
throw new Error(
`${label} must be ${options.allowEmpty ? 'a string' : 'a non-empty string'}.`,
)
}
return options.allowEmpty ? value : value.trim()
}
export function optionalString(value: unknown, label: string): string | undefined {
if (value === undefined || value === null) return undefined
return requireString(value, label)
}
export function optionalBoolean(value: unknown, label: string): boolean | undefined {
if (value === undefined) return undefined
if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`)
return value
}
export function optionalNumber(value: unknown, label: string): number | undefined {
if (value === undefined || value === null) return undefined
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new Error(`${label} must be a finite number.`)
}
return value
}
export function optionalStringArray(value: unknown, label: string): Array<string> | undefined {
if (value === undefined || value === null) return undefined
if (!Array.isArray(value)) throw new Error(`${label} must be an array of strings.`)
return value.map((item, index) => requireString(item, `${label}[${index}]`))
}
export function clampInt(
value: unknown,
min: number,
max: number,
fallback: number,
label = 'first',
): number {
if (value === undefined || value === null) return fallback
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new Error(`${label} must be an integer.`)
}
if (value < min || value > max) {
throw new Error(`${label} must be between ${min} and ${max}.`)
}
return value
}
export function requirePriority(value: unknown, label = 'priority'): number {
const priority = optionalNumber(value, label)
if (priority === undefined) {
throw new Error(`${label} is required.`)
}
if (![0, 1, 2, 3, 4].includes(priority)) {
throw new Error(`${label} must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low).`)
}
return priority
}
export function optionalPriority(value: unknown, label = 'priority'): number | undefined {
if (value === undefined || value === null) return undefined
return requirePriority(value, label)
}
export const WORKFLOW_STATE_TYPES = [
'triage',
'backlog',
'unstarted',
'started',
'completed',
'canceled',
] as const
export type WorkflowStateType = (typeof WORKFLOW_STATE_TYPES)[number]
export function optionalWorkflowStateType(
value: unknown,
label = 'stateType',
): WorkflowStateType | undefined {
if (value === undefined || value === null) return undefined
const type = requireString(value, label)
if (!(WORKFLOW_STATE_TYPES as readonly string[]).includes(type)) {
throw new Error(
`${label} must be one of: ${WORKFLOW_STATE_TYPES.join(', ')}.`,
)
}
return type as WorkflowStateType
}
export function compactRecord<T extends JsonRecord>(value: T): T {
const output: JsonRecord = {}
for (const [key, item] of Object.entries(value)) {
if (item === undefined) continue
output[key] = item
}
return output as T
}
/**
* Shared Linear types for this package.
* @example
* import types from 'kody:@kody/linear/types'
* const catalog = await types()
*/
export default async function typesOverview() {
return {
auth: ['LinearAuthInput', 'LinearAuthMode'],
models: [
'LinearIssueSummary',
'LinearProjectSummary',
'LinearTeamSummary',
'LinearCommentSummary',
'LinearWorkflowStateSummary',
],
}
}