mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.
Move each domain into its own folder and drop the now-redundant prefix:
src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts
src/shared/worktree-id.ts -> src/shared/worktree/id.ts
src/shared/linear-links.ts -> src/shared/linear/links.ts
This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.
Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.
Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.
Two things `tsc` cannot catch, handled explicitly:
- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
entry is REPOINTED to the new path rather than pruned. Pruning would drop the
bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
(`mobile/node_modules` is empty). Instead every relative specifier in the repo
was resolved against the filesystem: 174 unresolved before this change and 174
after — identical, so nothing broke in mobile either.
The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
194 lines
6.6 KiB
TypeScript
194 lines
6.6 KiB
TypeScript
/* eslint-disable no-control-regex -- ANSI SGR sequences are raw PTY input. */
|
|
/**
|
|
* Chunk-boundary-safe GitHub PR URL scan over PTY output.
|
|
*
|
|
* Why shared: terminal-side-effect-authority.md (slice 3) makes main emit
|
|
* `pr-link` facts from its per-PTY tracker for local/SSH PTYs, while the
|
|
* renderer keeps byte-scanning for remote-runtime PTYs and the kill-switch-off
|
|
* path. Both paths must share the carry/dedupe semantics or links split across
|
|
* chunks would resolve differently per authority mode.
|
|
*/
|
|
import type { RepoSlug } from './github/links'
|
|
import { parseGitHubIssueOrPRLink } from './github/links'
|
|
|
|
const GITHUB_PR_PATH_MARKER = '/pull/'
|
|
const TERMINAL_SGR_PATTERN = /\x1b\[[0-?]*[ -/]*m/g
|
|
const TERMINAL_CURSOR_CONTROL_PATTERN = /[\x08\x0b\x0c]/g
|
|
const TERMINAL_CONTROL_GUARD = '\ufffd'
|
|
const HTTP_SCHEME_PREFIXES = ['https://', 'http://'] as const
|
|
const TRAILING_TERMINAL_PUNCTUATION_RE = /[),.;\]}]+$/
|
|
const MAX_CARRY_LENGTH = 512
|
|
const MAX_TERMINAL_GITHUB_PR_URL_LENGTH = 2048
|
|
|
|
export type TerminalGitHubPRLink = {
|
|
url: string
|
|
slug: RepoSlug
|
|
number: number
|
|
}
|
|
|
|
function trimTerminalUrl(candidate: string): string {
|
|
return candidate.replace(TRAILING_TERMINAL_PUNCTUATION_RE, '')
|
|
}
|
|
|
|
function parseTerminalGitHubPRUrl(candidate: string): TerminalGitHubPRLink | null {
|
|
if (candidate.includes('\x1b') || candidate.includes(TERMINAL_CONTROL_GUARD)) {
|
|
return null
|
|
}
|
|
const url = trimTerminalUrl(candidate)
|
|
const parsed = parseGitHubIssueOrPRLink(url)
|
|
if (!parsed || parsed.type !== 'pr') {
|
|
return null
|
|
}
|
|
return { url, slug: parsed.slug, number: parsed.number }
|
|
}
|
|
|
|
function endsWithHttpSchemePrefixFragment(value: string): string {
|
|
for (const prefix of HTTP_SCHEME_PREFIXES) {
|
|
for (let length = Math.min(prefix.length - 1, value.length); length > 0; length--) {
|
|
if (value.endsWith(prefix.slice(0, length))) {
|
|
return value.slice(value.length - length)
|
|
}
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function lastIndexOfHttpScheme(value: string, fromIndex?: number): number {
|
|
let lastIndex = -1
|
|
for (const prefix of HTTP_SCHEME_PREFIXES) {
|
|
const candidate =
|
|
fromIndex === undefined ? value.lastIndexOf(prefix) : value.lastIndexOf(prefix, fromIndex)
|
|
if (candidate > lastIndex) {
|
|
lastIndex = candidate
|
|
}
|
|
}
|
|
return lastIndex
|
|
}
|
|
|
|
function getPotentialGitHubPRCarry(value: string): string {
|
|
// Why bounded: carry is always a suffix of at most MAX_CARRY_LENGTH, so a scheme
|
|
// further back can only ever be dropped — scanning to it is O(chunk) per PTY write.
|
|
const windowStart = value.length > MAX_CARRY_LENGTH ? value.length - MAX_CARRY_LENGTH : 0
|
|
const tailWindow = windowStart === 0 ? value : value.slice(windowStart)
|
|
const schemeIndexInWindow = lastIndexOfHttpScheme(tailWindow)
|
|
if (schemeIndexInWindow !== -1) {
|
|
const schemeIndex = windowStart + schemeIndexInWindow
|
|
return hasTerminalUrlWhitespace(value, schemeIndex, value.length)
|
|
? ''
|
|
: value.slice(schemeIndex)
|
|
}
|
|
|
|
const fragment = endsWithHttpSchemePrefixFragment(tailWindow)
|
|
if (fragment === '' || windowStart === 0) {
|
|
return fragment
|
|
}
|
|
// Why look behind: an older scheme means the URL already overran the cap, so the
|
|
// carry is abandoned rather than restarted from this fragment.
|
|
return lastIndexOfHttpScheme(value, windowStart - 1) === -1 ? fragment : ''
|
|
}
|
|
|
|
function hasTerminalUrlWhitespace(value: string, start: number, end: number): boolean {
|
|
for (let index = start; index < end; index += 1) {
|
|
if (/\s/.test(value.charAt(index))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type TerminalUrlCandidate = {
|
|
rawUrl: string
|
|
endIndex: number
|
|
}
|
|
|
|
function findNextHttpSchemeIndex(value: string, start: number): number {
|
|
let nextIndex = -1
|
|
for (const prefix of HTTP_SCHEME_PREFIXES) {
|
|
const candidate = value.indexOf(prefix, start)
|
|
if (candidate !== -1 && (nextIndex === -1 || candidate < nextIndex)) {
|
|
nextIndex = candidate
|
|
}
|
|
}
|
|
return nextIndex
|
|
}
|
|
|
|
function isTerminalUrlTerminator(char: string): boolean {
|
|
return char === '"' || char === "'" || char === '<' || char === '>' || /\s/.test(char)
|
|
}
|
|
|
|
function findTerminalUrlCandidateEnd(value: string, start: number): number {
|
|
const scanEnd = Math.min(value.length, start + MAX_TERMINAL_GITHUB_PR_URL_LENGTH + 1)
|
|
for (let index = start; index < scanEnd; index += 1) {
|
|
if (isTerminalUrlTerminator(value.charAt(index))) {
|
|
return index
|
|
}
|
|
}
|
|
return scanEnd
|
|
}
|
|
|
|
function* iterateTerminalUrlCandidates(value: string): Generator<TerminalUrlCandidate> {
|
|
let searchStart = 0
|
|
|
|
while (searchStart < value.length) {
|
|
const candidateStart = findNextHttpSchemeIndex(value, searchStart)
|
|
if (candidateStart === -1) {
|
|
return
|
|
}
|
|
|
|
const candidateEnd = findTerminalUrlCandidateEnd(value, candidateStart)
|
|
const rawUrl = value.slice(candidateStart, candidateEnd)
|
|
searchStart = Math.max(candidateEnd, candidateStart + 1)
|
|
if (
|
|
rawUrl.length > MAX_TERMINAL_GITHUB_PR_URL_LENGTH ||
|
|
!rawUrl.includes(GITHUB_PR_PATH_MARKER)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
yield { rawUrl, endIndex: candidateEnd }
|
|
}
|
|
}
|
|
|
|
export function createTerminalGitHubPRLinkDetector(): (data: string) => TerminalGitHubPRLink[] {
|
|
let carry = ''
|
|
const seenUrls = new Set<string>()
|
|
|
|
return (data: string): TerminalGitHubPRLink[] => {
|
|
const rawCombined = carry ? carry + data : data
|
|
|
|
// Why: PTY output is a hot path; avoid multi-pass ANSI normalization for
|
|
// chunks that cannot contain a GitHub pull-request URL.
|
|
if (!rawCombined.includes(GITHUB_PR_PATH_MARKER)) {
|
|
carry = getPotentialGitHubPRCarry(rawCombined)
|
|
return []
|
|
}
|
|
// Why: SGR styling has no screen width, so removing it is safe. Cursor
|
|
// controls get a guard; other escape sequences remain URL-invalid.
|
|
const combined = rawCombined
|
|
.replace(TERMINAL_SGR_PATTERN, '')
|
|
.replace(TERMINAL_CURSOR_CONTROL_PATTERN, TERMINAL_CONTROL_GUARD)
|
|
|
|
const links: TerminalGitHubPRLink[] = []
|
|
// Why: PTY data may echo a huge pasted line. Scan URL candidates directly
|
|
// instead of running a global regex across the whole chunk when it contains
|
|
// a /pull/ substring.
|
|
for (const { rawUrl, endIndex } of iterateTerminalUrlCandidates(combined)) {
|
|
// Why: PTY chunks can split the PR number; wait for a boundary before
|
|
// treating a URL at chunk-end as complete.
|
|
if (endIndex === combined.length) {
|
|
continue
|
|
}
|
|
|
|
const parsed = parseTerminalGitHubPRUrl(rawUrl)
|
|
if (!parsed || seenUrls.has(parsed.url)) {
|
|
continue
|
|
}
|
|
seenUrls.add(parsed.url)
|
|
links.push(parsed)
|
|
}
|
|
|
|
carry = getPotentialGitHubPRCarry(rawCombined)
|
|
return links
|
|
}
|
|
}
|