fix(windows): canonicalize native watch paths
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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/paths/README.md
|
||||
README.md: 2b3272e019ef2f37386da9156b06a5c151836d8c
|
||||
README.zh.md: 7fe0ec04117ae439ade653cefd1c8f5da094d8fd
|
||||
README.md: 8d10ed855a37f1205f87420b3f36f45b10e65bd3
|
||||
README.zh.md: ed3ca377bd48252fe0ef3f95186dc6eb1fb6e6a0
|
||||
@@ -18,9 +18,13 @@ Shared filesystem path helpers for DeepSeek Harness user data.
|
||||
|
||||
`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.
|
||||
- **Helpers do not touch the filesystem** — callers still own directory creation, existence checks, permissions, and trust policy for the resulting path.
|
||||
- **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.
|
||||
@@ -18,9 +18,13 @@ DeepSeek Harness 用户数据的共享文件系统路径辅助工具。
|
||||
|
||||
`expandHomePath()` 使用操作系统主目录展开 `~`、`~/...` 和 Windows 风格的 `~\...` 前缀。它会保留非波浪号路径和 `~user/...` 原样不变。
|
||||
|
||||
## 监听路径
|
||||
|
||||
`canonicalizeWatchPath()` 为原生文件系统 watcher 提供一种稳定的目标路径表示。它通过 `fs.realpath()` 解析层级最深的现有祖先路径,再拼回缺失的后缀,因此即使文件或目录尚未创建也仍可监听。尤其是,Windows 8.3 别名不能与原生 watcher 后端发出的长路径混用。
|
||||
|
||||
该包刻意保持规模小且不依赖 harness,以便产品包共享用户数据路径约定,而不必彼此依赖。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **展开范围刻意保持狭窄**:只有单独的 `~`、`~/...` 和 `~\...` 使用当前操作系统主目录;`~alice/...` 等指定用户的形式、环境变量和 shell 表达式保持不变。
|
||||
- **辅助工具不会操作文件系统**:调用方仍负责目录创建、存在性检查、权限,以及对结果路径应用信任策略。
|
||||
- **规范化会读取,但绝不修改**:`canonicalizeWatchPath()` 会执行 `realpath` 探测,并传播除路径不存在以外的错误;调用方仍负责目录创建、权限,以及对结果路径应用信任策略。
|
||||
@@ -4,8 +4,9 @@
|
||||
* @module @deepseek-ai/dsh-paths
|
||||
*/
|
||||
|
||||
import { realpath } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
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'
|
||||
@@ -16,6 +17,33 @@ 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}; the missing suffix is then restored.
|
||||
* This prevents Windows 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.
|
||||
*/
|
||||
export async function canonicalizeWatchPath(path: string): Promise<string> {
|
||||
let current = resolve(path)
|
||||
const missing: string[] = []
|
||||
while (true) {
|
||||
try {
|
||||
return join(await realpath(current), ...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.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { homedir } from 'node:os'
|
||||
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,
|
||||
@@ -53,4 +55,22 @@ describe('dsh path helpers', () => {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user