mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +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).
304 lines
9.6 KiB
TypeScript
304 lines
9.6 KiB
TypeScript
import {
|
|
LINEAR_CHILDREN_MAX_DEPTH,
|
|
clampLinearIssueDepth,
|
|
clampLinearSearchLimit,
|
|
type LinearIssueInclude
|
|
} from '../../shared/linear/agent-access'
|
|
import type { RpcDispatcher } from '../runtime/rpc/dispatcher'
|
|
import type { RpcResponse } from '../runtime/rpc/core'
|
|
import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error'
|
|
import { dispatchRemoteLinearListIssues } from './ssh-remote-linear-list-issues'
|
|
|
|
import {
|
|
LINEAR_ISSUE_FLAGS,
|
|
LINEAR_LIST_FLAGS,
|
|
LINEAR_PROJECT_LIST_FLAGS,
|
|
LINEAR_SEARCH_FLAGS,
|
|
LINEAR_TEAM_LIST_FLAGS,
|
|
LINEAR_TEAM_LOOKUP_FLAGS
|
|
} from './ssh-remote-linear-read-flags'
|
|
|
|
export async function tryDispatchRemoteLinearReadCli(
|
|
dispatcher: RpcDispatcher,
|
|
parsed: ParsedRemoteCli,
|
|
env: Record<string, string>
|
|
): Promise<RpcResponse | null> {
|
|
if (isRemoteCommand(parsed, 'linear', 'list-issues')) {
|
|
return await dispatchRemoteLinearListIssues(dispatcher, parsed)
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'issue')) {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command: ['linear', 'issue'],
|
|
allowedFlags: LINEAR_ISSUE_FLAGS,
|
|
positionalFlag: 'id',
|
|
maxPositionals: 1
|
|
})
|
|
return await call(dispatcher, 'linear.issueContext', buildRemoteLinearIssueRequest(parsed, env))
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'search')) {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command: ['linear', 'search'],
|
|
allowedFlags: LINEAR_SEARCH_FLAGS,
|
|
positionalFlag: 'query',
|
|
maxPositionals: 1
|
|
})
|
|
return await call(dispatcher, 'linear.agentSearchIssues', {
|
|
query: remotePositional(parsed, 2) ?? requiredString(parsed.flags, 'query'),
|
|
limit: clampLinearSearchLimit(optionalPositiveInteger(parsed.flags, 'limit')),
|
|
workspaceId: optionalString(parsed.flags, 'workspace')
|
|
})
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'team', 'list')) {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command: ['linear', 'team', 'list'],
|
|
allowedFlags: LINEAR_TEAM_LIST_FLAGS,
|
|
positionalFlag: 'id',
|
|
maxPositionals: 0
|
|
})
|
|
return await call(dispatcher, 'linear.agentTeamList', {
|
|
workspaceId: optionalString(parsed.flags, 'workspace')
|
|
})
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'team', 'members')) {
|
|
return await dispatchRemoteLinearTeamLookup(
|
|
dispatcher,
|
|
parsed,
|
|
['linear', 'team', 'members'],
|
|
'linear.agentTeamMembers'
|
|
)
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'team', 'states')) {
|
|
return await dispatchRemoteLinearTeamLookup(
|
|
dispatcher,
|
|
parsed,
|
|
['linear', 'team', 'states'],
|
|
'linear.agentTeamStates'
|
|
)
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'team', 'labels')) {
|
|
return await dispatchRemoteLinearTeamLookup(
|
|
dispatcher,
|
|
parsed,
|
|
['linear', 'team', 'labels'],
|
|
'linear.agentTeamLabels'
|
|
)
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'project', 'list')) {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command: ['linear', 'project', 'list'],
|
|
allowedFlags: LINEAR_PROJECT_LIST_FLAGS,
|
|
positionalFlag: 'id',
|
|
maxPositionals: 0
|
|
})
|
|
return await call(dispatcher, 'linear.agentProjectList', {
|
|
query: optionalString(parsed.flags, 'query'),
|
|
limit: clampLinearSearchLimit(optionalPositiveInteger(parsed.flags, 'limit')),
|
|
workspaceId: optionalString(parsed.flags, 'workspace')
|
|
})
|
|
}
|
|
if (isRemoteCommand(parsed, 'linear', 'list')) {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command: ['linear', 'list'],
|
|
allowedFlags: LINEAR_LIST_FLAGS,
|
|
positionalFlag: 'id',
|
|
maxPositionals: 0
|
|
})
|
|
return await call(dispatcher, 'linear.agentIssueList', {
|
|
filter: linearListFilter(parsed.flags),
|
|
teamInput: optionalString(parsed.flags, 'team'),
|
|
limit: optionalPositiveInteger(parsed.flags, 'limit'),
|
|
workspaceId: optionalString(parsed.flags, 'workspace')
|
|
})
|
|
}
|
|
return null
|
|
}
|
|
|
|
async function dispatchRemoteLinearTeamLookup(
|
|
dispatcher: RpcDispatcher,
|
|
parsed: ParsedRemoteCli,
|
|
command: string[],
|
|
method: string
|
|
): Promise<RpcResponse> {
|
|
validateLinearRemoteArgs(parsed, {
|
|
command,
|
|
allowedFlags: LINEAR_TEAM_LOOKUP_FLAGS,
|
|
positionalFlag: 'team',
|
|
maxPositionals: 0
|
|
})
|
|
return await call(dispatcher, method, {
|
|
teamInput: requiredString(parsed.flags, 'team'),
|
|
workspaceId: optionalString(parsed.flags, 'workspace')
|
|
})
|
|
}
|
|
|
|
function validateLinearRemoteArgs(
|
|
parsed: ParsedRemoteCli,
|
|
options: {
|
|
command: string[]
|
|
allowedFlags: ReadonlySet<string>
|
|
positionalFlag: string
|
|
maxPositionals: number
|
|
}
|
|
): void {
|
|
for (const flag of parsed.flags.keys()) {
|
|
if (!options.allowedFlags.has(flag)) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
`Unknown flag --${flag} for command: ${options.command.join(' ')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
const positionals = parsed.commandPath.slice(options.command.length)
|
|
if (positionals.length > options.maxPositionals) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
`Unknown command: ${parsed.commandPath.join(' ')}`
|
|
)
|
|
}
|
|
if (positionals.length > 0 && parsed.flags.has(options.positionalFlag)) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
`Pass --${options.positionalFlag} either positionally or as a flag, not both.`
|
|
)
|
|
}
|
|
}
|
|
|
|
function isRemoteCommand(parsed: ParsedRemoteCli, ...command: string[]): boolean {
|
|
return command.every((part, index) => parsed.commandPath[index] === part)
|
|
}
|
|
|
|
function remotePositional(parsed: ParsedRemoteCli, startIndex: number): string | undefined {
|
|
const value = parsed.commandPath.slice(startIndex).join(' ').trim()
|
|
return value || undefined
|
|
}
|
|
|
|
function buildRemoteLinearIssueRequest(
|
|
parsed: ParsedRemoteCli,
|
|
env: Record<string, string>
|
|
): Record<string, unknown> {
|
|
const full = parsed.flags.get('full') === true
|
|
const include: Record<LinearIssueInclude, boolean> = {
|
|
comments: full || parsed.flags.get('comments') === true,
|
|
children: full || parsed.flags.get('children') === true,
|
|
attachments: full || parsed.flags.get('attachments') === true,
|
|
relations: full || parsed.flags.get('relations') === true,
|
|
activity: full || parsed.flags.get('activity') === true
|
|
}
|
|
if (parsed.flags.has('depth') && !include.children) {
|
|
throw new RemoteCliArgumentError('invalid_argument', '--depth requires --children or --full')
|
|
}
|
|
const requestedDepth = optionalNonNegativeInteger(parsed.flags, 'depth')
|
|
if (requestedDepth !== undefined && requestedDepth > LINEAR_CHILDREN_MAX_DEPTH) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
`--depth must be at most ${LINEAR_CHILDREN_MAX_DEPTH}`
|
|
)
|
|
}
|
|
const workspaceId = optionalString(parsed.flags, 'workspace')
|
|
if (workspaceId === 'all') {
|
|
throw new RemoteCliArgumentError(
|
|
'linear_invalid_workspace',
|
|
'--workspace all is not valid for issue'
|
|
)
|
|
}
|
|
const input = optionalString(parsed.flags, 'id') ?? remotePositional(parsed, 2)
|
|
return {
|
|
input,
|
|
current: input ? false : parsed.flags.get('current') === true,
|
|
workspaceId,
|
|
include,
|
|
depth: clampLinearIssueDepth(requestedDepth),
|
|
context: {
|
|
remote: true,
|
|
...(env.ORCA_WORKTREE_ID ? { worktreeId: env.ORCA_WORKTREE_ID } : {}),
|
|
...(env.ORCA_TERMINAL_HANDLE ? { terminalHandle: env.ORCA_TERMINAL_HANDLE } : {})
|
|
}
|
|
}
|
|
}
|
|
|
|
async function call(
|
|
dispatcher: RpcDispatcher,
|
|
method: string,
|
|
params?: Record<string, unknown>
|
|
): Promise<RpcResponse> {
|
|
return await dispatcher.dispatch({
|
|
id: `remote-cli-${Date.now()}`,
|
|
authToken: 'remote-cli',
|
|
method,
|
|
params
|
|
})
|
|
}
|
|
|
|
function requiredString(flags: Map<string, string | boolean>, name: string): string {
|
|
const value = optionalString(flags, name)
|
|
if (!value) {
|
|
throw new RemoteCliArgumentError('invalid_argument', `Missing --${name}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function optionalString(flags: Map<string, string | boolean>, name: string): string | undefined {
|
|
const value = flags.get(name)
|
|
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
}
|
|
|
|
function optionalNumber(flags: Map<string, string | boolean>, name: string): number | undefined {
|
|
const value = optionalString(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
const parsed = Number(value)
|
|
if (!Number.isFinite(parsed)) {
|
|
throw new RemoteCliArgumentError('invalid_argument', `Invalid numeric value for --${name}`)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function optionalPositiveInteger(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | undefined {
|
|
const value = optionalNumber(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
if (!Number.isInteger(value) || value <= 0) {
|
|
throw new RemoteCliArgumentError('invalid_argument', `Invalid positive integer for --${name}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function linearListFilter(
|
|
flags: Map<string, string | boolean>
|
|
): 'assigned' | 'created' | 'all' | 'completed' | 'open' | undefined {
|
|
const filter = optionalString(flags, 'filter')
|
|
if (filter === undefined) {
|
|
return undefined
|
|
}
|
|
if (['assigned', 'created', 'all', 'completed', 'open'].includes(filter)) {
|
|
return filter as 'assigned' | 'created' | 'all' | 'completed' | 'open'
|
|
}
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
'--filter must be assigned, created, all, completed, or open'
|
|
)
|
|
}
|
|
|
|
function optionalNonNegativeInteger(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | undefined {
|
|
const value = optionalNumber(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
if (!Number.isInteger(value) || value < 0) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
`Invalid non-negative integer for --${name}`
|
|
)
|
|
}
|
|
return value
|
|
}
|