Files
deepseek-harness/packages/util/home-paths/src/index.ts
T
Tianyi Cui 21d0c2e29c feat(agent-instructions): load the shared agents-root AGENTS.md
The user-global instruction scope now reads two roots: $DSH_HOME/AGENTS.md
first, then the shared agents root that other agent tools use
($DSH_AGENTS_HOME, else ~/.agents). Both resolve from the process environment
through @deepseek-ai/dsh-home-paths; no config field is added, because the
stack base (PR #5718) removed the per-row home override.

The two files are candidates of one candidate group: identical trimmed content
renders once from the harness home, an unobservable candidate preserves the
group's last-good state, and reconciliation probes both roots so external
edits, removals, and new files become visible change notices. A candidate
dropped as a duplicate keeps being reconciled, so deleting or changing the
retained candidate promotes it. The harness-home file keeps its recorded
`user-global` scope key; the shared file carries `agents-global`.

A user-global delta states `These user-global instructions apply to all work.`
instead of naming its reconciliation scope, and a project path whose first
component would collide with a user-global scope directory renders with a
leading `.` so it keeps its own scope.
2026-10-05 17:20:30 +08:00

198 lines
8.6 KiB
TypeScript

/**
* Shared filesystem path helpers for DeepSeek Harness user data.
*
* @module @deepseek-ai/dsh-home-paths
*/
import { opendir, realpath } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, dirname, join, resolve } from 'node:path'
/** Directory name for the default DeepSeek Harness home under the OS home. */
export const DSH_HOME_DIR_NAME = '.dsh'
/** Stable user-facing display form for the default DeepSeek Harness home. */
export const DEFAULT_DSH_HOME_DISPLAY = `~/${DSH_HOME_DIR_NAME}`
/** Environment variable that overrides the default DeepSeek Harness home. */
export const DSH_HOME_ENV = 'DSH_HOME'
/** Directory name for the default shared agent configuration root under the OS home. */
export const AGENTS_HOME_DIR_NAME = '.agents'
/** Stable user-facing display form for the default shared agent configuration root. */
export const DEFAULT_AGENTS_HOME_DISPLAY = `~/${AGENTS_HOME_DIR_NAME}`
/** Environment variable that overrides the default shared agent configuration root. */
export const AGENTS_HOME_ENV = 'DSH_AGENTS_HOME'
/**
* Give a native filesystem watcher one canonical spelling of a path, even
* when its final components do not exist yet. The deepest existing ancestor
* is resolved through {@link realpath}; when a suffix is missing, that
* ancestor is also proved to be an enumerable directory before the suffix is
* restored. This prevents Windows from treating a regular-file ancestor as
* ordinary absence, and prevents short-name aliases from being mixed with
* long paths emitted by the native watcher backend.
* @param path - Watch target or root, resolved against the current directory.
* @returns the target with its existing ancestor canonicalized.
* @throws when ancestor traversal encounters an error other than absence, or
* the existing ancestor of a missing suffix is not an enumerable directory.
*/
export async function canonicalizeWatchPath(path: string): Promise<string> {
let current = resolve(path)
const missing: string[] = []
while (true) {
try {
const canonical = await realpath(current)
if (missing.length > 0) {
// A Windows file-as-parent probe reports ENOENT. Opening the resolved
// ancestor preserves the cross-platform directory requirement.
const directory = await opendir(canonical)
await directory.close()
}
return join(canonical, ...missing.reverse())
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const parent = dirname(current)
/* v8 ignore next -- a filesystem root exists, so traversal resolves before this guard */
if (parent === current) throw error
missing.push(basename(current))
current = parent
}
}
}
/**
* Resolve the default DeepSeek Harness home using Node's platform path rules.
* @returns the absolute default harness home path.
*/
export function defaultDshHome(): string {
return join(homedir(), DSH_HOME_DIR_NAME)
}
/**
* Resolve the default shared agent configuration root using Node's platform path rules.
* @returns the absolute default shared agent configuration root.
*/
export function defaultAgentsHome(): string {
return join(homedir(), AGENTS_HOME_DIR_NAME)
}
/**
* Expand supported tilde prefixes against the operating-system home.
* @param path - configured path that may begin with `~`, `~/`, or `~\`.
* @returns the expanded path, or the original value when no supported prefix is present.
*/
export function expandHomePath(path: string): string {
if (path === '~') return homedir()
if (path.startsWith('~/') || path.startsWith('~\\')) return join(homedir(), path.slice(2))
return path
}
/**
* Resolve one user-data root from an explicit path, an environment override, and a default.
* @param configured - explicit path, which has highest precedence.
* @param env - environment mapping to read.
* @param envName - environment variable that overrides the default.
* @param fallback - default root, used when neither the explicit path nor the environment provides one.
* @returns the normalized absolute root.
*/
function resolveRoot(
configured: string | undefined,
env: Record<string, string | undefined>,
envName: string,
fallback: () => string,
): string {
const fromEnv = env[envName]
const selected = configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : fallback())
return resolve(expandHomePath(selected))
}
/**
* Label one resolved user-data root symbolically for user-facing display.
* @param resolvedRoot - absolute path returned by the matching resolver.
* @param defaultRoot - absolute default root whose symbolic label applies to a match.
* @param defaultLabel - symbolic label for the default root.
* @param envName - environment variable name that labels any other root.
* @returns the symbolic label.
*/
function rootDisplay(resolvedRoot: string, defaultRoot: string, defaultLabel: string, envName: string): string {
return resolvedRoot === resolve(defaultRoot) ? defaultLabel : `$${envName}`
}
/**
* Resolve the single-root DeepSeek Harness home.
*
* Precedence, highest first: an explicit configured path, `$DSH_HOME`, then
* `~/.dsh`. The harness keeps all user data under one root. An empty or
* whitespace-only `$DSH_HOME` is treated as unset, so a blank override never
* resolves the home to the current working directory.
* @param configured - explicit harness-home override, which has highest precedence.
* @param env - environment mapping used to read `DSH_HOME`.
* @returns the normalized absolute harness home path.
*/
export function resolveDshHome(configured?: string, env: Record<string, string | undefined> = process.env): string {
return resolveRoot(configured, env, DSH_HOME_ENV, defaultDshHome)
}
/**
* Resolve the shared agent configuration root.
*
* Precedence, highest first: an explicit configured path, `$DSH_AGENTS_HOME`,
* then `~/.agents`. The root holds configuration shared with other agent tools,
* so it stays independent of the DeepSeek Harness home. An empty or
* whitespace-only `$DSH_AGENTS_HOME` is treated as unset, so a blank override
* never resolves the root to the current working directory.
* @param configured - explicit shared-root override, which has highest precedence.
* @param env - environment mapping used to read `DSH_AGENTS_HOME`.
* @returns the normalized absolute shared agent configuration root.
*/
export function resolveAgentsHome(configured?: string, env: Record<string, string | undefined> = process.env): string {
return resolveRoot(configured, env, AGENTS_HOME_ENV, defaultAgentsHome)
}
/**
* Join path segments onto the resolved DeepSeek Harness home.
* @param segments - path segments appended to the Harness home; an empty list returns the home itself.
* @returns the normalized absolute joined path.
*/
export function dshHomePath(...segments: string[]): string {
return join(resolveDshHome(), ...segments)
}
/**
* Join path segments onto the resolved Harness home's `cache` directory without creating it; no arguments returns the directory itself.
* @param optionsOrSegment - explicit home override, or the first path segment; omission uses the default home resolution.
* @param segments - additional path segments after the first child, if any.
* @returns the normalized absolute cache path.
*/
export function dshCachePath(optionsOrSegment: { dshHome?: string } | string = {}, ...segments: string[]): string {
if (typeof optionsOrSegment === 'string') return dshHomePath('cache', optionsOrSegment, ...segments)
return join(resolveDshHome(optionsOrSegment.dshHome), 'cache', ...segments)
}
/**
* Describe a resolved harness home symbolically for user-facing display.
*
* It never returns an absolute machine path: the default home is labelled
* `~/.dsh`, and any configured home is labelled `$DSH_HOME`.
* @param resolvedHome - the absolute path returned by {@link resolveDshHome}.
* @returns `~/.dsh` for the default home, otherwise `$DSH_HOME`.
*/
export function dshHomeDisplay(resolvedHome: string): string {
return rootDisplay(resolvedHome, defaultDshHome(), DEFAULT_DSH_HOME_DISPLAY, DSH_HOME_ENV)
}
/**
* Describe a resolved shared agent configuration root symbolically for user-facing display.
*
* It never returns an absolute machine path: the default root is labelled
* `~/.agents`, and any configured root is labelled `$DSH_AGENTS_HOME`.
* @param resolvedHome - the absolute path returned by {@link resolveAgentsHome}.
* @returns `~/.agents` for the default root, otherwise `$DSH_AGENTS_HOME`.
*/
export function agentsHomeDisplay(resolvedHome: string): string {
return rootDisplay(resolvedHome, defaultAgentsHome(), DEFAULT_AGENTS_HOME_DISPLAY, AGENTS_HOME_ENV)
}