Files
orca/src/shared/execution-host.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

200 lines
6.6 KiB
TypeScript

import type { GlobalSettings } from './global-settings-types'
import type { Repo } from './repo-types'
import type { Worktree } from './worktree/types'
export const LOCAL_EXECUTION_HOST_ID = 'local'
export const ALL_EXECUTION_HOSTS_SCOPE = 'all'
export type ExecutionHostKind = 'local' | 'ssh' | 'runtime'
export type ExecutionHostId = typeof LOCAL_EXECUTION_HOST_ID | `ssh:${string}` | `runtime:${string}`
export type ExecutionHostScope = typeof ALL_EXECUTION_HOSTS_SCOPE | ExecutionHostId
export type ParsedExecutionHost =
| { kind: 'local'; id: typeof LOCAL_EXECUTION_HOST_ID }
| { kind: 'ssh'; id: `ssh:${string}`; targetId: string }
| { kind: 'runtime'; id: `runtime:${string}`; environmentId: string }
function getCurrentLocalPlatform(): NodeJS.Platform | null {
const globalNavigator = (globalThis as { navigator?: { userAgent?: string; platform?: string } })
.navigator
const userAgent = globalNavigator?.userAgent || globalNavigator?.platform || ''
if (/Windows/i.test(userAgent)) {
return 'win32'
}
if (/Mac/i.test(userAgent)) {
return 'darwin'
}
if (/Linux|X11/i.test(userAgent)) {
return 'linux'
}
return typeof process === 'undefined' ? null : process.platform
}
export function getLocalExecutionHostLabel(platform: NodeJS.Platform | null = null): string {
const localPlatform = platform ?? getCurrentLocalPlatform()
if (localPlatform === 'darwin') {
return 'Local Mac'
}
if (localPlatform === 'win32') {
return 'Local Windows'
}
if (localPlatform === 'linux') {
return 'Local Linux'
}
return 'This computer'
}
function normalizeHostPart(value: string | null | undefined): string | null {
const trimmed = value?.trim()
return trimmed ? trimmed : null
}
export function toSshExecutionHostId(targetId: string): `ssh:${string}` {
return `ssh:${encodeURIComponent(targetId)}`
}
export function toRuntimeExecutionHostId(environmentId: string): `runtime:${string}` {
return `runtime:${encodeURIComponent(environmentId)}`
}
// Why: runtime-owned (ephemeral-VM) SSH targets are hidden from user-facing
// SSH/run-target surfaces. The renderer can't read the target.owner field, so it
// recognizes them by their deterministic id prefix. getRuntimeOwnedSshTargetId
// (main) builds on this same prefix to keep the two in sync.
export const RUNTIME_OWNED_SSH_TARGET_ID_PREFIX = 'runtime-ssh-'
export function isRuntimeOwnedSshTargetId(targetId: string | null | undefined): boolean {
return typeof targetId === 'string' && targetId.startsWith(RUNTIME_OWNED_SSH_TARGET_ID_PREFIX)
}
export function parseExecutionHostId(value: string | null | undefined): ParsedExecutionHost | null {
const normalized = normalizeHostPart(value)
if (!normalized) {
return null
}
if (normalized === LOCAL_EXECUTION_HOST_ID) {
return { kind: 'local', id: LOCAL_EXECUTION_HOST_ID }
}
if (normalized.startsWith('ssh:')) {
const encoded = normalized.slice('ssh:'.length)
if (!encoded) {
return null
}
try {
const targetId = decodeURIComponent(encoded)
return targetId ? { kind: 'ssh', id: `ssh:${encoded}`, targetId } : null
} catch {
return null
}
}
if (normalized.startsWith('runtime:')) {
const encoded = normalized.slice('runtime:'.length)
if (!encoded) {
return null
}
try {
const environmentId = decodeURIComponent(encoded)
return environmentId ? { kind: 'runtime', id: `runtime:${encoded}`, environmentId } : null
} catch {
return null
}
}
return null
}
export function normalizeExecutionHostId(value: string | null | undefined): ExecutionHostId | null {
return parseExecutionHostId(value)?.id ?? null
}
export function normalizeExecutionHostScope(value: string | null | undefined): ExecutionHostScope {
const normalized = normalizeHostPart(value)
if (!normalized || normalized === ALL_EXECUTION_HOSTS_SCOPE) {
return ALL_EXECUTION_HOSTS_SCOPE
}
return normalizeExecutionHostId(normalized) ?? ALL_EXECUTION_HOSTS_SCOPE
}
// An omitted scope on a request means this host, not a fan-out. Callers and the
// renderer share this so both agree on which requests answer with a merge.
export function requestedExecutionHostScope(value: string | null | undefined): ExecutionHostScope {
return normalizeExecutionHostScope(value ?? LOCAL_EXECUTION_HOST_ID)
}
export function normalizeVisibleExecutionHostIds(
value: readonly string[] | null | undefined
): ExecutionHostId[] | null {
if (!Array.isArray(value)) {
return null
}
const ids: ExecutionHostId[] = []
const seen = new Set<ExecutionHostId>()
for (const raw of value) {
const id = normalizeExecutionHostId(raw)
if (!id || seen.has(id)) {
continue
}
seen.add(id)
ids.push(id)
}
return ids.length > 0 ? ids : null
}
export function normalizeExecutionHostOrder(
value: readonly string[] | null | undefined
): ExecutionHostId[] {
const normalized = normalizeVisibleExecutionHostIds(value)
return normalized ?? []
}
export function getRepoExecutionHostId(
repo: Pick<Repo, 'connectionId' | 'executionHostId'>
): ExecutionHostId {
const executionHostId = normalizeExecutionHostId(repo.executionHostId)
if (executionHostId) {
return executionHostId
}
const connectionId = normalizeHostPart(repo.connectionId)
return connectionId ? toSshExecutionHostId(connectionId) : LOCAL_EXECUTION_HOST_ID
}
export function getWorktreeExecutionHostId(
worktree: Pick<Worktree, 'hostId'>,
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined,
defaultHostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID
): ExecutionHostId {
// Why: runtime and SSH snapshots can identify a more precise owner than
// the repo fallback; every sidebar host decision must use the same precedence.
return (
worktree.hostId ??
(repo?.connectionId || repo?.executionHostId ? getRepoExecutionHostId(repo) : defaultHostId)
)
}
export function getSettingsFocusedExecutionHostId(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
): ExecutionHostId {
const runtimeEnvironmentId = normalizeHostPart(settings?.activeRuntimeEnvironmentId)
return runtimeEnvironmentId
? toRuntimeExecutionHostId(runtimeEnvironmentId)
: LOCAL_EXECUTION_HOST_ID
}
export function getExecutionHostLabel(id: ExecutionHostScope): string {
if (id === ALL_EXECUTION_HOSTS_SCOPE) {
return 'All hosts'
}
const parsed = parseExecutionHostId(id)
if (!parsed) {
return 'All hosts'
}
switch (parsed.kind) {
case 'local':
return getLocalExecutionHostLabel()
case 'ssh':
return parsed.targetId
case 'runtime':
return parsed.environmentId
}
}