feat(util): add workspace path helpers

This commit is contained in:
imccyu
2026-08-23 23:01:48 +08:00
parent 11b69490bf
commit 64bb0427f9
16 changed files with 202 additions and 4 deletions
+2 -2
View File
@@ -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 docs/config-catalog.md
config-catalog.md: f255e38fdbc3c5831a625510110bb8a52ea280ad
config-catalog.zh.md: f1f6774d957858e452f7120baeb78ef1712c6543
config-catalog.md: 2fa4a59d00867c4be168347a4e1dfd827ea443f4
config-catalog.zh.md: c2d64b1a5cef1fa20a37d954afc36f29cd3147c9
+1
View File
@@ -3375,4 +3375,5 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-typert-protocol` ([`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts))
- `@deepseek-ai/dsh-typert-registry` ([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts))
- `@deepseek-ai/dsh-util-crypto` ([`packages/util/crypto/src/index.ts`](../packages/util/crypto/src/index.ts))
- `@deepseek-ai/dsh-util-workspace-path` ([`packages/util/workspace-path/src/index.ts`](../packages/util/workspace-path/src/index.ts))
- `@deepseek-ai/dsh-win32-process` ([`packages/subprocess/win32-process/src/index.ts`](../packages/subprocess/win32-process/src/index.ts))
+1
View File
@@ -3376,4 +3376,5 @@ export interface Config {
- `@deepseek-ai/dsh-typert-protocol`[`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts)
- `@deepseek-ai/dsh-typert-registry`[`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts)
- `@deepseek-ai/dsh-util-crypto`[`packages/util/crypto/src/index.ts`](../packages/util/crypto/src/index.ts)
- `@deepseek-ai/dsh-util-workspace-path`[`packages/util/workspace-path/src/index.ts`](../packages/util/workspace-path/src/index.ts)
- `@deepseek-ai/dsh-win32-process`[`packages/subprocess/win32-process/src/index.ts`](../packages/subprocess/win32-process/src/index.ts)
+2 -2
View File
@@ -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/README.md
README.md: 7867f6c235dfd3cb063214381ac2f40ed0f26ce3
README.zh.md: 33ca9912f3670dfd3419b7a2a33f462cb4be95a3
README.md: a4bdee07492b268901a7bc03b416e58bde44e21b
README.zh.md: 247c60dd9dc61d390b39efb50b2d0804cebbe3b7
+1
View File
@@ -12,3 +12,4 @@ These zero-dependency packages provide small primitives shared by multiple capab
| [`retention/`](output-retention/README.md) | Bounds retained text and item collections |
| [`atomic-write/`](atomic-write/README.md) | Replaces files atomically |
| [`native-command/`](native-command/README.md) | Runs host-native commands without a shell |
| [`workspace-path/`](workspace-path/README.md) | Provides browser-safe Workspace path and display helpers |
+1
View File
@@ -12,3 +12,4 @@
| [`retention/`](output-retention/README.zh.md) | 限制保留文本和项集合的大小 |
| [`atomic-write/`](atomic-write/README.zh.md) | 以原子方式替换文件 |
| [`native-command/`](native-command/README.zh.md) | 不经 shell 运行宿主原生命令 |
| [`workspace-path/`](workspace-path/README.zh.md) | 提供浏览器可用的 Workspace 路径与展示辅助函数 |
@@ -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/workspace-path/README.md
README.md: 2777a3d32387a414aa1b2f406dc3a1f4900e7f43
README.zh.md: 708c0e7c367fd8420e3732ab4f356c1200e6bce1
+10
View File
@@ -0,0 +1,10 @@
# dsh-util-workspace-path
English | [中文](README.zh.md)
Browser-safe path helpers shared by Workspace-facing client and controller packages. The package joins Workspace-relative paths, abbreviates POSIX home directories for display, and derives Workspace titles from POSIX or Windows paths. It has no Cordis service or runtime state.
## Known Limitations and Deferred Work
- **Resolution is lexical** — it recognizes POSIX absolute paths, Windows drive paths, and UNC paths but does not access a filesystem or canonicalize `.` and `..` segments.
- **Home abbreviation is POSIX-only** — Windows paths remain unchanged because a portable browser cannot infer Windows home-path equivalence safely.
+10
View File
@@ -0,0 +1,10 @@
# dsh-util-workspace-path
[English](README.md) | 中文
供 Workspace 相关客户端和控制器包共享、可在浏览器使用的路径辅助函数。该包负责拼接 Workspace 相对路径、缩写用于展示的 POSIX 主目录,以及从 POSIX 或 Windows 路径提取 Workspace 标题;它不提供 Cordis service,也不持有运行时状态。
## 已知限制与暂缓事项
- **路径解析仅处理字面值**——它识别 POSIX 绝对路径、Windows 盘符路径和 UNC 路径,但不访问文件系统,也不规范化 `.``..` 路径段。
- **主目录缩写仅支持 POSIX**——Windows 路径保持不变,因为可移植浏览器无法安全推断 Windows 主目录路径等价关系。
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@deepseek-ai/dsh-util-workspace-path",
"description": "Browser-safe Workspace path and display helpers",
"version": "0.1.1-rc.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/util/workspace-path"
},
"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": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Browser-safe Workspace path and display helpers.
* @module @deepseek-ai/dsh-util-workspace-path
*/
/** Whether a path uses a Windows drive or UNC prefix. */
function isWindowsStylePath(value: string): boolean {
return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith('\\\\')
}
/**
* Resolve a Workspace-relative path into the Host-facing spelling used by path operations.
* @param cwd - Session Workspace root, when known.
* @param path - Absolute or Workspace-relative path.
* @returns an absolute path when a Workspace root is available, otherwise the original path.
*/
export function resolveWorkspacePath(cwd: string | undefined, path: string): string {
if (path.startsWith('/') || isWindowsStylePath(path)) return path
if (cwd === undefined || cwd === '') return path
const base = cwd.replace(/[/\\]+$/, '')
const relative = path.replace(/^[/\\]+/, '')
return `${base}/${relative}`
}
/**
* Abbreviate a POSIX home directory for display.
* @param path - Absolute or already-short display path.
* @param home - Host account home; absent skips abbreviation.
* @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
*/
export function abbreviateHomePath(path: string, home?: string): string {
if (home === undefined || home === '') return path
if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path
const root = home.replace(/\/+$/, '')
if (root === '' || root === '/') return path
if (path.replace(/\/+$/, '') === root) return '~'
if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`
return path
}
/**
* Read the final non-empty segment of a Workspace path for display.
* @param path - Workspace directory path using POSIX or Windows separators.
* @returns the final segment, or an empty string for a separator-only path.
*/
export function workspaceTitleOf(path: string): string {
return path.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? ''
}
@@ -0,0 +1,27 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-util-workspace-path`.
* @module @deepseek-ai/dsh-util-workspace-path/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-util-workspace-path'
/** Cordis companion plugin name. */
export const name = 'workspace-path-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: this utility owns no mutable runtime relationship. */
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 */
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import {
abbreviateHomePath, resolveWorkspacePath, workspaceTitleOf,
} from '@deepseek-ai/dsh-util-workspace-path'
describe('Workspace path helpers', () => {
it('resolves relative paths without changing absolute paths', () => {
expect(resolveWorkspacePath('/w', 'src/a.ts')).toBe('/w/src/a.ts')
expect(resolveWorkspacePath('/w/', '/abs/a.ts')).toBe('/abs/a.ts')
expect(resolveWorkspacePath(undefined, 'src/a.ts')).toBe('src/a.ts')
expect(resolveWorkspacePath('', 'src/a.ts')).toBe('src/a.ts')
expect(resolveWorkspacePath('/w', 'C:\\x\\a.ts')).toBe('C:\\x\\a.ts')
expect(resolveWorkspacePath('/w', '\\\\server\\share')).toBe('\\\\server\\share')
})
it('abbreviates only descendants of a POSIX home', () => {
expect(abbreviateHomePath('/Users/u', '/Users/u')).toBe('~')
expect(abbreviateHomePath('/Users/u/', '/Users/u')).toBe('~')
expect(abbreviateHomePath('/Users/u/Documents/project', '/Users/u')).toBe('~/Documents/project')
expect(abbreviateHomePath('/Users/u2/a.ts', '/Users/u')).toBe('/Users/u2/a.ts')
expect(abbreviateHomePath('/Users/u/a.ts')).toBe('/Users/u/a.ts')
expect(abbreviateHomePath('/Users/u/a.ts', '')).toBe('/Users/u/a.ts')
expect(abbreviateHomePath('/etc/hosts', '/')).toBe('/etc/hosts')
expect(abbreviateHomePath('C:\\Users\\u\\project', 'C:\\Users\\u')).toBe('C:\\Users\\u\\project')
expect(abbreviateHomePath('\\\\server\\share\\u', '\\\\server\\share\\u'))
.toBe('\\\\server\\share\\u')
})
it('reads the final path segment on both path styles', () => {
expect(workspaceTitleOf('/work/project/')).toBe('project')
expect(workspaceTitleOf('C:\\work\\project\\')).toBe('project')
expect(workspaceTitleOf('/')).toBe('')
})
})
@@ -0,0 +1,15 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../runtime-diagnostics/invariants"
}
]
}
@@ -34,6 +34,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
}
/**
+1
View File
@@ -131,6 +131,7 @@
{ "path": "./packages/util/home-paths" },
{ "path": "./packages/util/timeout" },
{ "path": "./packages/util/crypto" },
{ "path": "./packages/util/workspace-path" },
{ "path": "./packages/util/output-retention" },
{ "path": "./packages/util/atomic-write" },
{ "path": "./packages/attachment/attachment" },