mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
/**
|
|
* Human-readable automation output for the CLI.
|
|
*
|
|
* The host lines come from the authority's own projection, never from the
|
|
* stored `executionTarget*` fields: a target that was removed — or removed and
|
|
* re-registered under the same id — leaves those fields untouched, so printing
|
|
* them alone would render a dead host identically to a healthy one and leave
|
|
* `orca automations show` unable to complete the recovery it is named in.
|
|
*/
|
|
|
|
import type { Automation, AutomationRun } from '../shared/automations-types'
|
|
import type { AutomationListItem } from '../shared/automation-list-scope'
|
|
import type { AutomationOwnerPrecondition } from '../shared/automation-owner-precondition'
|
|
import { getAutomationLegacyRepoId } from '../shared/automation-run-identity'
|
|
import { formatAutomationPrecheckTimeout } from '../shared/automation-precheck'
|
|
import { formatAutomationSchedule } from '../shared/automation-schedules'
|
|
|
|
export type AutomationListPayload = {
|
|
automations: Automation[]
|
|
/** Present only when the authority qualifies its rows; an older host sends none. */
|
|
items?: AutomationListItem[]
|
|
}
|
|
|
|
export type AutomationShowPayload = {
|
|
automation: Automation
|
|
/** Present only when the authority can project an owner; an older host sends none. */
|
|
owner?: AutomationOwnerPrecondition
|
|
}
|
|
|
|
const ORPHAN_HOST_LABEL = 'orphan (no host can run this automation)'
|
|
|
|
function formatOwnerSelector(selector: AutomationOwnerPrecondition['selector']): string {
|
|
if (selector.kind === 'ssh') {
|
|
return `ssh:${selector.targetId} (generation ${selector.targetGeneration})`
|
|
}
|
|
return selector.kind === 'orphan' ? ORPHAN_HOST_LABEL : 'self'
|
|
}
|
|
|
|
function formatListItemSelector(item: AutomationListItem): string {
|
|
const selector = item.selector
|
|
if (selector.kind === 'ssh') {
|
|
return `ssh:${selector.targetId} (generation ${selector.targetGeneration})`
|
|
}
|
|
return selector.kind === 'orphan' ? `orphan — ${selector.issue}` : 'self'
|
|
}
|
|
|
|
export function formatAutomationList(result: AutomationListPayload): string {
|
|
if (result.automations.length === 0) {
|
|
return 'No automations found.'
|
|
}
|
|
const hosts = new Map(result.items?.map((item) => [item.automationId, item]) ?? [])
|
|
return result.automations
|
|
.map((automation) => {
|
|
const status = automation.enabled ? 'enabled' : 'disabled'
|
|
const item = hosts.get(automation.id)
|
|
const host = item ? `\nhost: ${formatListItemSelector(item)}` : ''
|
|
return `${automation.id} ${automation.name} ${automation.agentId} ${status}\n${formatAutomationSchedule(automation.rrule)} next: ${new Date(automation.nextRunAt).toISOString()}${host}`
|
|
})
|
|
.join('\n\n')
|
|
}
|
|
|
|
export function formatAutomationShow(result: AutomationShowPayload): string {
|
|
const automation = result.automation
|
|
const runContext = automation.runContext ?? null
|
|
const projectLines = runContext
|
|
? [
|
|
`runProjectId: ${runContext.projectId}`,
|
|
`runHostId: ${runContext.hostId}`,
|
|
`projectHostSetupId: ${runContext.projectHostSetupId}`,
|
|
`runRepoId: ${runContext.repoId}`,
|
|
`runPath: ${runContext.path}`,
|
|
`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`
|
|
]
|
|
: [`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`]
|
|
return [
|
|
`id: ${automation.id}`,
|
|
`name: ${automation.name}`,
|
|
`provider: ${automation.agentId}`,
|
|
...(automation.extraAgentArgs ? [`extraAgentArgs: ${automation.extraAgentArgs}`] : []),
|
|
`enabled: ${automation.enabled}`,
|
|
`schedule: ${formatAutomationSchedule(automation.rrule)}`,
|
|
`rrule: ${automation.rrule}`,
|
|
`precheck: ${
|
|
automation.precheck
|
|
? `${automation.precheck.command} (timeout ${formatAutomationPrecheckTimeout(
|
|
automation.precheck.timeoutSeconds
|
|
)})`
|
|
: 'none'
|
|
}`,
|
|
`nextRunAt: ${new Date(automation.nextRunAt).toISOString()}`,
|
|
...projectLines,
|
|
`workspaceMode: ${automation.workspaceMode}`,
|
|
`workspaceId: ${automation.workspaceId ?? 'null'}`,
|
|
`baseBranch: ${automation.baseBranch ?? 'null'}`,
|
|
`reuseSession: ${automation.reuseSession}`,
|
|
`target: ${automation.executionTargetType}:${automation.executionTargetId}`,
|
|
// Omitted rather than guessed: a host that reports no owner has none to report.
|
|
...(result.owner ? [`host: ${formatOwnerSelector(result.owner.selector)}`] : []),
|
|
`prompt: ${automation.prompt}`
|
|
].join('\n')
|
|
}
|
|
|
|
export function formatAutomationRemoved(result: { removed: boolean; id: string }): string {
|
|
return result.removed
|
|
? `Removed automation ${result.id}.`
|
|
: `Automation ${result.id} not removed.`
|
|
}
|
|
|
|
export function formatAutomationRun(result: { run: AutomationRun }): string {
|
|
return [
|
|
`id: ${result.run.id}`,
|
|
`automationId: ${result.run.automationId}`,
|
|
`title: ${result.run.title}`,
|
|
`status: ${result.run.status}`,
|
|
`trigger: ${result.run.trigger}`,
|
|
`scheduledFor: ${new Date(result.run.scheduledFor).toISOString()}`,
|
|
`workspaceId: ${result.run.workspaceId ?? 'null'}`,
|
|
`precheck: ${formatAutomationRunPrecheck(result.run)}`,
|
|
`error: ${result.run.error ?? 'null'}`
|
|
].join('\n')
|
|
}
|
|
|
|
function formatAutomationRunPrecheck(run: AutomationRun): string {
|
|
const result = run.precheckResult
|
|
if (!result) {
|
|
return 'none'
|
|
}
|
|
const outcome = result.timedOut
|
|
? 'timed out'
|
|
: result.error
|
|
? 'error'
|
|
: `exit ${result.exitCode ?? 'unknown'}`
|
|
const output = result.stderr.trim() || result.stdout.trim()
|
|
return output ? `${outcome}; ${output}` : outcome
|
|
}
|
|
|
|
export function formatAutomationRunsPage(result: {
|
|
runs: AutomationRun[]
|
|
nextCursor: string | null
|
|
}): string {
|
|
const runs = formatAutomationRuns(result)
|
|
return result.nextCursor ? `${runs}\n\nNext page: --cursor ${result.nextCursor}` : runs
|
|
}
|
|
|
|
export function formatAutomationRuns(result: { runs: AutomationRun[] }): string {
|
|
if (result.runs.length === 0) {
|
|
return 'No automation runs found.'
|
|
}
|
|
return result.runs
|
|
.map(
|
|
(run) =>
|
|
`${run.id} ${run.automationId} ${run.status} ${run.trigger} ${new Date(run.scheduledFor).toISOString()}\n${run.title}${run.precheckResult ? `\nprecheck: ${formatAutomationRunPrecheck(run)}` : ''}${run.error ? `\nerror: ${run.error}` : ''}`
|
|
)
|
|
.join('\n\n')
|
|
}
|