Explorar el Código

feat(util): add workspace path helpers

imccyu hace 2 semanas
padre
commit
64bb0427f9

+ 2 - 2
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

+ 1 - 0
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))

+ 1 - 0
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))

+ 2 - 2
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

+ 1 - 0
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 |

+ 1 - 0
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 路径与展示辅助函数 |

+ 6 - 0
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

+ 10 - 0
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.

+ 10 - 0
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 主目录路径等价关系。

+ 42 - 0
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:^"
+  }
+}

+ 48 - 0
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() ?? ''
+}

+ 27 - 0
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 */

+ 34 - 0
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('')
+  })
+})

+ 15 - 0
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"
+    }
+  ]
+}

+ 1 - 0
scripts/verify-package-readme-model-experience.ts

@@ -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 - 0
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" },