refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
6
packages/util/home-paths/README.i18n.yaml
Normal file
6
packages/util/home-paths/README.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/util/home-paths/README.md
|
||||
README.md: edcab2dcab6fa8957cde965ab8221df71dbe69f7
|
||||
README.zh.md: 842d8351f2165f759c56a1153b758577ff1fc0bb
|
||||
30
packages/util/home-paths/README.md
Normal file
30
packages/util/home-paths/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# dsh-home-paths
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shared filesystem path helpers for DeepSeek Harness user data.
|
||||
|
||||
## DSH home
|
||||
|
||||
`resolveDshHome()` resolves 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.
|
||||
|
||||
`dshHomePath(...segments)` joins child segments onto that resolved home with Node's platform path rules. With no segments it returns the home itself.
|
||||
|
||||
`dshHomeDisplay()` names an active root symbolically for user-facing paths: `~/.dsh` for the default home, `$DSH_HOME` for any configured home. It never leaks an absolute machine path.
|
||||
|
||||
`DSH_HOME_DIR_NAME` owns the default user-data directory name: `.dsh`.
|
||||
|
||||
`defaultDshHome()` returns the default DeepSeek Harness home by joining the operating-system home directory with `.dsh`, using Node's platform path rules.
|
||||
|
||||
`expandHomePath()` expands `~`, `~/...`, and Windows-style `~\...` prefixes against the operating-system home directory. It leaves non-tilde paths and `~user/...` untouched.
|
||||
|
||||
## Watch paths
|
||||
|
||||
`canonicalizeWatchPath()` gives a native filesystem watcher one stable spelling of its target. It resolves the deepest existing ancestor through `fs.realpath()` and restores any missing suffix, so a file or directory may still be watched before it is created. In particular, Windows 8.3 aliases cannot be mixed with the long paths emitted by the native watcher backend.
|
||||
|
||||
This package is intentionally small and harness-dep-free so product packages can share user-data path conventions without depending on one another.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Expansion is deliberately narrow** — only bare `~`, `~/...`, and `~\...` use the current operating-system home; named-user forms such as `~alice/...`, environment variables, and shell expressions remain unchanged.
|
||||
- **Canonicalization reads but never mutates** — `canonicalizeWatchPath()` performs `realpath` probes and propagates errors other than absence; callers still own directory creation, permissions, and trust policy for the resulting path.
|
||||
30
packages/util/home-paths/README.zh.md
Normal file
30
packages/util/home-paths/README.zh.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# dsh-home-paths
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
DeepSeek Harness 用户数据的共享文件系统路径辅助工具。
|
||||
|
||||
## DSH 主目录
|
||||
|
||||
`resolveDshHome()` 解析 DeepSeek Harness 的单根主目录。优先级从高到低为:显式配置的路径、`$DSH_HOME`、`~/.dsh`。harness 将所有用户数据保存在同一根目录下。
|
||||
|
||||
`dshHomePath(...segments)` 使用 Node 的平台路径规则,将子路径段拼接到解析后的主目录下。不传入任何路径段时,返回主目录本身。
|
||||
|
||||
`dshHomeDisplay()` 以符号方式表示当前根目录,用于面向用户的路径:默认主目录表示为 `~/.dsh`,任何已配置的主目录表示为 `$DSH_HOME`。它绝不会泄露机器的绝对路径。
|
||||
|
||||
`DSH_HOME_DIR_NAME` 定义默认用户数据目录名:`.dsh`。
|
||||
|
||||
`defaultDshHome()` 使用 Node 的平台路径规则,将操作系统主目录与 `.dsh` 拼接,并返回默认 DeepSeek Harness 主目录。
|
||||
|
||||
`expandHomePath()` 使用操作系统主目录展开 `~`、`~/...` 和 Windows 风格的 `~\...` 前缀。它会保留非波浪号路径和 `~user/...` 原样不变。
|
||||
|
||||
## 监听路径
|
||||
|
||||
`canonicalizeWatchPath()` 为原生文件系统 watcher 提供一种稳定的目标路径表示。它通过 `fs.realpath()` 解析层级最深的现有祖先路径,再拼回缺失的后缀,因此即使文件或目录尚未创建也仍可监听。尤其是,Windows 8.3 别名不能与原生 watcher 后端发出的长路径混用。
|
||||
|
||||
该包刻意保持规模小且不依赖 harness,以便产品包共享用户数据路径约定,而不必彼此依赖。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **展开范围刻意保持狭窄**:只有单独的 `~`、`~/...` 和 `~\...` 使用当前操作系统主目录;`~alice/...` 等指定用户的形式、环境变量和 shell 表达式保持不变。
|
||||
- **规范化会读取,但绝不修改**:`canonicalizeWatchPath()` 会执行 `realpath` 探测,并传播除路径不存在以外的错误;调用方仍负责目录创建、权限,以及对结果路径应用信任策略。
|
||||
42
packages/util/home-paths/package.json
Normal file
42
packages/util/home-paths/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-home-paths",
|
||||
"description": "Shared filesystem path helpers for the DeepSeek Harness",
|
||||
"version": "0.0.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/util/home-paths"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
112
packages/util/home-paths/src/index.ts
Normal file
112
packages/util/home-paths/src/index.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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'
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 {
|
||||
const fromEnv = env[DSH_HOME_ENV]
|
||||
const selected = configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())
|
||||
return resolve(expandHomePath(selected))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 resolvedHome === resolve(defaultDshHome()) ? DEFAULT_DSH_HOME_DISPLAY : `$${DSH_HOME_ENV}`
|
||||
}
|
||||
30
packages/util/home-paths/src/invariant.ts
Normal file
30
packages/util/home-paths/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-home-paths`.
|
||||
* @module @deepseek-ai/dsh-home-paths/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-home-paths'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'home-paths-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value
|
||||
* algebra is enforced by unit tests.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
76
packages/util/home-paths/tests/home-paths.spec.ts
Normal file
76
packages/util/home-paths/tests/home-paths.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
DEFAULT_DSH_HOME_DISPLAY,
|
||||
DSH_HOME_DIR_NAME,
|
||||
canonicalizeWatchPath,
|
||||
defaultDshHome,
|
||||
dshHomeDisplay,
|
||||
dshHomePath,
|
||||
expandHomePath,
|
||||
resolveDshHome,
|
||||
} from '@deepseek-ai/dsh-home-paths'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
describe('dsh path helpers', () => {
|
||||
it('owns the shared default DSH home directory name', () => {
|
||||
expect(DSH_HOME_DIR_NAME).toBe('.dsh')
|
||||
expect(DEFAULT_DSH_HOME_DISPLAY).toBe('~/.dsh')
|
||||
expect(defaultDshHome()).toBe(join(homedir(), '.dsh'))
|
||||
})
|
||||
|
||||
it('expands tilde paths without changing non-tilde paths', () => {
|
||||
expect(expandHomePath('~')).toBe(homedir())
|
||||
expect(expandHomePath('~/.dsh')).toBe(join(homedir(), '.dsh'))
|
||||
expect(expandHomePath('~\\.dsh')).toBe(join(homedir(), '.dsh'))
|
||||
expect(expandHomePath('/tmp/.dsh')).toBe('/tmp/.dsh')
|
||||
expect(expandHomePath('~other/.dsh')).toBe('~other/.dsh')
|
||||
})
|
||||
|
||||
it('resolves explicit path before DSH_HOME and the default', () => {
|
||||
const envHome = join(homedir(), 'env-dsh')
|
||||
|
||||
expect(resolveDshHome('/tmp/explicit-dsh', { DSH_HOME: '~/env-dsh' })).toBe(resolve('/tmp/explicit-dsh'))
|
||||
expect(resolveDshHome(undefined, { DSH_HOME: '~/env-dsh' })).toBe(envHome)
|
||||
expect(resolveDshHome(undefined, {})).toBe(defaultDshHome())
|
||||
})
|
||||
|
||||
it('treats an empty or whitespace-only DSH_HOME as unset', () => {
|
||||
expect(resolveDshHome(undefined, { DSH_HOME: '' })).toBe(defaultDshHome())
|
||||
expect(resolveDshHome(undefined, { DSH_HOME: ' ' })).toBe(defaultDshHome())
|
||||
})
|
||||
|
||||
it('joins child segments onto the resolved DSH_HOME', () => {
|
||||
vi.stubEnv('DSH_HOME', '~/env-dsh')
|
||||
expect(dshHomePath()).toBe(join(homedir(), 'env-dsh'))
|
||||
expect(dshHomePath('storages', 'cache')).toBe(join(homedir(), 'env-dsh', 'storages', 'cache'))
|
||||
})
|
||||
|
||||
it('labels a resolved home by whether it is the default root', () => {
|
||||
expect(dshHomeDisplay(resolve(defaultDshHome()))).toBe('~/.dsh')
|
||||
expect(dshHomeDisplay('/some/other/root')).toBe('$DSH_HOME')
|
||||
})
|
||||
|
||||
it('canonicalizes a watcher ancestor while preserving a missing suffix', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-watch-path-'))
|
||||
const target = join(root, 'target')
|
||||
const alias = join(root, 'alias')
|
||||
try {
|
||||
await mkdir(target)
|
||||
await symlink(target, alias, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
await expect(canonicalizeWatchPath(join(alias, 'later', 'config.yml'))).resolves.toBe(
|
||||
join(await realpath(target), 'later', 'config.yml'),
|
||||
)
|
||||
const file = join(root, 'file')
|
||||
await writeFile(file, 'not a directory')
|
||||
await expect(canonicalizeWatchPath(join(file, 'child'))).rejects.toMatchObject({ code: 'ENOTDIR' })
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
15
packages/util/home-paths/tsconfig.json
Normal file
15
packages/util/home-paths/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user