mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
Install shared skills for 20 more agents and read every agent home in WSL (#29122)
* Add skill install entries for more agents; read every provider home env in WSL Adds table entries for CodeBuddy, Qoder, Kiro (KIRO_HOME), Antigravity and 16 agents that read the shared .agents/skills root. The Antigravity entry replaces its hand-written discovery source. The WSL env probe now reads every table homeEnv in one NUL-terminated login-shell probe, and WSL provider detection probes every table provider's detection commands. * Fence the WSL provider-home probe and reject unusable relocated homes The WSL probe now runs through buildWslCapturedLoginShellCommand, so rc output cannot be read as a home value, bash reads .bashrc exports, and fish falls back to sh. A relocated home at the filesystem root, the user home or an ancestor of it falls back to the default folder on host and WSL. * Treat an unusable WSL guest home as a failed provider-home probe
This commit is contained in:
@@ -13,7 +13,7 @@ import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
|
||||
import type { SkillInstallDestinationAuthority } from '../skills/skill-install-destinations'
|
||||
import {
|
||||
resolveEnvironmentSkillProviderRoots,
|
||||
resolveWslGrokSkillProviderRoot,
|
||||
resolveWslSkillProviderRoots,
|
||||
withClaudeSkillProviderRoot
|
||||
} from '../skills/skill-provider-runtime-roots'
|
||||
import type { SkillInstallRequest, SkillProviderRootOverrides } from './runtime-skill-types'
|
||||
@@ -84,13 +84,8 @@ export async function resolveSkillProviderRoots(
|
||||
if (destination.scope !== 'global') {
|
||||
return {}
|
||||
}
|
||||
const grok = destination.wslDistro
|
||||
? await resolveWslGrokSkillProviderRoot(destination.wslDistro)
|
||||
: null
|
||||
const roots = destination.wslDistro
|
||||
? grok
|
||||
? { grok }
|
||||
: {}
|
||||
? await resolveWslSkillProviderRoots(destination.wslDistro)
|
||||
: resolveEnvironmentSkillProviderRoots()
|
||||
const config = host.getClaudeConfigDirectory?.(
|
||||
destination.wslDistro
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('bounded concurrent skill discovery', () => {
|
||||
const line = String(info.mock.calls.at(0)?.at(0))
|
||||
// `present` is the signal that separates "big tree" from "big root set", and
|
||||
// is not derivable from the other counts.
|
||||
expect(line).toContain('[skills] scan roots=26 present=3 walked=26 skills=3')
|
||||
expect(line).toContain('[skills] scan roots=32 present=3 walked=32 skills=3')
|
||||
expect(line).toContain('home-claude')
|
||||
expect(line).not.toContain(home)
|
||||
expect(line).not.toContain(tmpdir())
|
||||
|
||||
@@ -140,6 +140,7 @@ export function buildSkillDiscoverySources(
|
||||
// Why: `npx skills add --global` writes into each agent's own home skills
|
||||
// directory, so coverage misses them unless we scan every provider root.
|
||||
...providerHomeSources(home, pathApi, providerRootOverrides),
|
||||
// Why: discovery-only agent folders; installs for these agents go to the canonical root.
|
||||
source(
|
||||
'home-opencode',
|
||||
'OpenCode home',
|
||||
@@ -173,14 +174,6 @@ export function buildSkillDiscoverySources(
|
||||
['agent-skills'],
|
||||
'prime-agent'
|
||||
),
|
||||
source(
|
||||
'home-antigravity',
|
||||
'Antigravity home',
|
||||
pathApi.join(home, '.gemini', 'config', 'skills'),
|
||||
'home',
|
||||
['agent-skills'],
|
||||
'antigravity'
|
||||
),
|
||||
// Why: Muse installs to `.agents/skills` but also reads XDG config home.
|
||||
source(
|
||||
'home-muse',
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('resolveSkillProviderDestinations', () => {
|
||||
resolveSkillProviderDestinations({
|
||||
scope: 'global',
|
||||
homeDirectory: HOME,
|
||||
detectedProviders: ['opencode', 'goose', 'not-an-agent']
|
||||
detectedProviders: ['aider', 'ante', 'not-an-agent']
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
@@ -55,7 +55,10 @@ describe('skill install provider table drives discovery', () => {
|
||||
continue
|
||||
}
|
||||
const relocated = resolve('/srv', provider.id)
|
||||
const overrides = resolveEnvironmentSkillProviderRoots({ [provider.homeEnv]: relocated })
|
||||
const overrides = resolveEnvironmentSkillProviderRoots(
|
||||
{ [provider.homeEnv]: relocated },
|
||||
resolve(HOME)
|
||||
)
|
||||
expect(overrides).toEqual({ [provider.id]: join(relocated, 'skills') })
|
||||
expect(
|
||||
discover(overrides).find(
|
||||
@@ -64,4 +67,12 @@ describe('skill install provider table drives discovery', () => {
|
||||
).toBe(overrides[provider.id])
|
||||
}
|
||||
})
|
||||
|
||||
it('names each provider home with a plain env var the WSL probe can expand', () => {
|
||||
for (const provider of SKILL_INSTALL_PROVIDERS) {
|
||||
if (provider.homeEnv) {
|
||||
expect(provider.homeEnv).toMatch(/^[A-Z_][A-Z0-9_]*$/u)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SKILL_INSTALL_PROVIDERS } from '../../shared/skill-install-providers'
|
||||
import {
|
||||
resolveDefaultHermesSkillsRoot,
|
||||
resolveEnvironmentHermesSkillsRoot,
|
||||
resolveEnvironmentSkillProviderRoots,
|
||||
resolveWslGrokSkillProviderRoot,
|
||||
resolveWslSkillProviderRoots,
|
||||
withClaudeSkillProviderRoot
|
||||
} from './skill-provider-runtime-roots'
|
||||
|
||||
// The probe prints `$HOME` then each table `homeEnv` in table order, each NUL-terminated.
|
||||
function wslHomes(values: Record<string, string>, home = '/home/dev'): string {
|
||||
return [
|
||||
home,
|
||||
...SKILL_INSTALL_PROVIDERS.flatMap((provider) =>
|
||||
provider.homeEnv ? [values[provider.homeEnv] ?? ''] : []
|
||||
)
|
||||
]
|
||||
.map((value) => `${value}\0`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
describe('skill provider runtime roots', () => {
|
||||
it('maps Claude and Grok config homes to their global skill roots', () => {
|
||||
const claudeRoot = resolve('/srv/claude')
|
||||
@@ -35,6 +48,23 @@ describe('skill provider runtime roots', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the default folder for a home at the root, the user home or above it', () => {
|
||||
const userHome = resolve('/home/dev')
|
||||
for (const unusable of [resolve('/'), resolve('/home'), userHome]) {
|
||||
expect(
|
||||
resolveEnvironmentSkillProviderRoots(
|
||||
{ GROK_HOME: unusable, KIRO_HOME: unusable },
|
||||
userHome
|
||||
),
|
||||
unusable
|
||||
).toEqual({})
|
||||
}
|
||||
const inside = join(userHome, 'profiles', 'kiro')
|
||||
expect(resolveEnvironmentSkillProviderRoots({ KIRO_HOME: inside }, userHome)).toEqual({
|
||||
kiro: join(inside, 'skills')
|
||||
})
|
||||
})
|
||||
|
||||
it('maps a relocated HERMES_HOME to its skill root and ignores relative ones', () => {
|
||||
const hermesRoot = resolve('/srv/hermes')
|
||||
expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: hermesRoot })).toBe(
|
||||
@@ -103,23 +133,49 @@ describe('skill provider runtime roots', () => {
|
||||
expect(seen).toEqual([])
|
||||
})
|
||||
|
||||
it('maps the WSL login shell GROK_HOME to a host-readable skill root', async () => {
|
||||
it('maps a WSL login shell home var to a host-readable skill root', async () => {
|
||||
await expect(
|
||||
resolveWslGrokSkillProviderRoot('Ubuntu-24.04', async () => '/srv/grok\n')
|
||||
).resolves.toBe('\\\\wsl.localhost\\Ubuntu-24.04\\srv\\grok\\skills')
|
||||
resolveWslSkillProviderRoots('Ubuntu-24.04', async () =>
|
||||
wslHomes({ GROK_HOME: '/srv/grok\n' })
|
||||
)
|
||||
).resolves.toEqual({ grok: '\\\\wsl.localhost\\Ubuntu-24.04\\srv\\grok\\skills' })
|
||||
})
|
||||
|
||||
it('ignores unsafe or missing WSL GROK_HOME values', async () => {
|
||||
it('ignores unsafe, unusable, incomplete or missing WSL home values', async () => {
|
||||
for (const value of ['../grok', '/srv/a\tb', '/', '/home', '/home/dev', '/home/dev/']) {
|
||||
await expect(
|
||||
resolveWslSkillProviderRoots('Ubuntu', async () => wslHomes({ GROK_HOME: value })),
|
||||
value
|
||||
).resolves.toEqual({})
|
||||
}
|
||||
await expect(
|
||||
resolveWslGrokSkillProviderRoot('Ubuntu', async () => '../grok')
|
||||
).resolves.toBeNull()
|
||||
resolveWslSkillProviderRoots('Ubuntu', async () =>
|
||||
wslHomes({ GROK_HOME: '/home/dev/profiles/grok' })
|
||||
)
|
||||
).resolves.toEqual({
|
||||
grok: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\profiles\\grok\\skills'
|
||||
})
|
||||
// No final terminator: the payload is incomplete, so nothing in it is trusted.
|
||||
await expect(
|
||||
resolveWslGrokSkillProviderRoot('Ubuntu', async () => '/srv/grok\0other')
|
||||
).resolves.toBeNull()
|
||||
resolveWslSkillProviderRoots('Ubuntu', async () =>
|
||||
wslHomes({ GROK_HOME: '/srv/grok' }).slice(0, -1)
|
||||
)
|
||||
).resolves.toEqual({})
|
||||
// An empty or relative guest $HOME disables the home guard, so nothing is trusted.
|
||||
for (const home of ['', 'home/dev']) {
|
||||
await expect(
|
||||
resolveWslSkillProviderRoots('Ubuntu', async () =>
|
||||
wslHomes({ GROK_HOME: '/srv/grok' }, home)
|
||||
),
|
||||
home
|
||||
).resolves.toEqual({})
|
||||
}
|
||||
// A lost fence.
|
||||
await expect(resolveWslSkillProviderRoots('Ubuntu', async () => null)).resolves.toEqual({})
|
||||
await expect(
|
||||
resolveWslGrokSkillProviderRoot('Ubuntu', async () => {
|
||||
resolveWslSkillProviderRoots('Ubuntu', async () => {
|
||||
throw new Error('probe failed')
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
).resolves.toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
import { statSync } from 'node:fs'
|
||||
import { isAbsolute, join, resolve } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import path, { isAbsolute, join, posix, resolve } from 'node:path'
|
||||
import { SKILL_INSTALL_PROVIDERS } from '../../shared/skill-install-providers'
|
||||
import { buildWslCapturedLoginShellCommand } from '../../shared/wsl-login-shell-command'
|
||||
import { toWindowsWslPath } from '../../shared/wsl-paths'
|
||||
import { runWslProcess } from '../wsl/wsl-runner'
|
||||
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
|
||||
|
||||
const PROVIDER_ROOT_MAX_LENGTH = 32_768
|
||||
const WSL_ENV_PROBE_TIMEOUT_MS = 8_000
|
||||
const WSL_ENV_PROBE_MAX_BYTES = 4_097
|
||||
const WSL_GROK_HOME_SCRIPT = [
|
||||
'entry=$(getent passwd "$(id -u)" 2>/dev/null || true)',
|
||||
'login_shell=${entry##*:}',
|
||||
'case "$login_shell" in /*) ;; *) login_shell=/bin/sh ;; esac',
|
||||
`exec "$login_shell" -lc 'printf %s "\${GROK_HOME:-}" | head -c ${WSL_ENV_PROBE_MAX_BYTES}'`
|
||||
].join('\n')
|
||||
const WSL_ENV_PROBE_VALUE_MAX_LENGTH = 4_096
|
||||
// Room for rc output ahead of the fence; a clipped fence reads as a failed probe.
|
||||
const WSL_ENV_PROBE_MAX_BYTES = 65_536
|
||||
const PROVIDER_HOME_ENVS = SKILL_INSTALL_PROVIDERS.flatMap((provider) =>
|
||||
provider.homeEnv ? [{ id: provider.id, name: provider.homeEnv }] : []
|
||||
)
|
||||
// Why $HOME first: unusable homes are judged against the guest's own home.
|
||||
const WSL_PROVIDER_HOMES_PROBE = buildWslCapturedLoginShellCommand(
|
||||
`printf '%s\\0' "$HOME" ${PROVIDER_HOME_ENVS.map(({ name }) => `"\${${name}:-}"`).join(' ')}`
|
||||
)
|
||||
|
||||
type HomePathApi = Pick<typeof posix, 'dirname' | 'isAbsolute' | 'relative' | 'sep'>
|
||||
|
||||
/**
|
||||
* Kiro documents ignoring a `KIRO_HOME` at the filesystem root, the user home or
|
||||
* an ancestor of it; Orca applies that rule to every `homeEnv` on purpose, since
|
||||
* `<home>/skills` there would be an unrelated folder.
|
||||
*/
|
||||
function isUsableProviderHome(candidate: string, userHome: string, pathApi: HomePathApi): boolean {
|
||||
if (pathApi.dirname(candidate) === candidate) {
|
||||
return false
|
||||
}
|
||||
const toUserHome = pathApi.relative(candidate, userHome)
|
||||
return (
|
||||
toUserHome === '..' ||
|
||||
toUserHome.startsWith(`..${pathApi.sep}`) ||
|
||||
pathApi.isAbsolute(toUserHome)
|
||||
)
|
||||
}
|
||||
|
||||
function normalizedRoot(value: string | undefined): string | null {
|
||||
const candidate = value?.trim()
|
||||
@@ -29,12 +53,13 @@ function normalizedRoot(value: string | undefined): string | null {
|
||||
}
|
||||
|
||||
export function resolveEnvironmentSkillProviderRoots(
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
userHome: string = homedir()
|
||||
): SkillProviderRootOverrides {
|
||||
const roots: SkillProviderRootOverrides = {}
|
||||
for (const provider of SKILL_INSTALL_PROVIDERS) {
|
||||
const home = provider.homeEnv ? normalizedRoot(env[provider.homeEnv]) : null
|
||||
if (home) {
|
||||
if (home && isUsableProviderHome(home, resolve(userHome), path)) {
|
||||
roots[provider.id] = join(home, 'skills')
|
||||
}
|
||||
}
|
||||
@@ -91,50 +116,70 @@ export function withClaudeSkillProviderRoot(
|
||||
return configDirectory ? { ...roots, claude: join(configDirectory, 'skills') } : roots
|
||||
}
|
||||
|
||||
type WslEnvironmentProbe = (distro: string) => Promise<string>
|
||||
/** The fenced payload, or null when the probe failed or the fence was lost. */
|
||||
type WslEnvironmentProbe = (distro: string) => Promise<string | null>
|
||||
|
||||
async function probeWslGrokHome(distro: string): Promise<string> {
|
||||
async function probeWslProviderHomes(distro: string): Promise<string | null> {
|
||||
const result = await runWslProcess({
|
||||
distro,
|
||||
// 'none': the script runs its own `"$login_shell" -lc`, so asking the
|
||||
// runner to probe first buys a second login shell and spends up to half
|
||||
// the 8s budget before the -lc that actually reads GROK_HOME starts.
|
||||
// 'none': the fenced command runs its own login shell, so a runner probe
|
||||
// would buy a second one out of the same 8s budget.
|
||||
loginPath: 'none',
|
||||
script: WSL_GROK_HOME_SCRIPT,
|
||||
// POSIX (`case`, `exec`); declared because the payload is opaque here.
|
||||
script: WSL_PROVIDER_HOMES_PROBE.command,
|
||||
shell: 'sh',
|
||||
timeoutMs: WSL_ENV_PROBE_TIMEOUT_MS,
|
||||
maxOutputBytes: WSL_ENV_PROBE_MAX_BYTES
|
||||
})
|
||||
// A timeout mid-write can leave a truncated but shape-valid absolute path.
|
||||
if (result.code !== 0 || result.timedOut) {
|
||||
return ''
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
export async function resolveWslGrokSkillProviderRoot(
|
||||
distro: string,
|
||||
probe: WslEnvironmentProbe = probeWslGrokHome
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const value = await probe(distro)
|
||||
const candidate = value.trim()
|
||||
if (
|
||||
!candidate ||
|
||||
candidate.length >= WSL_ENV_PROBE_MAX_BYTES ||
|
||||
!candidate.startsWith('/') ||
|
||||
candidate.includes('\\') ||
|
||||
Array.from(candidate).some((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return code <= 0x1f || code === 0x7f
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const grokHome = candidate.replace(/\/+$/u, '') || '/'
|
||||
return toWindowsWslPath(`${grokHome === '/' ? '' : grokHome}/skills`, distro)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return WSL_PROVIDER_HOMES_PROBE.readStdout(result.stdout)
|
||||
}
|
||||
|
||||
/** A clean absolute guest path, or null. */
|
||||
function cleanWslPath(value: string): string | null {
|
||||
const candidate = value.trim()
|
||||
if (
|
||||
!candidate ||
|
||||
candidate.length > WSL_ENV_PROBE_VALUE_MAX_LENGTH ||
|
||||
!candidate.startsWith('/') ||
|
||||
candidate.includes('\\') ||
|
||||
Array.from(candidate).some((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return code <= 0x1f || code === 0x7f
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return posix.resolve(candidate)
|
||||
}
|
||||
|
||||
/** Every table `homeEnv` as the WSL login shell sees it, read in one probe. */
|
||||
export async function resolveWslSkillProviderRoots(
|
||||
distro: string,
|
||||
probe: WslEnvironmentProbe = probeWslProviderHomes
|
||||
): Promise<SkillProviderRootOverrides> {
|
||||
try {
|
||||
// `$HOME` then each value, all NUL-terminated; any other shape is a broken probe.
|
||||
const fields = (await probe(distro))?.split('\0') ?? []
|
||||
if (fields.length !== PROVIDER_HOME_ENVS.length + 2) {
|
||||
return {}
|
||||
}
|
||||
const [rawUserHome, ...values] = fields
|
||||
// Without the guest home the root/home/ancestor guard cannot run.
|
||||
const userHome = cleanWslPath(rawUserHome)
|
||||
if (!userHome) {
|
||||
return {}
|
||||
}
|
||||
const roots: SkillProviderRootOverrides = {}
|
||||
PROVIDER_HOME_ENVS.forEach(({ id }, index) => {
|
||||
const home = cleanWslPath(values[index])
|
||||
if (home && isUsableProviderHome(home, userHome, posix)) {
|
||||
roots[id] = toWindowsWslPath(`${home}/skills`, distro)
|
||||
}
|
||||
})
|
||||
return roots
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
const runWslProcessMock = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: runWslProcessMock }))
|
||||
|
||||
import { SKILL_INSTALL_PROVIDERS } from '../../shared/skill-install-providers'
|
||||
import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
import { detectSkillProvidersInWsl } from './skill-wsl-provider-detection'
|
||||
|
||||
type RunWslProcessSpec = { distro: string; loginPath: string; script: string }
|
||||
@@ -48,7 +50,27 @@ describe('detectSkillProvidersInWsl', () => {
|
||||
|
||||
const found = await detectSkillProvidersInWsl('Ubuntu')
|
||||
|
||||
expect(found).toEqual(['codex', 'claude'])
|
||||
expect(found.toSorted()).toEqual(['claude', 'codex'])
|
||||
})
|
||||
|
||||
it('probes every install provider by the commands Orca detects it with', async () => {
|
||||
runWslProcessMock.mockResolvedValue({
|
||||
environmentResolved: true,
|
||||
code: 0,
|
||||
stdout: 'cursor-agent\n',
|
||||
stderr: '',
|
||||
timedOut: false
|
||||
})
|
||||
|
||||
const found = await detectSkillProvidersInWsl('Ubuntu')
|
||||
|
||||
const [spec] = runWslProcessMock.mock.calls.at(-1) as [RunWslProcessSpec]
|
||||
for (const provider of SKILL_INSTALL_PROVIDERS) {
|
||||
for (const command of getTuiAgentDetectCommands(TUI_AGENT_CONFIG[provider.id])) {
|
||||
expect(spec.script, provider.id).toContain(`'${command}'`)
|
||||
}
|
||||
}
|
||||
expect(found).toEqual(['cursor'])
|
||||
})
|
||||
|
||||
it('ignores stray output that is not a recognized provider name', async () => {
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { isSkillInstallProviderId } from '../../shared/skill-install-providers'
|
||||
import {
|
||||
getTuiAgentDetectionProbeCommands,
|
||||
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
|
||||
resolveDetectedTuiAgentIds
|
||||
} from '../../shared/tui-agent-detection-commands'
|
||||
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
|
||||
import { runWslProcess } from '../wsl/wsl-runner'
|
||||
|
||||
const PROVIDER_DETECTION_COMMANDS = KNOWN_TUI_AGENT_DETECTION_COMMANDS.filter((command) =>
|
||||
isSkillInstallProviderId(command.id)
|
||||
)
|
||||
|
||||
const DETECTION_SCRIPT = [
|
||||
'set -eu',
|
||||
'command -v codex >/dev/null 2>&1 && printf "codex\\n" || true',
|
||||
'command -v claude >/dev/null 2>&1 && printf "claude\\n" || true'
|
||||
`for cmd in ${getTuiAgentDetectionProbeCommands(PROVIDER_DETECTION_COMMANDS, 'wsl')
|
||||
.map(quotePosixShell)
|
||||
.join(' ')}; do`,
|
||||
'command -v "$cmd" >/dev/null 2>&1 && printf "%s\\n" "$cmd"',
|
||||
'done',
|
||||
'true'
|
||||
].join('\n')
|
||||
|
||||
export async function detectSkillProvidersInWsl(distro: string): Promise<string[]> {
|
||||
let result
|
||||
try {
|
||||
// Why probe: a bare `sh -c` has no login shell, so a PATH built by nvm/mise
|
||||
// rc files never applies and an installed codex/claude reads as absent.
|
||||
// rc files never applies and an installed agent reads as absent.
|
||||
result = await runWslProcess({
|
||||
distro,
|
||||
loginPath: 'preferred',
|
||||
@@ -25,7 +39,7 @@ export async function detectSkillProvidersInWsl(distro: string): Promise<string[
|
||||
if (result.code !== 0) {
|
||||
throw new Error('skill-install-wsl-provider-detection-failed')
|
||||
}
|
||||
// Unconditional, not just on an empty list: the script ends in `|| true`, so
|
||||
// Unconditional, not just on an empty list: the script always exits 0, so
|
||||
// without the login PATH each lookup independently reads absent. A `claude`
|
||||
// on the default PATH via Windows interop plus an nvm-only `codex` returns a
|
||||
// plausible-looking `['claude']`, and the caller then skips the ~/.codex
|
||||
@@ -33,7 +47,9 @@ export async function detectSkillProvidersInWsl(distro: string): Promise<string[
|
||||
if (!result.environmentResolved) {
|
||||
throw new Error('skill-install-wsl-provider-detection-failed')
|
||||
}
|
||||
return result.stdout
|
||||
.split(/\r?\n/u)
|
||||
.filter((provider) => provider === 'codex' || provider === 'claude')
|
||||
return resolveDetectedTuiAgentIds(
|
||||
PROVIDER_DETECTION_COMMANDS,
|
||||
new Set(result.stdout.split(/\r?\n/u)),
|
||||
'wsl'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type SkillInstallProviderEntry = {
|
||||
displayName: string
|
||||
globalSegments: readonly string[] | null
|
||||
workspaceSegments: readonly string[] | null
|
||||
/** Env var relocating the agent's home (skills in `<home>/skills`); the WSL probe is still Grok-only. */
|
||||
/** Env var relocating the agent's home (skills in `<home>/skills`). */
|
||||
homeEnv?: string
|
||||
}
|
||||
|
||||
@@ -90,6 +90,130 @@ export const SKILL_INSTALL_PROVIDERS = Object.freeze([
|
||||
displayName: 'ZCode',
|
||||
globalSegments: ['.zcode', 'skills'],
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'codebuddy',
|
||||
displayName: 'CodeBuddy',
|
||||
globalSegments: ['.codebuddy', 'skills'],
|
||||
workspaceSegments: ['.codebuddy', 'skills']
|
||||
},
|
||||
{
|
||||
id: 'qoder',
|
||||
displayName: 'Qoder',
|
||||
globalSegments: ['.qoder', 'skills'],
|
||||
workspaceSegments: ['.qoder', 'skills']
|
||||
},
|
||||
{
|
||||
id: 'kiro',
|
||||
displayName: 'Kiro',
|
||||
globalSegments: ['.kiro', 'skills'],
|
||||
workspaceSegments: ['.kiro', 'skills'],
|
||||
homeEnv: 'KIRO_HOME'
|
||||
},
|
||||
// Why: the Antigravity CLI keeps user skills under its config home and
|
||||
// project skills in the canonical root.
|
||||
{
|
||||
id: 'antigravity',
|
||||
displayName: 'Antigravity',
|
||||
globalSegments: ['.gemini', 'config', 'skills'],
|
||||
workspaceSegments: null
|
||||
},
|
||||
// Why: every agent below reads the canonical .agents/skills root at both scopes.
|
||||
{
|
||||
id: 'autohand',
|
||||
displayName: 'Autohand Code',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'goose',
|
||||
displayName: 'Goose',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'crush',
|
||||
displayName: 'Crush',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'command-code',
|
||||
displayName: 'Command Code',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'mistral-vibe',
|
||||
displayName: 'Mistral Vibe',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'qwen-code',
|
||||
displayName: 'Qwen Code',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'rovo',
|
||||
displayName: 'Rovo Dev',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'openclaw',
|
||||
displayName: 'OpenClaw',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'devin',
|
||||
displayName: 'Devin',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'pi',
|
||||
displayName: 'Pi',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'amp',
|
||||
displayName: 'Amp',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'kilo',
|
||||
displayName: 'Kilo Code',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'cline',
|
||||
displayName: 'Cline',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'kimi',
|
||||
displayName: 'Kimi',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
},
|
||||
{
|
||||
id: 'copilot',
|
||||
displayName: 'GitHub Copilot',
|
||||
globalSegments: null,
|
||||
workspaceSegments: null
|
||||
}
|
||||
] satisfies SkillInstallProviderEntry[])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user