Files
orca/src/shared/orca-yaml.ts
T
NeilandOrca 879aad7dd6 oom(foundation): bound shared readers/limits + add BoundedMap primitive (#10299)
* oom(01): A1-shared-readers — reintroduce #10179 subset

Files: 18 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

* oom(02): A2-shared-image-media — reintroduce #10179 subset

Files: 7 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

* oom(03): A3-shared-fs-listing — reintroduce #10179 subset

Files: 21 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

* oom(04): A4-shared-remote-relay — reintroduce #10179 subset

Files: 8 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

* oom(05): A5-shared-misc — reintroduce #10179 subset

Files: 28 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

* oom(06): B-shared-wiring — reintroduce #10179 subset

Files: 81 applied, 0 deleted (from 6eb70d8370)

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-24 21:36:57 -07:00

206 lines
6.2 KiB
TypeScript

import { parseDocument } from 'yaml'
import type {
OrcaDefaultTabTemplate,
OrcaHooks,
OrcaVmRecipe,
OrcaVmRecipeDiagnostic
} from './types'
import {
isOrcaYamlFieldWithinLimit,
isOrcaYamlTextWithinLimit,
MAX_ORCA_YAML_ALIAS_COUNT,
MAX_ORCA_YAML_COLLECTION_ENTRIES
} from './orca-yaml-file-limit'
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
}
function asTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string' || !isOrcaYamlFieldWithinLimit(value)) {
return undefined
}
const trimmed = value.trim()
return trimmed || undefined
}
const DEFAULT_TAB_COLOR_RE = /^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?$/
export const ORCA_VM_RECIPE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/
export const ORCA_VM_RECIPE_ID_RULE =
'Use 1-64 lowercase letters, numbers, dots, underscores, or hyphens, starting with a letter or number.'
function normalizeDefaultTabs(value: unknown): OrcaDefaultTabTemplate[] {
if (!Array.isArray(value) || value.length > MAX_ORCA_YAML_COLLECTION_ENTRIES) {
return []
}
return value
.map((entry) => {
const record = asRecord(entry)
if (!record) {
return null
}
const title = asTrimmedString(record.title)
const command = asTrimmedString(record.command)
const color = asTrimmedString(record.color)
const normalizedColor = color && DEFAULT_TAB_COLOR_RE.test(color) ? color : undefined
if (!title && !command && !normalizedColor) {
return null
}
return {
...(title ? { title } : {}),
...(normalizedColor ? { color: normalizedColor } : {}),
...(command ? { command } : {})
}
})
.filter((entry): entry is OrcaDefaultTabTemplate => entry !== null)
}
type VmRecipeParseResult = {
recipes: OrcaVmRecipe[]
diagnostics: OrcaVmRecipeDiagnostic[]
}
function normalizeVmRecipes(value: unknown): VmRecipeParseResult {
const diagnostics: OrcaVmRecipeDiagnostic[] = []
if (!Array.isArray(value)) {
return { recipes: [], diagnostics }
}
if (value.length > MAX_ORCA_YAML_COLLECTION_ENTRIES) {
return {
recipes: [],
diagnostics: [
{
index: MAX_ORCA_YAML_COLLECTION_ENTRIES,
message: `At most ${MAX_ORCA_YAML_COLLECTION_ENTRIES} environment recipes are supported.`
}
]
}
}
const seenIds = new Set<string>()
const recipes = value
.map((entry, index) => {
const record = asRecord(entry)
if (!record) {
diagnostics.push({
index,
message: 'Recipe entry must be a mapping.'
})
return null
}
const id = asTrimmedString(record.id)
const name = asTrimmedString(record.name)
const create = asTrimmedString(record.create) ?? asTrimmedString(record.command)
if (!id) {
diagnostics.push({ index, field: 'id', message: 'Recipe id is required.' })
return null
}
if (!ORCA_VM_RECIPE_ID_PATTERN.test(id)) {
diagnostics.push({
index,
field: 'id',
message: `Invalid recipe id "${id}". ${ORCA_VM_RECIPE_ID_RULE}`
})
return null
}
if (seenIds.has(id)) {
diagnostics.push({
index,
field: 'id',
message: `Duplicate recipe id "${id}". Recipe ids must be unique.`
})
return null
}
if (!name) {
diagnostics.push({ index, field: 'name', message: `Recipe "${id}" is missing name.` })
return null
}
if (!create) {
diagnostics.push({ index, field: 'create', message: `Recipe "${id}" is missing create.` })
return null
}
seenIds.add(id)
const description = asTrimmedString(record.description)
const suspend = asTrimmedString(record.suspend)
const resume = asTrimmedString(record.resume)
const destroyValue = asTrimmedString(record.destroy) ?? asTrimmedString(record.cleanup)
const destroyDisabled = destroyValue === 'none'
return {
id,
name,
create,
...(description ? { description } : {}),
...(suspend ? { suspend } : {}),
...(resume ? { resume } : {}),
...(destroyValue && !destroyDisabled ? { destroy: destroyValue } : {}),
...(destroyDisabled ? { destroyDisabled: true } : {})
}
})
.filter((entry): entry is OrcaVmRecipe => entry !== null)
return { recipes, diagnostics }
}
/**
* Parse the supported project defaults from `orca.yaml`.
*/
export function parseOrcaYaml(content: string): OrcaHooks | null {
if (!isOrcaYamlTextWithinLimit(content)) {
return null
}
let root: unknown
try {
const document = parseDocument(content, {
keepSourceTokens: false,
logLevel: 'silent',
prettyErrors: false,
uniqueKeys: true
})
if (document.errors.length > 0) {
return null
}
root = document.toJS({ maxAliasCount: MAX_ORCA_YAML_ALIAS_COUNT })
} catch {
return null
}
const record = asRecord(root)
if (!record) {
return null
}
const scriptsRecord = asRecord(record.scripts)
const setup = scriptsRecord ? asTrimmedString(scriptsRecord.setup) : undefined
const archive = scriptsRecord ? asTrimmedString(scriptsRecord.archive) : undefined
const issueCommand = asTrimmedString(record.issueCommand)
const defaultTabs = normalizeDefaultTabs(record.defaultTabs)
const environmentRecipeParse = normalizeVmRecipes(record.environmentRecipes)
const environmentRecipes = environmentRecipeParse.recipes
const environmentRecipeDiagnostics = environmentRecipeParse.diagnostics
if (
!setup &&
!archive &&
!issueCommand &&
defaultTabs.length === 0 &&
environmentRecipes.length === 0 &&
environmentRecipeDiagnostics.length === 0
) {
return null
}
return {
scripts: {
...(setup ? { setup } : {}),
...(archive ? { archive } : {})
},
...(issueCommand ? { issueCommand } : {}),
...(defaultTabs.length > 0 ? { defaultTabs } : {}),
...(environmentRecipes.length > 0 ? { environmentRecipes } : {}),
...(environmentRecipeDiagnostics.length > 0 ? { environmentRecipeDiagnostics } : {})
}
}