diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 20cb5cf8..78bc2019 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -6,6 +6,8 @@ All notable CLI behavior changes are documented in this file. ### Added +- Add the `pi` agent profile, displayed as Pi, with `--agent pi`, project-level + `/.pi/skills/`, and user-level `~/.pi/agent/skills/` support. - Add the user-level `astudio` agent profile, displayed as AStudio, with automatic detection of `~/.acode/skills` on Linux, macOS, and Windows. - Add repeatable `sync pull --skill ` selection for non-interactive and JSON workflows, with diff --git a/cli/README.md b/cli/README.md index b229ead5..49d04e75 100644 --- a/cli/README.md +++ b/cli/README.md @@ -172,6 +172,9 @@ skillhub install pdf-parser --agent codex # Install to AStudio's fixed user-level directory skillhub install pdf-parser --agent astudio +# Install to Pi's user-level directory (use --scope project for the project directory) +skillhub install pdf-parser --agent pi + # Install to multiple Agents skillhub install pdf-parser --agent codex --agent claude-code @@ -219,6 +222,7 @@ Most Agents have both project-level and user-level skills directories. Use `--sc | `openclaw` | `/.openclaw/skills/` | `~/.openclaw/skills/` | | `opencode` | `/.opencode/skills/` | `~/.opencode/skills/` | | `kilo` | `/.kilo/skills/` | `~/.kilo/skills/` | +| `pi` (Pi) | `/.pi/skills/` | `~/.pi/agent/skills/` | | _fallback_ | `/.agents/skills/` | `~/.agents/skills/` | For a custom path or an unsupported Agent directory, use `--dir` to specify the installation path. In interactive user scope, the `generic` target is offered alongside detected Agent targets. AStudio appears in that selector when `~/.acode/skills/` exists. When `--scope user|project` finds no matching agent directory, the CLI falls back to the `_fallback_` row above. diff --git a/cli/src/agents/detector.ts b/cli/src/agents/detector.ts index 50abff75..dab2167e 100644 --- a/cli/src/agents/detector.ts +++ b/cli/src/agents/detector.ts @@ -14,19 +14,20 @@ import { traeProfile } from './profiles/trae' import { traeCnProfile } from './profiles/trae-cn' import { opencodeProfile } from './profiles/opencode' import { kiloProfile } from './profiles/kilo' +import { piProfile } from './profiles/pi' export { aStudioProfile, claudeCodeProfile, codexProfile, cursorProfile, githubCopilotProfile, geminiCliProfile, openhandsProfile, windsurfProfile, openclawProfile, kiroCliProfile, rooProfile, traeProfile, traeCnProfile, - opencodeProfile, kiloProfile + opencodeProfile, kiloProfile, piProfile } export const allProfiles: AgentProfile[] = [ aStudioProfile, claudeCodeProfile, codexProfile, cursorProfile, githubCopilotProfile, geminiCliProfile, openhandsProfile, windsurfProfile, openclawProfile, kiroCliProfile, rooProfile, traeProfile, traeCnProfile, - opencodeProfile, kiloProfile + opencodeProfile, kiloProfile, piProfile ] export const profileMap = new Map(allProfiles.map(p => [p.id, p])) diff --git a/cli/src/agents/profiles/pi.ts b/cli/src/agents/profiles/pi.ts new file mode 100644 index 00000000..1387095a --- /dev/null +++ b/cli/src/agents/profiles/pi.ts @@ -0,0 +1,2 @@ +import { makeProfile } from './make-profile' +export const piProfile = makeProfile('pi', 'Pi', '.pi/skills', '.pi/agent/skills') diff --git a/cli/test/integration/install-command.test.ts b/cli/test/integration/install-command.test.ts index f6b6f21c..36084f33 100644 --- a/cli/test/integration/install-command.test.ts +++ b/cli/test/integration/install-command.test.ts @@ -667,6 +667,72 @@ describe('install command — server errors', () => { // --------------------------------------------------------------------------- describe('install command — multi-agent & auto-detect', () => { + test('--agent pi defaults to the user root and persists the Pi agent id', async () => { + const env = await createTempHome() + registry = await startFakeRegistry({ + token: 'sk_ok', + user: { handle: 'u', displayName: 'U' }, + skills: [{ namespace: 'global', slug: 'pdf-parser', version: '1.0.0', zipBytes: makeSkillZip() }] + }) + await runCli(['login', '--registry', registry.url, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home }) + + const result = await runCli( + [ + 'install', 'pdf-parser', + '--agent', 'pi', + '--registry', registry.url, + '--token', 'sk_ok', + '--json' + ], + { HOME: env.home, USERPROFILE: env.home }, + { cwd: env.cwd } + ) + + expect(result.exitCode).toBe(0) + const installDir = join(env.home, '.pi', 'agent', 'skills', 'pdf-parser') + const parsed = JSON.parse(result.stdout) as { installed: Array<{ agent: string; dir: string }> } + expect(parsed.installed).toEqual([{ agent: 'pi', dir: installDir }]) + + const metadataPath = join(installDir, '.skillhub', 'metadata.json') + expect(JSON.parse(await readFile(metadataPath, 'utf-8')).agent).toBe('pi') + + const inventory = JSON.parse(await readFile(join(env.home, '.skillhub', 'inventory.json'), 'utf-8')) as { + items: Array<{ targets: Array<{ agent: string; installDir: string }> }> + } + expect(inventory.items[0]?.targets[0]).toMatchObject({ agent: 'pi', installDir }) + }) + + test('auto-detects an existing Pi project skills directory end to end', async () => { + const env = await createTempHome() + registry = await startFakeRegistry({ + token: 'sk_ok', + user: { handle: 'u', displayName: 'U' }, + skills: [{ namespace: 'global', slug: 'pdf-parser', version: '1.0.0', zipBytes: makeSkillZip() }] + }) + await runCli(['login', '--registry', registry.url, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home }) + await mkdir(join(env.cwd, '.pi', 'skills'), { recursive: true }) + + const result = await runCli( + ['install', 'pdf-parser', '--registry', registry.url, '--token', 'sk_ok', '--json'], + { HOME: env.home, USERPROFILE: env.home }, + { cwd: env.cwd } + ) + + expect(result.exitCode).toBe(0) + const parsed = JSON.parse(result.stdout) as { installed: Array<{ agent: string; dir: string }> } + expect(parsed.installed).toHaveLength(1) + expect(parsed.installed[0]?.agent).toBe('pi') + expect(parsed.installed[0]?.dir).toMatch(/[/\\]\.pi[/\\]skills[/\\]pdf-parser/) + expect(await Bun.file(join( + env.cwd, + '.pi', + 'skills', + 'pdf-parser', + '.skillhub', + 'metadata.json' + )).exists()).toBe(true) + }) + test('--agent astudio installs and persists the stable lowercase agent id', async () => { const env = await createTempHome() registry = await startFakeRegistry({ @@ -993,6 +1059,38 @@ describe('install command — multi-agent & auto-detect', () => { // --------------------------------------------------------------------------- describe('install command — --scope', () => { + test('--scope project --agent pi installs to /.pi/skills', async () => { + const env = await createTempHome() + registry = await startFakeRegistry({ + token: 'sk_ok', + user: { handle: 'u1', displayName: 'User One' }, + skills: [{ namespace: 'global', slug: 'foo', version: '1.0.0', zipBytes: makeSkillZip() }] + }) + + await runCli( + ['login', '--registry', registry.url, '--token', 'sk_ok'], + { HOME: env.home, USERPROFILE: env.home } + ) + + const result = await runCli( + ['install', 'foo', '--scope', 'project', '--agent', 'pi', + '--registry', registry.url, '--token', 'sk_ok', '--json'], + { HOME: env.home, USERPROFILE: env.home }, + { cwd: env.cwd } + ) + + expect(result.exitCode).toBe(0) + const installDir = join(env.cwd, '.pi', 'skills', 'foo') + const parsed = JSON.parse(result.stdout) as { installed: Array<{ agent: string; dir: string }> } + expect(parsed.installed).toHaveLength(1) + expect(parsed.installed[0]?.agent).toBe('pi') + expect(parsed.installed[0]?.dir).toMatch(/[/\\]\.pi[/\\]skills[/\\]foo/) + expect(JSON.parse(await readFile( + join(installDir, '.skillhub', 'metadata.json'), + 'utf-8' + )).agent).toBe('pi') + }) + test('--scope project --agent codex installs to /.codex/skills', async () => { const env = await createTempHome() registry = await startFakeRegistry({ diff --git a/cli/test/unit/agents/profiles.test.ts b/cli/test/unit/agents/profiles.test.ts index ec567fe7..cd667520 100644 --- a/cli/test/unit/agents/profiles.test.ts +++ b/cli/test/unit/agents/profiles.test.ts @@ -5,8 +5,8 @@ import { describe, expect, test } from 'bun:test' import { allProfiles, profileMap } from '../../../src/agents/detector' describe('agent profiles', () => { - test('has 15 tier 1 profiles', () => { - expect(allProfiles).toHaveLength(15) + test('has 16 tier 1 profiles', () => { + expect(allProfiles).toHaveLength(16) }) test('all profiles have unique ids', () => { @@ -15,12 +15,13 @@ describe('agent profiles', () => { }) test('profileMap contains all profiles', () => { - expect(profileMap.size).toBe(15) + expect(profileMap.size).toBe(16) expect(profileMap.has('astudio')).toBe(true) expect(profileMap.has('claude-code')).toBe(true) expect(profileMap.has('codex')).toBe(true) expect(profileMap.has('cursor')).toBe(true) expect(profileMap.has('kilo')).toBe(true) + expect(profileMap.has('pi')).toBe(true) }) test('claude-code profile returns correct roots', () => { @@ -40,6 +41,53 @@ describe('agent profiles', () => { expect(profile.projectRoots('/repo')).toEqual(['/repo/.cursor/skills']) }) + test('Pi exposes and detects its project and user skills directories', async () => { + const base = await mkdtemp(join(tmpdir(), 'skillhub-pi-profile-')) + const cwd = join(base, 'repo') + const home = join(base, 'home') + const projectRoot = `${cwd}/.pi/skills` + const userRoot = `${home}/.pi/agent/skills` + const profile = profileMap.get('pi')! + + try { + await mkdir(cwd, { recursive: true }) + await mkdir(home, { recursive: true }) + + expect(profile.displayName).toBe('Pi') + expect(profile.projectRoots(cwd)).toEqual([projectRoot]) + expect(profile.userRoots(home)).toEqual([userRoot]) + expect(await profile.detectInstalled(cwd, home)).toEqual([]) + + await mkdir(join(cwd, '.pi'), { recursive: true }) + await mkdir(join(home, '.pi', 'agent'), { recursive: true }) + await writeFile(projectRoot, 'not a directory') + await writeFile(userRoot, 'not a directory') + expect(await profile.detectInstalled(cwd, home)).toEqual([]) + + await rm(projectRoot) + await rm(userRoot) + await mkdir(projectRoot, { recursive: true }) + await mkdir(userRoot, { recursive: true }) + + expect(await profile.detectInstalled(cwd, home)).toEqual([ + { + agent: 'pi', + rootDir: projectRoot, + scope: 'project', + source: 'detected' + }, + { + agent: 'pi', + rootDir: userRoot, + scope: 'user', + source: 'detected' + } + ]) + } finally { + await rm(base, { recursive: true, force: true }) + } + }) + test('AStudio exposes only its fixed user-level directory on Linux, macOS, and Windows', () => { const profile = profileMap.get('astudio')! diff --git a/docs/skillhub/en/guide/cli.md b/docs/skillhub/en/guide/cli.md index 45272a4a..3fd0940b 100644 --- a/docs/skillhub/en/guide/cli.md +++ b/docs/skillhub/en/guide/cli.md @@ -157,6 +157,9 @@ skillhub install pdf-parser --agent codex # Install to AStudio's fixed user-level directory skillhub install pdf-parser --agent astudio +# Install to Pi's user-level directory (use --scope project for the project directory) +skillhub install pdf-parser --agent pi + # Install to multiple Agents skillhub install pdf-parser --agent codex --agent claude-code @@ -204,6 +207,7 @@ Most Agents have both project-level and user-level skills directories. Use `--sc | `openclaw` | `/.openclaw/skills/` | `~/.openclaw/skills/` | | `opencode` | `/.opencode/skills/` | `~/.opencode/skills/` | | `kilo` | `/.kilo/skills/` | `~/.kilo/skills/` | +| `pi` (Pi) | `/.pi/skills/` | `~/.pi/agent/skills/` | | _fallback_ | `/.agents/skills/` | `~/.agents/skills/` | For a custom path or an unsupported Agent directory, use `--dir` to specify the installation path. In interactive user scope, the `generic` target is offered alongside detected Agent targets. AStudio appears in that selector when `~/.acode/skills/` exists. When `--scope user|project` finds no matching agent directory, the CLI falls back to the `_fallback_` row above. diff --git a/docs/skillhub/guide/cli.md b/docs/skillhub/guide/cli.md index 9c7bf3e9..8ae5e341 100644 --- a/docs/skillhub/guide/cli.md +++ b/docs/skillhub/guide/cli.md @@ -153,6 +153,9 @@ skillhub install pdf-parser --agent codex # 安装到 AStudio 的固定用户级目录 skillhub install pdf-parser --agent astudio +# 安装到 Pi 的用户级目录(添加 --scope project 可安装到项目级目录) +skillhub install pdf-parser --agent pi + # 安装到多个 Agent skillhub install pdf-parser --agent codex --agent claude-code @@ -200,6 +203,7 @@ CLI 按以下逻辑确定安装位置: | `openclaw` | `/.openclaw/skills/` | `~/.openclaw/skills/` | | `opencode` | `/.opencode/skills/` | `~/.opencode/skills/` | | `kilo` | `/.kilo/skills/` | `~/.kilo/skills/` | +| `pi`(Pi) | `/.pi/skills/` | `~/.pi/agent/skills/` | | _fallback_ | `/.agents/skills/` | `~/.agents/skills/` | 对于自定义路径或不在列表中的 Agent 目录,使用 `--dir` 显式指定安装路径。交互式 user scope 下会与已探测 Agent 目标一同提供 `generic` 目标;当 `~/.acode/skills/` 存在时,选择器会显示 AStudio。当 `--scope user|project` 找不到匹配的 agent 目录时,CLI 会回退到上表的 `_fallback_` 行。