diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 775907aed0..1230b31702 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f255e38fdb..2fa4a59d00 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -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)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index f1f6774d95..c2d64b1a5c 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -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)) diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml index 04314b33ef..92e1899a9f 100644 --- a/packages/util/README.i18n.yaml +++ b/packages/util/README.i18n.yaml @@ -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 diff --git a/packages/util/README.md b/packages/util/README.md index 7867f6c235..a4bdee0749 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -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 | diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md index 33ca9912f3..247c60dd9d 100644 --- a/packages/util/README.zh.md +++ b/packages/util/README.zh.md @@ -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 路径与展示辅助函数 | diff --git a/packages/util/workspace-path/README.i18n.yaml b/packages/util/workspace-path/README.i18n.yaml new file mode 100644 index 0000000000..31d6dc2d2f --- /dev/null +++ b/packages/util/workspace-path/README.i18n.yaml @@ -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 diff --git a/packages/util/workspace-path/README.md b/packages/util/workspace-path/README.md new file mode 100644 index 0000000000..2777a3d323 --- /dev/null +++ b/packages/util/workspace-path/README.md @@ -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. diff --git a/packages/util/workspace-path/README.zh.md b/packages/util/workspace-path/README.zh.md new file mode 100644 index 0000000000..708c0e7c36 --- /dev/null +++ b/packages/util/workspace-path/README.zh.md @@ -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 主目录路径等价关系。 diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json new file mode 100644 index 0000000000..c1572fe690 --- /dev/null +++ b/packages/util/workspace-path/package.json @@ -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:^" + } +} diff --git a/packages/util/workspace-path/src/index.ts b/packages/util/workspace-path/src/index.ts new file mode 100644 index 0000000000..9b9f559337 --- /dev/null +++ b/packages/util/workspace-path/src/index.ts @@ -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() ?? '' +} diff --git a/packages/util/workspace-path/src/invariant.ts b/packages/util/workspace-path/src/invariant.ts new file mode 100644 index 0000000000..7cac9479dd --- /dev/null +++ b/packages/util/workspace-path/src/invariant.ts @@ -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 */ diff --git a/packages/util/workspace-path/tests/index.spec.ts b/packages/util/workspace-path/tests/index.spec.ts new file mode 100644 index 0000000000..7dccbe4530 --- /dev/null +++ b/packages/util/workspace-path/tests/index.spec.ts @@ -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('') + }) +}) diff --git a/packages/util/workspace-path/tsconfig.json b/packages/util/workspace-path/tsconfig.json new file mode 100644 index 0000000000..779effc3cc --- /dev/null +++ b/packages/util/workspace-path/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index c6cabd41e6..1fb184af82 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -34,6 +34,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly> = { '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.', } /** diff --git a/tsconfig.host.json b/tsconfig.host.json index 35258110d5..b64992cdf1 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -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" },