scripts/gen-website-api.ts renders website/zh-CN/api/{cordis,harness}/* and the
api-sidebar.json fragment the VitePress config imports, so pages and navigation
can never drift from the code: signatures, @param/@returns prose, dispatch
modes, and GitHub source links are extracted, never transcribed, and the
generator hard-errors on any rendered member missing docs. verify-website-api
(doc-sync + run-gates) is the freshness gate.
Replaces the hand-written zh api pages (7 pages covering 7 of 15 services,
with phantom APIs: Context.current/Context.events, agent/post-step, tool/call,
compact/*, llm/pre-request none of which exist) with generated English
references: 5 cordis pages, 15 per-service pages, and a 35-event catalog
grouped by scope. The hand-written hub api/index.md stays and now indexes the
full surface; zh for these pages arrives with the unified translation flow.
7.0 KiB
ctx.fs
FileSystem (abstract seam) — provided by @deepseek-ai/dsh-fs.
Abstract filesystem provider service. Subclass, implement the seven storage primitives, and load the subclass as a plugin — it registers as ctx.fs (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every backend must honor:
- resolve returns a stable FsTarget; the same underlying file reached by different input paths must yield the same
targetKeyso stale guards and target lookup agree across paths (e.g. through symlinks). - stat returns FsInfo metadata (never content) or
undefinedwhen the target is absent. - readText/streamText read the whole regular text file (the stream for large files); both own regular-file checks, UTF-8 decoding, binary/NUL rejection, and
FS_NOT_TEXT. - listDir returns direct children of a directory in stable name order with resolved child targets and cheap metadata only. It never reads file contents. Missing targets throw
FS_NOT_FOUND, non-directories throwFS_NOT_DIRECTORY, permission failures throwFS_PERMISSION_DENIED, and other backend I/O failures throwFS_IO_ERROR. - writeText is atomic temp-file + rename.
expectedis OPTIONAL: omit it for an unconditional create-or-overwrite (the bare-provider default), or supply a FsWriteIntent to guard the write. - editText verifies
expected.versionBEFORE literal matching (so a stale edit reportsFS_STALE_VERSION, notFS_EDIT_NOT_FOUND/FS_AMBIGUOUS_EDITagainst newer content), then applies literal replacement and writes atomically — all inside one mutation critical section.expectedis OPTIONAL: omit it for an unconditional edit of the current content (a missing target still reportsFS_STALE_VERSION).
ctx.fs.resolve(path, opts?)
abstract resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget>
Resolve a model/plugin-supplied path into a stable FsTarget. May perform I/O (a remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence async even though the local backend only normalizes + realpaths.
opts.cwd is the base directory a RELATIVE path resolves against; an absolute path ignores it. Omitted ⇒ the backend's own default base (the local backend uses its configured cwd). The CALLER supplies this — the seam does not read a session or agent — so a tool can resolve against the caller's per-session workspace (exec.agent.session.header.cwd) without the provider depending on dsh-agent/dsh-session. Mirrors how dsh-tool-bash defaults a bash workdir to the session cwd.
path— the path to resolve; relative paths resolve againstopts.cwd.opts—cwdoverrides the backend's default base for relative paths.
Returns the stable target; the same file yields the same targetKey.
ctx.fs.stat(target, signal?)
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
Return target metadata, or undefined when the target does not exist.
target— the resolved target to stat.signal— aborts the metadata round-trip.
Returns metadata only, never content; undefined for an absent target.
ctx.fs.readText(target, signal?)
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
Read the whole regular text file as a single decoded string.
target— the resolved target to read.signal— aborts the read.
Returns the full decoded UTF-8 content.
ctx.fs.streamText(target, signal?)
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
Stream the whole regular text file as decoded text chunks (same text semantics as readText, for large files). The backend owns cross-chunk UTF-8 decoding and binary rejection so the policy layer never touches raw bytes.
target— the resolved target to read.signal— aborts the stream, including between chunks.
Returns the chunk iterable, decoded and validated like {@link readText}.
ctx.fs.listDir(target, signal?)
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
List direct children of a directory in stable name order. Returns resolved child targets plus cheap metadata only; never reads file contents.
target— the resolved directory target.signal— aborts the listing.
Returns one entry per direct child, in stable name order.
ctx.fs.writeText(target, content, expected?, signal?)
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>
Create or fully replace a UTF-8 text file atomically. expected is the create-vs-replace decision and stale guard when supplied; OMITTING it is an unconditional create-or-overwrite (the bare provider — no version guard, no read-first requirement). Atomic either way.
target— the resolved target to write.content— the full new file content.expected— the write intent guarding the write; omit for unconditional.signal— aborts before the atomic rename takes effect.
Returns the outcome, including the version the write produced.
ctx.fs.editText(target, edit, expected?, signal?)
abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise<FsEditOutcome>
Apply a literal edit to an existing UTF-8 text file. When expected is supplied, verifies expected.version as the stale guard BEFORE literal matching; OMITTING it edits the current content unconditionally (no version guard). Either way applies the replacement and writes atomically — one mutation critical section — and a missing target reports FS_STALE_VERSION.
target— the resolved target to edit.edit— the literal search/replace request.expected— the version guard; omit for an unconditional edit.signal— aborts before the atomic rename takes effect.
Returns the outcome, including the version the edit produced.