UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

157 lines (156 loc) • 11 kB
/** * THE WORKING-DIRECTORY THREAD — where a command stands at each of its segments, and what `PWD` / `OLDPWD` hold there. * ONE walk (`shell-read-plan.ts` drives it, segment by segment, beside the binding thread) over a CLOSED word set that * moves the base ({@link BaseShift}): `cd` in its forms — a bare relative target searched through `CDPATH` first, `cd -` * going to `OLDPWD`, a bare `cd` to `HOME`, `-P` / `set -P` leaving `PWD` a spelling the text does not hold — `pushd` / * `popd` with the stack the text pushed, and a literal `eval` text read as the text it is, in this shell. Any other text * that may move the base where the plan cannot follow it (`source`, a non-literal `eval`, a computed program word, a * call into a function body or a code body that may move, an option the stack reading does not follow) LOSES it: what * stands after it cannot be placed. A move inside a subshell, a pipeline member or a function body nobody called is read * as a move too — a candidate SET, more places, never fewer (a move may also fail: the unmoved place stays a candidate * unless an `&&` run proves the move ran). */ import type { Bare } from "./shell-read-argv.js"; import type { BindingTable, WorkingDirectoryState } from "./shell-read-bindings.js"; import type { Read } from "./shell-read-word.js"; import { type LexedWord } from "../../shell/lex.js"; import { type FunctionCall } from "./shell-read-functions.js"; /** A working directory the shell could be standing in, with the latest move this candidate assumes did NOT run * (`skippedCd`), and what the shell holds there: `pwd` — what `PWD` spells (absent: not the base's spelling — a physical * move, or a start the plan does not place), `old` — what `OLDPWD` holds (absent: what the shell inherited, unknown), * `stack` — the directories the text pushed and has not popped, top first (absent: the text changed the stack in a way * this walk does not follow; what lies below the text's own entries is the shell's, unknown). */ export interface BaseCandidate { readonly base: string; readonly skippedCd: number; readonly pwd?: string; readonly old?: string; readonly dirStack?: readonly string[]; } /** How one segment moves the base — the closed word set. `cd`: a `cd` (`push`: a `pushd DIR`) to a target, the * declared-home spelling of an unsettled target for the deny floor; `swap`: `pushd` with no directory; `pop`: `popd`; * `stack`: the stack changes and the base does not (`pushd -n`, `popd -n`, `dirs -c`); `eval`: a literal `eval` text; * `lost`: a move this walk does not follow. */ export type BaseShift = { readonly kind: "cd"; readonly target: Bare | "home" | "previous"; readonly physical: boolean; readonly push?: true; } | { readonly kind: "swap" | "pop" | "stack" | "lost"; } | { readonly kind: "eval"; readonly text: string; }; /** What the walk asks of the plan around it: the declared home (a `~` target), whether the text binds a name anywhere * (`OLDPWD` — then `cd -` reads the table's value; `PWD` — then a move's `OLDPWD` is not the base the candidate stood * in), and where a literal `eval` text started at a candidate ends ({@link WorkdirThread.end}). */ export interface WorkdirContext { readonly homeDir?: string; readonly textBinds: (name: string) => boolean; readonly evalEnd: (text: string, from: BaseCandidate, table: BindingTable) => { readonly end: readonly BaseCandidate[] | "unmodelled"; readonly physical: boolean; }; } /** One segment's move as the walk applies it: the shift, whether an `&&` run proves it ran, the first segment of the * reader's `&&` run, the names there, and whether `cdable_vars` may be on (a bare name may be a variable's value). */ export interface ShiftAt { readonly shift: BaseShift; readonly provable: boolean; readonly andRunStart: number; readonly table: CommandTable; readonly cdable: boolean; } /** The names at a `cd` segment with the command's own prefix assignments in force (`HOME=/ cd`), and which names those are. */ export interface CommandTable extends BindingTable { readonly commandSets: ReadonlySet<string>; } export declare class WorkdirThread { /** The start is where the shell stands (not a placeholder a nested plan is re-based from): `PWD` reads it. */ private readonly readable; private candidates; private lostAt; /** The reachable bases at the moment the walk lost track — the deny floor of a word it cannot place. */ private floors; /** `set -P` (or `-o physical`) may be on — a text it began in, an `eval` text that set it — : a later move leaves `PWD` a physical * spelling the text does not hold, and a `..` in a target resolves links the plan cannot see. */ private physical; /** The last segment whose move was applied or lost track ({@link WorkingDirectoryState.movedAt}). */ private movedAt; constructor(start: readonly BaseCandidate[] | readonly string[] | "unmodelled", /** The start is where the shell stands (not a placeholder a nested plan is re-based from): `PWD` reads it. */ readable: boolean, /** The physical mode may already be on where the text starts (an `eval` text run in a shell that set it). */ startsPhysical?: boolean); get lost(): boolean; get floorBases(): readonly string[] | undefined; /** The candidates a reader whose `&&` run starts at `andRunStart` may stand in. */ reachable(andRunStart: number): readonly BaseCandidate[]; /** Where the text ends: every candidate, or `unmodelled`. */ end(): readonly BaseCandidate[] | "unmodelled"; /** What `PWD` / `OLDPWD` hold for a reader whose `&&` run starts at `andRunStart`. */ where(andRunStart: number): WorkingDirectoryState; /** `set -P` / `set -o physical`: from here on a move's `PWD` is a physical spelling. */ setPhysical(): void; /** Where the text ends the physical mode may be on (an `eval` text hands it back to the shell that ran it). */ get mayBePhysical(): boolean; /** Stop placing anything from segment `at` on: the reachable bases (and a target's floor spellings) become the deny floor. */ lose(at: number, reachable: readonly BaseCandidate[], floorTargets?: readonly string[]): void; /** Apply segment `si`'s move. */ shift(si: number, at: ShiftAt, ctx: WorkdirContext): void; /** Where one candidate goes: the places (the CDPATH entries a bare target is searched through, then the target itself), * each with the `PWD`, `OLDPWD` and stack the move leaves — or `lost`. */ private movedFrom; } /** Does bash search `CDPATH` for this `cd` target: not an absolute one, not `.` / `..`, not one starting `./` / `../`? */ export declare function searchesCdPath(dir: string): boolean; /** The base shift of a `cd` / `pushd` / `popd` / `dirs` segment, read off its arguments as the program receives them (and * the move the plan reports for it: a `cd` target, or `unmodelled` for a stack move it cannot name), or `undefined` for * another program. `targetOf` turns the target word into its candidate (and records it as a read where the caller does). */ export declare function baseShiftOf(program: string, args: readonly Read[], targetOf: (r: Read) => Bare): { shift: BaseShift; reported?: Bare | "home" | "previous" | "unmodelled"; } | undefined; /** May this text move the base of the shell that runs it: a program of the closed set or one running text in this shell, * a computed program word, a call into one of `functions` — or text the lexer cannot read? */ export declare function textMayMove(text: string, functions: ReadonlySet<string>): boolean; /** Does this text turn the physical mode on in the shell that runs it? An `eval` text that does is no text to skip. */ export declare function textSetsPhysical(text: string): boolean; /** The table a `cd` / `pushd` reads `HOME`, `CDPATH` and `OLDPWD` from: the segment's own, with each prefix assignment in * front of the builtin (`HOME=/ cd`, `CDPATH=/ cd usr`) holding its value there — it is in the builtin's environment. A * prefix value naming an earlier prefix's name (`HOME=/ CDPATH=$HOME cd usr`) reads it as bash 5 does (the earlier * assignment applied) and as bash 3.2 does (the value before the command): both. */ export declare function withCommandEnvironment(table: BindingTable, argv: readonly LexedWord[]): CommandTable; /** Does this text call one of `functions` (a program word naming one) — or is it text the lexer cannot read? */ export declare function textCalls(text: string, functions: ReadonlySet<string>): boolean; /** May a segment with these words move the base ({@link textMayMove})? */ export declare function segmentMayMove(argv: readonly LexedWord[], functions: ReadonlySet<string>): boolean; /** What the walk reads of a whole text before it walks it: the text's functions (the names a program word can be one of; * whether it IS one at a segment is the binding thread's answer, `callAt`); per segment whether it may move the base * ({@link segmentMayMove}), may call a function, runs code bodies later (`trap`, `alias`, …), turns on the physical mode * (`set -P`, `set -o physical`); whether a function body may move the base; where `cdable_vars` goes on. */ export interface TextMoves { readonly functions: ReadonlySet<string>; /** The names whose bodies the plan has not read (an `eval` text defined them). */ readonly unread: ReadonlySet<string>; readonly mayMove: readonly boolean[]; /** Segment `si` may call a function (read before the walk, to bound where bodies run). */ readonly mayCallAt: readonly boolean[]; /** Segment `si` may run a function body: it may call one, hands a code body to run later, or runs text the plan does not read. */ readonly runsBodyAt: readonly boolean[]; readonly bodiesAt: readonly (readonly string[])[]; readonly physicalAt: readonly boolean[]; readonly cdableFrom: number; readonly bodyMoves: boolean; /** The function segment `si`'s program word names where the walk stands (`table`) — {@link functionCallOf}. */ callAt(si: number, table: BindingTable): FunctionCall | undefined; /** The segment may call a function a literal `eval` text defined: its body is not read — it may move the base and bind any name. */ callsUnread(si: number, table: BindingTable): boolean; /** The segment runs a body that may move the base where the walk cannot follow it: a call when a body may move or whose body is not read, a code body that may move or call one. */ losesBaseAt(si: number, table: BindingTable): boolean; } export declare function textMovesOf(defined: ReadonlySet<string>, argvs: readonly (readonly LexedWord[])[], inFunctionBody: (si: number) => boolean, inherited?: { readonly functions: ReadonlySet<string>; readonly unread: ReadonlySet<string>; }): TextMoves;