@sema-agent/core
Version:
Stateless, task-oriented AI agent core
157 lines (156 loc) • 11 kB
TypeScript
/**
* 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;