Files
orca/src/shared/orca-yaml.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

262 lines
8.3 KiB
TypeScript

import { parseDocument } from 'yaml'
import type {
OrcaDefaultTabTemplate,
OrcaHooks,
OrcaVmRecipe,
OrcaVmRecipeDiagnostic
} from './orca-yaml-hook-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.'
// Why: bound the work one repo file can request; entries beyond this are ignored.
const MAX_SHARED_DIRECTORIES = 100
/** Normalize `worktree.sharedDirectories` into deduped repo-root-relative paths.
* `\` becomes `/`, a `./` prefix and trailing `/` are stripped. Absolute paths,
* `..` traversal and `.git` are dropped here so callers get only safe entries.
*
* Entries that would still need collapsing (`apps/./web`) are dropped rather
* than rewritten: `resolve()` collapses them when the symlink is created, but
* Git reports the collapsed path, so every later comparison against the stored
* entry would miss and the link would look like permanent untracked work. */
function normalizeSharedDirectories(value: unknown): string[] {
if (!Array.isArray(value)) {
return []
}
const seen = new Set<string>()
for (const entry of value.slice(0, MAX_SHARED_DIRECTORIES)) {
const raw = asTrimmedString(entry)
if (!raw) {
continue
}
const normalized = raw.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
const segments = normalized.split('/')
if (
!normalized ||
normalized.startsWith('/') ||
/^[a-zA-Z]:/.test(normalized) ||
segments.includes('..') ||
segments.includes('.') ||
segments.includes('') ||
segments.includes('.git')
) {
continue
}
seen.add(normalized)
}
return Array.from(seen)
}
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 checkoutMode = asTrimmedString(record.checkoutMode)
if (checkoutMode && checkoutMode !== 'orca-worktree' && checkoutMode !== 'provisioned-root') {
diagnostics.push({
index,
field: 'checkoutMode',
message: `Recipe "${id}" checkoutMode must be "orca-worktree" or "provisioned-root".`
})
return null
}
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,
...(checkoutMode ? { checkoutMode } : {}),
...(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
const worktreeRecord = asRecord(record.worktree)
const sharedDirectories = worktreeRecord
? normalizeSharedDirectories(worktreeRecord.sharedDirectories)
: []
if (
!setup &&
!archive &&
!issueCommand &&
defaultTabs.length === 0 &&
environmentRecipes.length === 0 &&
environmentRecipeDiagnostics.length === 0 &&
sharedDirectories.length === 0
) {
return null
}
return {
scripts: {
...(setup ? { setup } : {}),
...(archive ? { archive } : {})
},
...(issueCommand ? { issueCommand } : {}),
...(defaultTabs.length > 0 ? { defaultTabs } : {}),
...(environmentRecipes.length > 0 ? { environmentRecipes } : {}),
...(environmentRecipeDiagnostics.length > 0 ? { environmentRecipeDiagnostics } : {}),
...(sharedDirectories.length > 0 ? { worktree: { sharedDirectories } } : {})
}
}