From ae0dfd100331180899dceefcd6410945f814abc7 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 19 Oct 2026 15:35:14 +0800 Subject: [PATCH] fix(fs-local): expand a leading ~ to the home directory Refs #6548 --- packages/fs/fs-local/README.i18n.yaml | 4 ++-- packages/fs/fs-local/README.md | 2 +- packages/fs/fs-local/README.zh.md | 2 +- packages/fs/fs-local/package.json | 1 + packages/fs/fs-local/src/fsio.ts | 7 +++++-- packages/fs/fs-local/tests/fsio.spec.ts | 8 +++++++- packages/fs/fs-local/tsconfig.json | 3 +++ pnpm-lock.yaml | 3 +++ 8 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/fs/fs-local/README.i18n.yaml b/packages/fs/fs-local/README.i18n.yaml index d5e1ee2008..e65bc696b0 100644 --- a/packages/fs/fs-local/README.i18n.yaml +++ b/packages/fs/fs-local/README.i18n.yaml @@ -21,8 +21,8 @@ en: 1bceab0cdb2e8f8e zh: 7d14d0c1ad90e9e2 /deepseek-ai-dsh-fs-local/use-this-package/minimal-configuration: - en: 60678d0c5fbf3c56 - zh: 1698a9645c12578b + en: 43a819d492ee1e64 + zh: 913c81168ea57cf4 /deepseek-ai-dsh-fs-local/use-this-package/what-you-can-do: en: cff37f0ccf4cff2a zh: d26ce57570880789 diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index 92d853bf86..0850d55692 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -33,7 +33,7 @@ Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs ### Minimal configuration -Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it. A relative base is anchored to the provider process working directory, and display paths remain absolute. On POSIX, resolution follows filesystem semantics before lexical normalization: `symlink/..` reaches the parent of the link target, including when the final file does not exist yet. Directory listings preserve the same physical traversal in displayed child paths. Windows retains native drive-relative normalization. +Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it. A leading `~`, `~/`, or `~\` names the host user's home directory. A relative base is anchored to the provider process working directory, and display paths remain absolute. On POSIX, resolution follows filesystem semantics before lexical normalization: `symlink/..` reaches the parent of the link target, including when the final file does not exist yet. Directory listings preserve the same physical traversal in displayed child paths. Windows retains native drive-relative normalization. ```yaml - name: '@deepseek-ai/dsh-fs-local' diff --git a/packages/fs/fs-local/README.zh.md b/packages/fs/fs-local/README.zh.md index e493ede1bc..fe6439db12 100644 --- a/packages/fs/fs-local/README.zh.md +++ b/packages/fs/fs-local/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### 最小配置 -用一个基础目录加载后端;相对路径基于它解析,绝对路径则忽略它。相对基础目录以提供方进程工作目录为起点,展示路径始终保持绝对路径。在 POSIX 上,解析先遵循文件系统语义,再进行词法规范化:`symlink/..` 到达链接目标的父目录,即使最终文件尚不存在也如此。目录列表中的子项展示路径保留同样的物理遍历语义。Windows 保留原生的驱动器相对路径规范化行为。 +用一个基础目录加载后端;相对路径基于它解析,绝对路径则忽略它。开头的 `~`、`~/` 或 `~\` 表示宿主用户的主目录。相对基础目录以提供方进程工作目录为起点,展示路径始终保持绝对路径。在 POSIX 上,解析先遵循文件系统语义,再进行词法规范化:`symlink/..` 到达链接目标的父目录,即使最终文件尚不存在也如此。目录列表中的子项展示路径保留同样的物理遍历语义。Windows 保留原生的驱动器相对路径规范化行为。 ```yaml - name: '@deepseek-ai/dsh-fs-local' diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index f4cf08b014..7228aeecfa 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -33,6 +33,7 @@ "dependencies": { "chokidar": "5.0.0", "koffi": "3.1.1", + "@deepseek-ai/dsh-home-paths": "workspace:*", "@deepseek-ai/schemastery": "workspace:~" }, "devDependencies": { diff --git a/packages/fs/fs-local/src/fsio.ts b/packages/fs/fs-local/src/fsio.ts index a9d97fd6ac..71d38c2422 100644 --- a/packages/fs/fs-local/src/fsio.ts +++ b/packages/fs/fs-local/src/fsio.ts @@ -12,6 +12,7 @@ import type { BigIntStats, Dirent, Stats } from 'node:fs' import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path' import { TextDecoder, promisify } from 'node:util' import { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' +import { expandHomePath } from '@deepseek-ai/dsh-home-paths' import { copyFileDaclWin32, replaceFileWin32 } from './win32.ts' const BINARY_SAMPLE_BYTES = 8192 @@ -138,11 +139,13 @@ export interface LocalDirEntry { /** * Anchor a path using native drive semantics and POSIX physical parent traversal. + * A leading `~`, `~/`, or `~\` names the host user's home directory, not a child of `cwd`. * @param cwd - provider base directory for relative paths. - * @param path - non-empty requested path. + * @param requested - non-empty requested path. * @returns absolute display spelling shared by target resolution and no-follow metadata. */ -export function localDisplayPath(cwd: string, path: string): string { +export function localDisplayPath(cwd: string, requested: string): string { + const path = expandHomePath(requested) const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}` const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}` const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path) diff --git a/packages/fs/fs-local/tests/fsio.spec.ts b/packages/fs/fs-local/tests/fsio.spec.ts index 40ed836309..0be899d4eb 100644 --- a/packages/fs/fs-local/tests/fsio.spec.ts +++ b/packages/fs/fs-local/tests/fsio.spec.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { chmod, mkdtemp, readFile, rename, rm, stat, symlink, unlink, writeFile, mkdir, readdir, realpath } from 'node:fs/promises' -import { tmpdir } from 'node:os' +import { homedir, tmpdir } from 'node:os' import { isAbsolute, join, parse, relative, resolve } from 'node:path' import { createServer } from 'node:net' import { @@ -89,6 +89,12 @@ describe('resolveLocalTarget', () => { expect(before.targetKey).toBe(after.targetKey) }) + it('resolves a leading tilde against the home directory instead of cwd', async () => { + expect((await resolveLocalTarget(dir, '~')).displayPath).toBe(homedir()) + expect((await resolveLocalTarget(dir, '~/dsh-missing.txt')).displayPath).toBe(join(homedir(), 'dsh-missing.txt')) + expect((await resolveLocalTarget(dir, 'a/~/b.txt')).displayPath).toBe(join(dir, 'a', '~', 'b.txt')) + }) + it('rejects a blank path', async () => { await expect(resolveLocalTarget(dir, ' ')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' }) }) diff --git a/packages/fs/fs-local/tsconfig.json b/packages/fs/fs-local/tsconfig.json index 3f14e43d35..c30747e9e9 100644 --- a/packages/fs/fs-local/tsconfig.json +++ b/packages/fs/fs-local/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "../fs" + }, + { + "path": "../../util/home-paths" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b99264282..722ec6e954 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10171,6 +10171,9 @@ importers: packages/fs/fs-local: dependencies: + '@deepseek-ai/dsh-home-paths': + specifier: workspace:* + version: link:../../util/home-paths '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery