Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/guide/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <role-id>` once. Their next pull withdraws teamai-delivered copies outside the new namespaces (locally modified copies are kept).
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/zh-CN/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <role-id>`。之后的 pull 会收回新 namespace 之外、由 teamai 下发的副本(本地改过的保留)。
Expand Down
45 changes: 42 additions & 3 deletions src/__tests__/home-normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,56 @@
* 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;

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', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading