mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* Add CLI commands to manage and find workspace references * Fix reference review findings: source-context twins, legacy link collapse, closed chats, old-host errors - Keep same-URL links with different source contexts separate; CLI add/remove no longer drops a twin - Restore merging a bare legacy link into its single matching URL link (no duplicate PR rows, no resurrection on older-client removal) - Carry absorbed data through collapse chains; resolve repoId conflicts deterministically - Identity: ignore www., case-insensitive GitHub/GitLab/Gitea paths (stored URL keeps case), no Jira match on git hosts; find accepts self-hosted issue URLs - reference find: skip closed native chats, require a session id for linked, include an explicitly named archived workspace, warm-cache fallback for --worktree - Selector parity: collapse same-host duplicate paths, issue: ignores unlinked rows - CLI: clear update-host error for hosts without reference.*, local flag checks before host probe, help text fixes * Fix CI: add reference rm alias for verb policy; expect linkedItems in worktree ps summary * Simplify reference CLI: share identifier/legacy-match helpers, drop redundant dedupe
201 lines
7.1 KiB
TypeScript
201 lines
7.1 KiB
TypeScript
import type { RuntimeStore } from './runtime-store-contract'
|
|
import type {
|
|
ReferenceWorkspace,
|
|
RuntimeReferenceListResult
|
|
} from '../../shared/runtime-reference-contracts'
|
|
import type { Worktree } from '../../shared/worktree/types'
|
|
import { mergeWorktree } from '../ipc/worktree-metadata-merge'
|
|
import { readAllWorktreeMetaForHost } from '../persistence/host-qualified-worktree-meta'
|
|
import { getRepoExecutionHostId } from '../../shared/execution-host'
|
|
import { splitWorktreeIdForFilesystem, worktreeIdsEqual } from '../../shared/worktree/id'
|
|
import { getWorktreeHostIdentity } from '../../shared/worktree/host-qualified-identity'
|
|
import { folderWorkspaceToWorktree } from '../../shared/folder-workspace-worktree'
|
|
import {
|
|
getWorkspaceAttachments,
|
|
matchesLegacyWorkspaceAttachment
|
|
} from '../../shared/workspace-attachments'
|
|
import { legacyWorkspaceAttachments } from '../../shared/workspace-attachment-legacy'
|
|
import { getWorkspaceReferenceIdentity } from '../../shared/workspace-reference-identity'
|
|
import {
|
|
isPathInsideOrEqual,
|
|
isWslUncPathForCallerLinuxPath,
|
|
isWslUncPathForLinuxMountedPath
|
|
} from '../../shared/cross-platform-path'
|
|
import { parseWslUncPath } from '../../shared/wsl-paths'
|
|
import { branchSelectorMatches, runtimePathsEqual } from './runtime-worktree-path-identity'
|
|
|
|
export function listReferenceWorkspaces(
|
|
store: Pick<
|
|
RuntimeStore,
|
|
| 'getRepos'
|
|
| 'getAllWorktreeMeta'
|
|
| 'getAllWorktreeMetaForHost'
|
|
| 'getProjectGroups'
|
|
| 'getFolderWorkspaces'
|
|
>,
|
|
cached: readonly Worktree[] = []
|
|
): ReferenceWorkspace[] {
|
|
const repos = store.getRepos()
|
|
const ownerCounts = new Map<string, number>()
|
|
for (const repo of repos) {
|
|
ownerCounts.set(repo.id, (ownerCounts.get(repo.id) ?? 0) + 1)
|
|
}
|
|
const snapshots = new Map(
|
|
[...new Set(repos.map(getRepoExecutionHostId))].map((host) => [
|
|
host,
|
|
readAllWorktreeMetaForHost(store, host)
|
|
])
|
|
)
|
|
const cachedByIdentity = new Map(cached.map((row) => [getWorktreeHostIdentity(row), row]))
|
|
const reposByIdentity = new Map(
|
|
repos.map((repo) => [
|
|
getWorktreeHostIdentity({
|
|
id: repo.id,
|
|
hostId: getRepoExecutionHostId(repo)
|
|
}),
|
|
repo
|
|
])
|
|
)
|
|
const result: ReferenceWorkspace[] = []
|
|
for (const [hostId, metadata] of snapshots) {
|
|
for (const [id, meta] of Object.entries(metadata)) {
|
|
const parsed = splitWorktreeIdForFilesystem(id)
|
|
if (!parsed) {
|
|
continue
|
|
}
|
|
const repo = reposByIdentity.get(getWorktreeHostIdentity({ id: parsed.repoId, hostId }))
|
|
if (!repo) {
|
|
continue
|
|
}
|
|
if (!meta.hostId && (ownerCounts.get(repo.id) ?? 0) > 1) {
|
|
continue
|
|
}
|
|
const previous = cachedByIdentity.get(getWorktreeHostIdentity({ id, hostId }))
|
|
const worktree = mergeWorktree(
|
|
repo.id,
|
|
{
|
|
path: parsed.worktreePath,
|
|
head: previous?.head ?? '',
|
|
branch: previous?.branch ?? '',
|
|
isBare: previous?.isBare ?? false,
|
|
isMainWorktree: runtimePathsEqual(parsed.worktreePath, repo.path)
|
|
},
|
|
{ ...meta, hostId },
|
|
repo.displayName
|
|
)
|
|
result.push({
|
|
...worktree,
|
|
id,
|
|
kind: 'worktree',
|
|
name: worktree.displayName,
|
|
repo: repo.displayName,
|
|
linkedItems: getWorkspaceAttachments(worktree)
|
|
})
|
|
}
|
|
}
|
|
const known = new Set(result.map(getWorktreeHostIdentity))
|
|
for (const worktree of cached) {
|
|
const repo = reposByIdentity.get(
|
|
getWorktreeHostIdentity({ id: worktree.repoId, hostId: worktree.hostId })
|
|
)
|
|
if (!repo || known.has(getWorktreeHostIdentity(worktree))) {
|
|
continue
|
|
}
|
|
result.push({
|
|
...worktree,
|
|
kind: 'worktree',
|
|
name: worktree.displayName,
|
|
repo: repo.displayName,
|
|
linkedItems: getWorkspaceAttachments(worktree)
|
|
})
|
|
}
|
|
const groups = new Map((store.getProjectGroups?.() ?? []).map((group) => [group.id, group]))
|
|
for (const folder of store.getFolderWorkspaces?.() ?? []) {
|
|
const worktree = folderWorkspaceToWorktree(folder)
|
|
result.push({
|
|
...worktree,
|
|
kind: 'folder',
|
|
name: folder.name,
|
|
repo: groups.get(folder.projectGroupId)?.name ?? folder.projectGroupId,
|
|
linkedItems: getWorkspaceAttachments(worktree)
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function listWorkspaceReferences(worktree: ReferenceWorkspace): RuntimeReferenceListResult {
|
|
const selected = legacyWorkspaceAttachments(worktree)
|
|
return {
|
|
worktree,
|
|
references: getWorkspaceAttachments(worktree).map((item) => ({
|
|
...item,
|
|
key: getWorkspaceReferenceIdentity(item),
|
|
selected: selected.some((legacy) => matchesLegacyWorkspaceAttachment(item, legacy))
|
|
}))
|
|
}
|
|
}
|
|
|
|
export function selectReferenceWorkspace(
|
|
workspaces: readonly ReferenceWorkspace[],
|
|
selector: string,
|
|
cwd?: string
|
|
): ReferenceWorkspace {
|
|
let matches: ReferenceWorkspace[]
|
|
if (selector === 'current') {
|
|
if (!cwd) {
|
|
throw new Error('Current workspace requires a local working directory')
|
|
}
|
|
matches = workspaces.filter(
|
|
(row) => row.hostId === 'local' && isPathInsideOrEqual(row.path, cwd)
|
|
)
|
|
const longest = Math.max(...matches.map((row) => row.path.length))
|
|
matches = matches.filter((row) => row.path.length === longest)
|
|
} else if (selector.startsWith('identity:')) {
|
|
matches = workspaces.filter((row) => row.identity?.key === selector.slice(9))
|
|
} else if (selector.startsWith('id:')) {
|
|
matches = workspaces.filter((row) => worktreeIdsEqual(row.id, selector.slice(3)))
|
|
} else if (selector.startsWith('name:')) {
|
|
matches = workspaces.filter((row) => row.name === selector.slice(5))
|
|
} else if (selector.startsWith('path:')) {
|
|
const path = selector.slice(5)
|
|
const distro = cwd ? parseWslUncPath(cwd)?.distro : undefined
|
|
const linuxPath = path.startsWith('/') && !path.startsWith('//') && !path.includes('\\')
|
|
matches = workspaces.filter(
|
|
(row) =>
|
|
runtimePathsEqual(row.path, path) ||
|
|
(cwd &&
|
|
linuxPath &&
|
|
row.hostId === 'local' &&
|
|
(isWslUncPathForLinuxMountedPath(row.path, path) ||
|
|
(distro && isWslUncPathForCallerLinuxPath(row.path, path, distro))))
|
|
)
|
|
} else if (selector.startsWith('branch:')) {
|
|
matches = workspaces.filter(
|
|
(row) => row.branch && branchSelectorMatches(row.branch, selector.slice(7))
|
|
)
|
|
} else if (selector.startsWith('issue:')) {
|
|
matches = workspaces.filter(
|
|
(row) => typeof row.linkedIssue === 'number' && String(row.linkedIssue) === selector.slice(6)
|
|
)
|
|
} else {
|
|
matches = workspaces.filter(
|
|
(row) =>
|
|
row.id === selector ||
|
|
runtimePathsEqual(row.path, selector) ||
|
|
(row.branch && branchSelectorMatches(row.branch, selector))
|
|
)
|
|
}
|
|
// Why: duplicate registrations on one host describe one path; identical paths on different hosts do not.
|
|
if (
|
|
(selector === 'current' || selector.startsWith('path:')) &&
|
|
matches.length > 1 &&
|
|
new Set(matches.map((row) => row.hostId)).size === 1
|
|
) {
|
|
return matches[0]
|
|
}
|
|
if (matches.length !== 1) {
|
|
throw new Error(matches.length ? 'selector_ambiguous' : 'selector_not_found')
|
|
}
|
|
return matches[0]
|
|
}
|