feat(cli): add Pi agent profile

This commit is contained in:
dongmucat 2026-09-17 14:23:30 +08:00 committed by GitHub
parent 48376069db
commit c888be7212
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 168 additions and 5 deletions

View file

@ -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
`<project>/.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 <slug>` selection for non-interactive and JSON workflows, with

View file

@ -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` | `<project>/.openclaw/skills/` | `~/.openclaw/skills/` |
| `opencode` | `<project>/.opencode/skills/` | `~/.opencode/skills/` |
| `kilo` | `<project>/.kilo/skills/` | `~/.kilo/skills/` |
| `pi` (Pi) | `<project>/.pi/skills/` | `~/.pi/agent/skills/` |
| _fallback_ | `<project>/.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.

View file

@ -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]))

View file

@ -0,0 +1,2 @@
import { makeProfile } from './make-profile'
export const piProfile = makeProfile('pi', 'Pi', '.pi/skills', '.pi/agent/skills')

View file

@ -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 <cwd>/.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 <cwd>/.codex/skills', async () => {
const env = await createTempHome()
registry = await startFakeRegistry({

View file

@ -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')!

View file

@ -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` | `<project>/.openclaw/skills/` | `~/.openclaw/skills/` |
| `opencode` | `<project>/.opencode/skills/` | `~/.opencode/skills/` |
| `kilo` | `<project>/.kilo/skills/` | `~/.kilo/skills/` |
| `pi` (Pi) | `<project>/.pi/skills/` | `~/.pi/agent/skills/` |
| _fallback_ | `<project>/.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.

View file

@ -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` | `<project>/.openclaw/skills/` | `~/.openclaw/skills/` |
| `opencode` | `<project>/.opencode/skills/` | `~/.opencode/skills/` |
| `kilo` | `<project>/.kilo/skills/` | `~/.kilo/skills/` |
| `pi`Pi | `<project>/.pi/skills/` | `~/.pi/agent/skills/` |
| _fallback_ | `<project>/.agents/skills/` | `~/.agents/skills/` |
对于自定义路径或不在列表中的 Agent 目录,使用 `--dir` 显式指定安装路径。交互式 user scope 下会与已探测 Agent 目标一同提供 `generic` 目标;当 `~/.acode/skills/` 存在时,选择器会显示 AStudio。当 `--scope user|project` 找不到匹配的 agent 目录时CLI 会回退到上表的 `_fallback_` 行。