mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
171 lines
6.5 KiB
TypeScript
171 lines
6.5 KiB
TypeScript
/**
|
|
* Resolves which host a folder workspace executes on.
|
|
*
|
|
* Main and the renderer both have to answer this — the list projects a record's
|
|
* host from it, dispatch targets one from it — so the answer lives here rather
|
|
* than being re-derived per side and drifting. The workspace's own
|
|
* `executionHostId` pin wins: a pinned workspace runs there whatever its repos
|
|
* say, which is exactly the case that used to read as local.
|
|
*
|
|
* `ambiguous` is a distinct answer, not a missing one: a scope that spans a
|
|
* local repo and an SSH one has no single host, and callers must fail closed
|
|
* with something visible rather than defaulting to local.
|
|
*/
|
|
|
|
import type { FolderWorkspace } from './folder-workspace-types'
|
|
import type { ProjectGroup } from './project-group-types'
|
|
import type { Repo } from './repo-types'
|
|
import { isPathInsideOrEqual } from './cross-platform-path'
|
|
import { getProjectGroupSubtreeIds } from './project-groups'
|
|
import { getRepoExecutionHostId, normalizeHostPart, parseExecutionHostId } from './execution-host'
|
|
|
|
export type FolderWorkspaceHostState = {
|
|
folderWorkspaces: readonly FolderWorkspace[]
|
|
projectGroups: readonly ProjectGroup[]
|
|
repos: readonly Repo[]
|
|
}
|
|
|
|
export type FolderWorkspaceHost =
|
|
| { kind: 'missing' }
|
|
| { kind: 'local' }
|
|
| { kind: 'ssh'; targetId: string }
|
|
// A paired server's folder; `sshTargetId` is that server's own target, addressable only with it.
|
|
| { kind: 'runtime'; environmentId: string; sshTargetId: string | null }
|
|
| { kind: 'ambiguous' }
|
|
|
|
/**
|
|
* The SSH target whose filesystem holds this repo's files, or `null` for anything else.
|
|
*
|
|
* SSH ownership has two spellings on a repo row — the legacy `connectionId` field and the unified
|
|
* `executionHostId` — so reading the raw field sees only one of them and a row carrying only
|
|
* `executionHostId: 'ssh:<target>'` reads as if it had no connection at all. Every comparison in
|
|
* this file goes through here: the candidate filters decide which rows reach the resolver, so
|
|
* reading raw in either place drops the row before the resolver can classify it.
|
|
*
|
|
* A non-SSH host falls back to the raw field so a `runtime:` row keeps contributing its nested
|
|
* target, which is not this client's to dial.
|
|
*/
|
|
function getRepoScopeConnectionId(repo: Repo): string | null {
|
|
const host = parseExecutionHostId(getRepoExecutionHostId(repo))
|
|
return host?.kind === 'ssh' ? host.targetId : normalizeHostPart(repo.connectionId)
|
|
}
|
|
|
|
function getFolderScopeCandidateRepos(args: {
|
|
folderPath: string
|
|
projectGroupId: string
|
|
connectionId: string | null
|
|
projectGroups: readonly ProjectGroup[]
|
|
repos: readonly Repo[]
|
|
}): Repo[] {
|
|
const groupIds = getProjectGroupSubtreeIds(args.projectGroups, args.projectGroupId)
|
|
// Classify each repo once. The previous pair of filters read every
|
|
// projectGroupId twice before applying the same path predicate.
|
|
const groupRepos: Repo[] = []
|
|
const pathRepos: Repo[] = []
|
|
for (const repo of args.repos) {
|
|
const projectGroupId = repo.projectGroupId
|
|
if (typeof projectGroupId === 'string' && groupIds.has(projectGroupId)) {
|
|
groupRepos.push(repo)
|
|
} else if (isPathInsideOrEqual(args.folderPath, repo.path)) {
|
|
pathRepos.push(repo)
|
|
}
|
|
}
|
|
if (args.connectionId) {
|
|
return [
|
|
...groupRepos,
|
|
...pathRepos.filter((repo) => getRepoScopeConnectionId(repo) === args.connectionId)
|
|
]
|
|
}
|
|
if (groupRepos.length === 0) {
|
|
return pathRepos
|
|
}
|
|
// Both sides resolved: comparing a resolved path repo against a raw group read would reintroduce
|
|
// the same mismatch from the other direction.
|
|
const groupConnectionIds = new Set(groupRepos.map(getRepoScopeConnectionId))
|
|
return [
|
|
...groupRepos,
|
|
...pathRepos.filter((repo) => groupConnectionIds.has(getRepoScopeConnectionId(repo)))
|
|
]
|
|
}
|
|
|
|
export function findFolderWorkspaceCandidateRepos(
|
|
state: FolderWorkspaceHostState,
|
|
folderWorkspaceId: string
|
|
): Repo[] {
|
|
const workspace = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
|
|
if (!workspace) {
|
|
return []
|
|
}
|
|
const group = state.projectGroups.find((entry) => entry.id === workspace.projectGroupId)
|
|
return getFolderScopeCandidateRepos({
|
|
folderPath: workspace.folderPath,
|
|
projectGroupId: workspace.projectGroupId,
|
|
connectionId: normalizeHostPart(workspace.connectionId ?? group?.connectionId),
|
|
projectGroups: state.projectGroups,
|
|
repos: state.repos
|
|
})
|
|
}
|
|
|
|
export function resolveFolderWorkspaceHost(
|
|
state: FolderWorkspaceHostState,
|
|
folderWorkspaceId: string
|
|
): FolderWorkspaceHost {
|
|
const workspace = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
|
|
if (!workspace) {
|
|
return { kind: 'missing' }
|
|
}
|
|
const readScopeConnectionId = (): string | null =>
|
|
normalizeHostPart(
|
|
workspace.connectionId ??
|
|
state.projectGroups.find((entry) => entry.id === workspace.projectGroupId)?.connectionId
|
|
)
|
|
const explicitHost = parseExecutionHostId(workspace.executionHostId)
|
|
switch (explicitHost?.kind) {
|
|
case 'ssh':
|
|
return { kind: 'ssh', targetId: explicitHost.targetId }
|
|
case 'runtime': {
|
|
// Why: another host may hold a folder with this id, and this lookup cannot tell which one is
|
|
// selected, so the server's target is named only when the id is unambiguous.
|
|
const isOnlyOwner = state.folderWorkspaces.every(
|
|
(entry) => entry.id !== folderWorkspaceId || entry === workspace
|
|
)
|
|
return {
|
|
kind: 'runtime',
|
|
environmentId: explicitHost.environmentId,
|
|
sshTargetId: isOnlyOwner ? readScopeConnectionId() : null
|
|
}
|
|
}
|
|
case 'local':
|
|
return { kind: 'local' }
|
|
case undefined:
|
|
break
|
|
}
|
|
const scopeConnectionId = readScopeConnectionId()
|
|
const candidateRepos = findFolderWorkspaceCandidateRepos(state, folderWorkspaceId)
|
|
let hasLocalRepo = false
|
|
const connectionIds = new Set<string>()
|
|
for (const repo of candidateRepos) {
|
|
const connectionId = getRepoScopeConnectionId(repo)
|
|
if (connectionId) {
|
|
connectionIds.add(connectionId)
|
|
} else {
|
|
hasLocalRepo = true
|
|
}
|
|
}
|
|
if (scopeConnectionId) {
|
|
const hasDifferentSshConnection = [...connectionIds].some(
|
|
(connectionId) => connectionId !== scopeConnectionId
|
|
)
|
|
return hasLocalRepo || hasDifferentSshConnection
|
|
? { kind: 'ambiguous' }
|
|
: { kind: 'ssh', targetId: scopeConnectionId }
|
|
}
|
|
if (candidateRepos.length === 0 || connectionIds.size === 0) {
|
|
return { kind: 'local' }
|
|
}
|
|
if (hasLocalRepo || connectionIds.size > 1) {
|
|
return { kind: 'ambiguous' }
|
|
}
|
|
return { kind: 'ssh', targetId: [...connectionIds][0] }
|
|
}
|