Files
deepseek-harness/packages/host/directory-picker-browse/src/index.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

325 lines
14 KiB
TypeScript

/**
* Browse backend of the directory-picker seam: registers `ctx.directoryPicker`
* with the `browse` capability — one-level directory listing and child-directory
* creation over the host filesystem via Node's stdlib (which already carries
* the per-OS adaptation). Nothing renders on the host display, so this backend
* serves remote clients the dialog backend cannot. Policy decisions (hidden
* entries flagged but returned, symlinks followed, whole-filesystem scope) are
* recorded in the directory-picker seam Agent Note.
* @module @deepseek-ai/dsh-host-directory-picker-browse
*/
import { mkdir, opendir, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, dirname, join, posix, resolve, win32 } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import {
DirectoryPicker, DirectoryPickerError,
} from '@deepseek-ai/dsh-host-directory-picker'
import type {
DirectoryEntry, DirectoryListing, DirectoryPickerCapability,
} from '@deepseek-ai/dsh-host-directory-picker'
/**
* Ancestor chain from the filesystem root to `target` inclusive — the
* breadcrumb rows of a listing, every one a jump target.
*/
function ancestryCrumbs(target: string): DirectoryEntry[] {
const crumbs: DirectoryEntry[] = []
let current = target
for (;;) {
const parent = dirname(current)
// basename of a root is '' — label the root crumb by its full path ('/', 'C:\').
crumbs.unshift({ name: parent === current ? current : basename(current), path: current, hidden: false })
if (parent === current) return crumbs
current = parent
}
}
/**
* True when the path names one fixed filesystem location regardless of
* process state: POSIX-absolute on POSIX; on Windows only drive-qualified
* (`C:\…`) or complete UNC (`\\server\share…`) forms. Rooted drive-less
* forms (`\foo`, `/foo`) and incomplete UNC prefixes (`\\`, `\\server`)
* pass `isAbsolute` yet still resolve against the process's current drive.
* @param path - candidate path.
* @param platform - replaces `process.platform` for deterministic tests.
* @returns whether the path is fully qualified on the platform.
*/
export function fullyQualified(path: string, platform: NodeJS.Platform = process.platform): boolean {
return platform === 'win32'
? win32.isAbsolute(path) && /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)/.test(path)
: posix.isAbsolute(path)
}
/** One streamed listing candidate: the dirent facts a row needs, nothing else retained. */
export interface ListingCandidate {
/** Base name within the streamed level. */
name: string
/** Dirent says directory (no probe needed). */
isDirectory: boolean
/** Dirent says symlink (enterability needs a stat probe). */
isSymbolicLink: boolean
}
/**
* Insert a streamed candidate into the name-sorted bounded window, evicting
* the name-largest candidate when the window exceeds `keep`. Memory over an
* arbitrarily large level therefore stays O(keep) regardless of how many
* children the directory holds.
* @param window - the name-ascending window, mutated in place.
* @param candidate - the streamed candidate to place.
* @param keep - the window bound.
* @returns true when an eviction happened (the level has candidates beyond the window).
*/
export function boundedInsert(window: ListingCandidate[], candidate: ListingCandidate, keep: number): boolean {
// Full window, name at or beyond the tail: one comparison rejects, so an
// oversized level costs O(1) per candidate past the head instead of a
// window scan (100k children against a 1,001 window must not approach
// 10^8 comparisons).
// oxlint-disable-next-line typescript/no-non-null-assertion -- a full window (length === keep >= 1) has a tail
if (window.length === keep && candidate.name.localeCompare(window[window.length - 1]!.name) >= 0) return true
// Binary insertion keeps a retained candidate at O(log keep) comparisons.
let lo = 0
let hi = window.length
while (lo < hi) {
const mid = (lo + hi) >>> 1
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
if (candidate.name.localeCompare(window[mid]!.name) < 0) hi = mid
else lo = mid + 1
}
window.splice(lo, 0, candidate)
if (window.length <= keep) return false
window.pop()
return true
}
/**
* Await `operation`, but reject with the signal's reason the moment it
* aborts. Node's filesystem reads are not retractable, so the operation
* itself keeps running against a handle the caller then closes — its late
* settlement is swallowed here so an abandoned read cannot surface as an
* unhandled rejection.
* @param operation - the in-flight filesystem step.
* @param signal - caller lifetime; absent means plain awaiting.
* @returns the operation's value.
*/
export function raceAbort<T>(operation: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
if (signal === undefined) return operation
return new Promise<T>((resolve, reject) => {
const onAbort = (): void => {
operation.catch(() => {
// Abandoned read: its handle is being closed by the aborting caller,
// and the abort reason already carried the outcome.
})
reject(asError(signal.reason))
}
if (signal.aborted) {
onAbort()
return
}
signal.addEventListener('abort', onAbort, { once: true })
operation.then(
(value) => {
signal.removeEventListener('abort', onAbort)
resolve(value)
},
(reason: unknown) => {
signal.removeEventListener('abort', onAbort)
reject(asError(reason))
},
)
})
}
/** The thrown value as an Error (wire/abort reasons may be anything). */
function asError(reason: unknown): Error {
return reason instanceof Error ? reason : new Error(String(reason))
}
/* v8 ignore start -- a close failure of an abandoned handle has no consumer, and forcing one needs a filesystem torn down mid-request. */
/** Swallow the close failure of a handle its caller already departed. */
function swallowCloseFailure(): void {}
/* v8 ignore stop */
/** Message text of an unknown thrown value. */
function messageOf(error: unknown): string {
/* v8 ignore next -- node:fs rejects with Error instances; the String arm only satisfies the unknown narrowing. */
return error instanceof Error ? error.message : String(error)
}
/**
* One listing row for a dirent, following symlinks to directories; null for
* non-directories and broken/cyclic links (skipped silently — the browser
* shows what can be entered, and a broken link cannot).
*/
async function directoryRow(
parent: string, name: string, isDirectory: boolean, isSymbolicLink: boolean, signal: AbortSignal | undefined,
): Promise<DirectoryEntry | null> {
const path = join(parent, name)
let enterable = isDirectory
if (!enterable && isSymbolicLink) {
try {
// The probe races the caller too: a symlink target on a stalled
// network filesystem must not keep a departed caller's request alive.
enterable = (await raceAbort(stat(path), signal)).isDirectory()
} catch {
/* v8 ignore next 2 -- an abort landing mid-probe needs a stalled stat; the per-candidate check in list covers the settled path. */
if (signal?.aborted) throw asError(signal.reason)
// Broken or cyclic symlink: stat is the probe, failure means "not enterable".
return null
}
}
if (!enterable) return null
// POSIX hidden convention; Windows' hidden attribute is not exposed by
// dirents (Known Limitations). The client owns whether hidden rows show.
return { name, path, hidden: name.startsWith('.') }
}
/** Validated plugin configuration. */
export interface Config {
/** Complete-result bound of one listing level; see {@link BrowseDirectoryPicker.Config}. */
maxEntries: number
}
/** The `ctx.directoryPicker` browse implementation (stable capability object per service life). */
export default class BrowseDirectoryPicker extends DirectoryPicker {
/**
* `maxEntries` bounds the complete listing level a single `list` call may
* materialize and put on the wire: at most this many child-directory rows
* (hidden rows included), with `truncated` flagging a cut level. The
* default follows GitHub's web UI, which truncates directory listings at
* 1,000 entries.
*/
static Config: z<Config> = z.object({
maxEntries: z.natural().min(1).default(1000),
})
private readonly browseCapability: DirectoryPickerCapability = {
kind: 'browse',
list: (path, signal) => this.list(path, signal),
createDirectory: (path, name) => this.createDirectory(path, name),
}
constructor(ctx: Context, private readonly config: Config) {
super(ctx)
}
/**
* The browse interaction capability.
* @returns the stable `browse` capability object.
*/
capability(): DirectoryPickerCapability {
return this.browseCapability
}
private async list(path?: string, signal?: AbortSignal): Promise<DirectoryListing> {
const home = homedir()
// The seam contract takes fully qualified paths only; resolve() would
// silently rebase a relative or empty wire value under the host process
// cwd (or, for rooted drive-less Windows forms, its current drive).
if (path !== undefined && !fullyQualified(path)) {
throw new DirectoryPickerError('directory-unreadable', path, `cannot list "${path}": not a fully qualified path`)
}
const target = resolve(path ?? home)
// Stream the level (opendir, one dirent at a time) into a name-sorted
// window of maxEntries + 1 candidates: memory stays bounded no matter how
// many children the directory holds, the window keeps the name-sorted
// head, and the +1 slot lets an in-window extra row prove the cut. A
// window candidate that turns out non-enterable (broken symlink) is not
// backfilled from beyond the window — an eviction already marks the
// level truncated, which stays the honest answer.
const keep = this.config.maxEntries + 1
const window: ListingCandidate[] = []
let evicted = false
try {
// Every filesystem await races the caller's signal: a stalled
// opendir/read on a network filesystem must not keep a departed
// caller's scan alive, and an already-aborted request rejects even
// when the level is empty.
const opening = opendir(target)
const level = await raceAbort(opening, signal).catch((error: unknown) => {
// The abandoned open can still mint a handle after the abort won;
// close it so a departed caller cannot leak a descriptor. (A lost
// race against opendir's own rejection has nothing to close, and
// the close's own failure is swallowed — the request already
// returned, so a cleanup error has no consumer.)
void opening.then(dir => dir.close().catch(swallowCloseFailure), () => {
// Already rejected: raceAbort surfaced or swallowed it.
})
throw error
})
try {
for (;;) {
const dirent = await raceAbort(level.read(), signal)
if (dirent === null) break
// Only rows a browser could enter contend for the window; dirent
// says "directory" outright, a symlink needs the later stat probe.
if (!dirent.isDirectory() && !dirent.isSymbolicLink()) continue
const candidate = { name: dirent.name, isDirectory: dirent.isDirectory(), isSymbolicLink: dirent.isSymbolicLink() }
if (boundedInsert(window, candidate, keep)) evicted = true
}
} finally {
// Manual read() never auto-closes; close on every exit. The aborted
// exit must not await it — Node queues close behind any in-flight
// read, so awaiting would chain the departed caller back onto the
// very stall the abort escaped (the abandoned read's settlement is
// already swallowed by raceAbort).
const closing = level.close()
/* v8 ignore next 3 -- an abort between open and close needs a stalled read; the abandoned-close arm has no observable outcome. */
if (signal?.aborted) {
closing.catch(swallowCloseFailure)
} else {
await closing
}
}
} catch (error: unknown) {
// An abort is the caller's own reason, not an unreadable directory.
signal?.throwIfAborted()
throw new DirectoryPickerError('directory-unreadable', target, `cannot list ${target}: ${messageOf(error)}`)
}
const entries: DirectoryEntry[] = []
let truncated = evicted
for (const candidate of window) {
// A caller that departed between reads and probes stops before the
// next probe (each probe's own await is raced inside directoryRow).
signal?.throwIfAborted()
const row = await directoryRow(target, candidate.name, candidate.isDirectory, candidate.isSymbolicLink, signal)
if (row === null) continue
if (entries.length === this.config.maxEntries) {
truncated = true
break
}
entries.push(row)
}
return { path: target, home, crumbs: ancestryCrumbs(target), entries, truncated }
}
private async createDirectory(path: string, name: string): Promise<string> {
// Same fully-qualified fence as list: never rebase a parent under the
// cwd or the current drive.
if (!fullyQualified(path)) {
throw new DirectoryPickerError('directory-create-failed', path, `cannot create under "${path}": not a fully qualified parent path`)
}
const parent = resolve(path)
// The backend owns segment validation (the wire schema also refuses these,
// but direct service consumers must hit the same fence).
if (name.trim() === '' || name === '.' || name === '..' || /[/\\]/.test(name)) {
throw new DirectoryPickerError('directory-create-failed', join(parent, name), `"${name}" is not a single path segment`)
}
const target = join(parent, name)
try {
// Non-recursive: the parent is the directory the browser is showing, so
// a missing parent is a real failure, not a level to invent.
await mkdir(target)
return target
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
throw new DirectoryPickerError('directory-exists', target, `${target} already exists`)
}
throw new DirectoryPickerError('directory-create-failed', target, `cannot create ${target}: ${messageOf(error)}`)
}
}
}