From 0a3b2fc940c048e3e10c9045ac0bbec39cbbad2b Mon Sep 17 00:00:00 2001 From: ydflow <314143294+ydflow@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:38:26 +0800 Subject: [PATCH] fix(home): expand native Windows home-relative paths --- docs/guide/distribution.md | 2 ++ docs/guide/zh-CN/distribution.md | 2 ++ src/__tests__/home-normalize.test.ts | 45 ++++++++++++++++++++++++++-- src/utils/home.ts | 3 +- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/guide/distribution.md b/docs/guide/distribution.md index 37c44d659..3bb4e72f9 100644 --- a/docs/guide/distribution.md +++ b/docs/guide/distribution.md @@ -177,6 +177,8 @@ teamai projects set checkout teamai pull ``` +`repo.localPath` accepts home-relative paths such as `~/.teamai/team-repo`, including the native `~\.teamai\team-repo` form on Windows. + ## Restricting skills to some members Namespaces only filter for members who have set a role: a member with no `primaryRole` syncs everything, so `manifest/roles.yaml` alone restricts no one. `teamai init` makes new members pick a role; members from before the manifest need `teamai roles set ` once. Their next pull withdraws teamai-delivered copies outside the new namespaces (locally modified copies are kept). diff --git a/docs/guide/zh-CN/distribution.md b/docs/guide/zh-CN/distribution.md index ae17390cb..af03c2fb4 100644 --- a/docs/guide/zh-CN/distribution.md +++ b/docs/guide/zh-CN/distribution.md @@ -177,6 +177,8 @@ teamai projects set checkout teamai pull ``` +`repo.localPath` 支持 `~/.teamai/team-repo` 这样的用户目录相对路径,在 Windows 上也支持原生的 `~\.teamai\team-repo` 写法。 + ## 把技能限定给部分成员 Namespace 只对设置了角色的成员过滤:`primaryRole` 未设置的成员会同步全部技能,因此只有 `manifest/roles.yaml` 并不限定任何人。`teamai init` 会让新成员选择角色;manifest 之前入队的成员需要执行一次 `teamai roles set `。之后的 pull 会收回新 namespace 之外、由 teamai 下发的副本(本地改过的保留)。 diff --git a/src/__tests__/home-normalize.test.ts b/src/__tests__/home-normalize.test.ts index 5358dd02b..85628c91f 100644 --- a/src/__tests__/home-normalize.test.ts +++ b/src/__tests__/home-normalize.test.ts @@ -5,9 +5,10 @@ * the repo cache and the path-safety allowlist would all land somewhere the user * never looks. `normalizeHomePath` converts exactly that shape and nothing else. */ -import { afterEach, describe, expect, it } from 'vitest'; +import path from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; -import { normalizeHomePath } from '../utils/home.js'; +import { expandHome, normalizeHomePath } from '../utils/home.js'; const realPlatform = process.platform; @@ -15,7 +16,45 @@ function setPlatform(value: NodeJS.Platform): void { Object.defineProperty(process, 'platform', { value, configurable: true }); } -afterEach(() => setPlatform(realPlatform)); +afterEach(() => { + setPlatform(realPlatform); + vi.restoreAllMocks(); + vi.unstubAllEnvs(); +}); + +describe.each(['win32', 'linux'] as const)('expandHome on %s', (platform) => { + it('expands home paths before and after native path joining', () => { + setPlatform(platform); + const nativePath = platform === 'win32' ? path.win32 : path.posix; + const join = nativePath.join; + vi.spyOn(path, 'join').mockImplementation(join); + const home = platform === 'win32' ? 'C:\\Users\\alice' : '/home/alice'; + vi.stubEnv('HOME', home); + + expect(expandHome('~')).toBe(home); + expect(expandHome('~/.teamai/team-repo')).toBe(nativePath.join(home, '.teamai', 'team-repo')); + expect(expandHome(nativePath.join('~/.teamai/team-repo', 'manifest', 'roles.yaml'))).toBe( + nativePath.join(home, '.teamai', 'team-repo', 'manifest', 'roles.yaml'), + ); + }); + + it('only treats backslashes as separators on Windows', () => { + setPlatform(platform); + const nativePath = platform === 'win32' ? path.win32 : path.posix; + const join = nativePath.join; + vi.spyOn(path, 'join').mockImplementation(join); + const home = platform === 'win32' ? 'C:\\Users\\alice' : '/home/alice'; + vi.stubEnv('HOME', home); + + const input = '~\\.teamai\\team-repo'; + expect(expandHome(input)).toBe(platform === 'win32' + ? nativePath.join(home, '.teamai', 'team-repo') + : input); + for (const unchanged of ['~alice/repo', 'relative/repo', home]) { + expect(expandHome(unchanged)).toBe(unchanged); + } + }); +}); describe('normalizeHomePath', () => { it('converts a Git Bash drive path to native form', () => { diff --git a/src/utils/home.ts b/src/utils/home.ts index ff9c99d2c..97aad9d7a 100644 --- a/src/utils/home.ts +++ b/src/utils/home.ts @@ -82,7 +82,8 @@ export function legacyUserHome(): string | null { * Expand ~ to the platform user home directory in paths. */ export function expandHome(p: string): string { - if (p.startsWith('~/') || p === '~') { + // path.join converts ~/ to ~\ on Windows before manifest readers expand it. + if (p.startsWith('~/') || p === '~' || (process.platform === 'win32' && p.startsWith('~\\'))) { return path.join(getUserHome(), p.slice(1)); } return p;