mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(skills): pin the skills CLI and contract-test --agent scoping (#11593)
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
name: Skill add agent scoping
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/shared/skills-cli-version.ts'
|
||||
- 'src/shared/agent-feature-install-commands.ts'
|
||||
- 'src/cli/handlers/skills.ts'
|
||||
- 'src/main/skills/skill-update-run.ts'
|
||||
- 'config/scripts/verify-skill-add-agent-scoping.mjs'
|
||||
- '.github/workflows/skill-add-agent-scoping.yml'
|
||||
merge_group:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
scoping:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
skills-cli: [pinned]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
skills-cli: latest
|
||||
continue-on-error: ${{ matrix.skills-cli == 'latest' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- name: Verify --agent scoping against the resolved skills CLI
|
||||
run: node config/scripts/verify-skill-add-agent-scoping.mjs --cli=${{ matrix.skills-cli }}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
// Proves the resolved skills CLI still honours our --agent scoping (#11593).
|
||||
// The vendor's parser silently drops flags it does not recognize, and its
|
||||
// zero-detected non-interactive branch installs into every agent it knows
|
||||
// (~75), so a renamed --agent would revive that fan-out with exit 0. Run 1
|
||||
// asserts our exact argv shape stays scoped; run 2 replays the rename (our
|
||||
// flag unknown, its value demoted to an ignored positional) and asserts the
|
||||
// fan-out still exists — if run 2 ever stops fanning out, the failure shape
|
||||
// this contract discriminates on has changed and needs a human re-read.
|
||||
|
||||
function option(name) {
|
||||
return process.argv.find((value) => value.startsWith(`--${name}=`))?.slice(name.length + 3)
|
||||
}
|
||||
|
||||
const cliOption = option('cli') ?? 'pinned'
|
||||
const pinSource = await readFile('src/shared/skills-cli-version.ts', 'utf8')
|
||||
const pinnedVersion = /SKILLS_CLI_VERSION = '([^']+)'/.exec(pinSource)?.[1]
|
||||
if (!pinnedVersion) {
|
||||
throw new Error('Could not read SKILLS_CLI_VERSION from src/shared/skills-cli-version.ts')
|
||||
}
|
||||
// Why: defaulting to the pin means a version-bump PR re-proves the contract
|
||||
// against the new version with no workflow edit; `latest` is the drift canary.
|
||||
const cliVersion = cliOption === 'pinned' ? pinnedVersion : cliOption
|
||||
|
||||
const FANOUT_MIN = 20
|
||||
const isWindows = process.platform === 'win32'
|
||||
const sandbox = await mkdtemp(path.join(tmpdir(), 'orca-skill-add-agent-scoping-'))
|
||||
const fixture = path.join(sandbox, 'fixture')
|
||||
const probeName = 'scoping-probe'
|
||||
|
||||
// Why: an explicit allowlist, never process.env — inherited agent homes
|
||||
// (CODEX_HOME), in-agent markers, or agent CLIs on PATH would make the CLI's
|
||||
// detection non-zero and mask the zero-detected branch this contract pins.
|
||||
const nodeBinDir = path.dirname(process.execPath)
|
||||
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows'
|
||||
const scrubbedPath = isWindows
|
||||
? [nodeBinDir, path.join(systemRoot, 'System32'), systemRoot].join(path.delimiter)
|
||||
: [nodeBinDir, '/usr/bin', '/bin', '/usr/sbin', '/sbin'].join(path.delimiter)
|
||||
|
||||
function sandboxEnv(home) {
|
||||
return {
|
||||
PATH: scrubbedPath,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_STATE_HOME: path.join(sandbox, 'state'),
|
||||
npm_config_cache: path.join(sandbox, 'npm-cache'),
|
||||
TMPDIR: path.join(sandbox, 'tmp'),
|
||||
TEMP: path.join(sandbox, 'tmp'),
|
||||
TMP: path.join(sandbox, 'tmp'),
|
||||
DO_NOT_TRACK: '1',
|
||||
CI: '1',
|
||||
...(isWindows
|
||||
? {
|
||||
APPDATA: path.join(home, 'AppData', 'Roaming'),
|
||||
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
|
||||
SystemRoot: systemRoot,
|
||||
windir: process.env.windir ?? systemRoot,
|
||||
ComSpec: process.env.ComSpec ?? path.join(systemRoot, 'System32', 'cmd.exe'),
|
||||
PATHEXT: process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD'
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
function execSkills(home, args) {
|
||||
const executable = isWindows ? (process.env.ComSpec ?? 'cmd.exe') : 'npx'
|
||||
const cliArgs = ['--yes', `skills@${cliVersion}`, ...args]
|
||||
execFileSync(executable, isWindows ? ['/d', '/s', '/c', 'npx.cmd', ...cliArgs] : cliArgs, {
|
||||
// Why: the sandbox cwd keeps the repo's own .npmrc out of the child npm.
|
||||
cwd: sandbox,
|
||||
env: sandboxEnv(home),
|
||||
stdio: 'inherit'
|
||||
})
|
||||
}
|
||||
|
||||
// npm/npx working directories the child may create in a bare home; everything
|
||||
// else a run leaves behind is an agent placement.
|
||||
const NON_AGENT_ENTRIES = new Set(['.npm', '.cache', '.config', '.local', 'AppData'])
|
||||
|
||||
async function agentEntries(home) {
|
||||
const entries = await readdir(home).catch(() => [])
|
||||
return entries.filter((entry) => !NON_AGENT_ENTRIES.has(entry)).sort()
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(path.join(fixture, 'skills', probeName), { recursive: true })
|
||||
await writeFile(
|
||||
path.join(fixture, 'skills', probeName, 'SKILL.md'),
|
||||
`---\nname: ${probeName}\ndescription: Contract probe for --agent scoping.\n---\n\n# Scoping probe\n`
|
||||
)
|
||||
await mkdir(path.join(sandbox, 'tmp'), { recursive: true })
|
||||
|
||||
// Run 1 — the exact argv shape Orca spawns, minus the repo source: --agent
|
||||
// must both skip detection and confine placements to its target.
|
||||
const homeScoped = path.join(sandbox, 'home-scoped')
|
||||
await mkdir(homeScoped, { recursive: true })
|
||||
execSkills(homeScoped, [
|
||||
'add',
|
||||
fixture,
|
||||
'--skill',
|
||||
probeName,
|
||||
'--global',
|
||||
'--agent',
|
||||
'claude-code',
|
||||
'-y'
|
||||
])
|
||||
const scoped = await agentEntries(homeScoped)
|
||||
await stat(path.join(homeScoped, '.claude', 'skills', probeName))
|
||||
// The CLI may also write the canonical universal placement; anything else is
|
||||
// the fan-out this contract exists to catch.
|
||||
const leaked = scoped.filter((entry) => entry !== '.claude' && entry !== '.agents')
|
||||
if (!scoped.includes('.claude') || leaked.length > 0) {
|
||||
throw new Error(
|
||||
`skills@${cliVersion} no longer honours --agent claude-code: placements [${scoped.join(', ')}]`
|
||||
)
|
||||
}
|
||||
console.log(`[skill-add-agent-scoping] scoped install placed only: ${scoped.join(', ')}`)
|
||||
|
||||
// Run 2 — the upstream-rename replay: an unknown flag whose value degrades
|
||||
// to an ignored positional. Today that means the all-agents fan-out.
|
||||
const homeDrift = path.join(sandbox, 'home-drift')
|
||||
await mkdir(homeDrift, { recursive: true })
|
||||
execSkills(homeDrift, [
|
||||
'add',
|
||||
fixture,
|
||||
'--skill',
|
||||
probeName,
|
||||
'--global',
|
||||
'--orca-scoping-contract-probe',
|
||||
'claude-code',
|
||||
'-y'
|
||||
])
|
||||
const drifted = await agentEntries(homeDrift)
|
||||
if (drifted.length < FANOUT_MIN) {
|
||||
throw new Error(
|
||||
`The rename replay produced ${drifted.length} agent placements (expected >= ${FANOUT_MIN}). ` +
|
||||
`skills@${cliVersion} changed the unscoped non-interactive branch; re-read its target ` +
|
||||
'resolution before trusting this contract.'
|
||||
)
|
||||
}
|
||||
console.log(
|
||||
`[skill-add-agent-scoping] rename replay fanned out to ${drifted.length} agent placements`
|
||||
)
|
||||
} finally {
|
||||
await rm(sandbox, { recursive: true, force: true })
|
||||
}
|
||||
+15
-15
@@ -336,7 +336,7 @@ describe('orca skills CLI', () => {
|
||||
await main(['skills', 'install', '--skill', 'alpha', '--dry-run'], '/tmp/repo')
|
||||
|
||||
expect(stdoutText(stdoutSpy)).toBe(
|
||||
'npx --yes skills add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n\n' +
|
||||
'npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n\n' +
|
||||
'Rerun without --dry-run to install now.\n'
|
||||
)
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
@@ -351,7 +351,7 @@ describe('orca skills CLI', () => {
|
||||
`${JSON.stringify(
|
||||
{
|
||||
command:
|
||||
'npx --yes skills add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y',
|
||||
'npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y',
|
||||
skills: ['alpha'],
|
||||
global: true,
|
||||
executed: false
|
||||
@@ -368,7 +368,7 @@ describe('orca skills CLI', () => {
|
||||
await main(['skills', 'install', '--skill', 'alpha', '--local', '--dry-run'], '/tmp/repo')
|
||||
|
||||
expect(stdoutText(stdoutSpy)).toBe(
|
||||
'npx --yes skills add https://github.com/stablyai/orca --skill alpha --agent claude-code --agent universal -y\n\n' +
|
||||
'npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --agent claude-code --agent universal -y\n\n' +
|
||||
'Rerun without --dry-run to install now.\n'
|
||||
)
|
||||
|
||||
@@ -382,7 +382,7 @@ describe('orca skills CLI', () => {
|
||||
`${JSON.stringify(
|
||||
{
|
||||
command:
|
||||
'npx --yes skills add https://github.com/stablyai/orca --skill alpha --agent claude-code --agent universal -y',
|
||||
'npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --agent claude-code --agent universal -y',
|
||||
skills: ['alpha'],
|
||||
global: false,
|
||||
executed: false
|
||||
@@ -407,7 +407,7 @@ describe('orca skills CLI', () => {
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -442,7 +442,7 @@ describe('orca skills CLI', () => {
|
||||
'/c',
|
||||
'C:\\Program Files\\nodejs\\npx.cmd',
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -490,7 +490,7 @@ describe('orca skills CLI', () => {
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -520,7 +520,7 @@ describe('orca skills CLI', () => {
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -584,7 +584,7 @@ describe('orca skills CLI', () => {
|
||||
await main(['skills', 'update', '--skill', 'legacy-alpha', '--dry-run'], '/tmp/repo')
|
||||
|
||||
expect(stdoutText(stdoutSpy)).toBe(
|
||||
'npx --yes skills update alpha --global -y\n\nRerun without --dry-run to update now.\n'
|
||||
'npx --yes skills@1.5.21 update alpha --global -y\n\nRerun without --dry-run to update now.\n'
|
||||
)
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -600,7 +600,7 @@ describe('orca skills CLI', () => {
|
||||
expect(stdoutText(stdoutSpy)).toBe(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
command: 'npx --yes skills update alpha --project -y',
|
||||
command: 'npx --yes skills@1.5.21 update alpha --project -y',
|
||||
skills: ['alpha'],
|
||||
global: false,
|
||||
executed: false
|
||||
@@ -623,7 +623,7 @@ describe('orca skills CLI', () => {
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
['--yes', 'skills', 'update', 'alpha', '--project', '-y'],
|
||||
['--yes', 'skills@1.5.21', 'update', 'alpha', '--project', '-y'],
|
||||
expect.objectContaining({ stdio: 'inherit' })
|
||||
)
|
||||
})
|
||||
@@ -847,7 +847,7 @@ describe('orca skills CLI', () => {
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -874,7 +874,7 @@ describe('orca skills CLI', () => {
|
||||
)
|
||||
|
||||
expect(stdoutText(stdoutSpy)).toBe(
|
||||
'npx --yes skills add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n\n' +
|
||||
'npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n\n' +
|
||||
'Rerun without --dry-run to install now.\n'
|
||||
)
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
@@ -892,7 +892,7 @@ describe('orca skills CLI', () => {
|
||||
|
||||
// Why: stdout belongs to the child, so this record has to go to stderr.
|
||||
expect(stderrSpy).toHaveBeenCalledWith(
|
||||
'Running: npx --yes skills add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n'
|
||||
'Running: npx --yes skills@1.5.21 add https://github.com/stablyai/orca --skill alpha --global --agent claude-code --agent universal -y\n'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -908,7 +908,7 @@ describe('orca skills CLI', () => {
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
['--yes', 'skills', 'update', 'alpha', 'gamma', 'zeta', '--global', '-y'],
|
||||
['--yes', 'skills@1.5.21', 'update', 'alpha', 'gamma', 'zeta', '--global', '-y'],
|
||||
expect.objectContaining({ stdio: 'inherit' })
|
||||
)
|
||||
expect(process.exitCode).toBe(2)
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('skill freshness name-scoped update eligibility', () => {
|
||||
|
||||
it('builds only an explicit, deterministic global command', () => {
|
||||
expect(buildTargetedSkillUpdateCommand(['orchestration', 'orca-cli', 'orca-cli'])).toBe(
|
||||
'npx skills update orca-cli orchestration --global'
|
||||
'npx skills@1.5.21 update orca-cli orchestration --global'
|
||||
)
|
||||
expect(buildTargetedSkillUpdateCommand([])).toBeNull()
|
||||
expect(buildTargetedSkillUpdateCommand(['orca-cli;echo unsafe'])).toBeNull()
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('SkillUpdateRunner', () => {
|
||||
// non-interactive branch. Dropping either can wedge the run.
|
||||
expect(spawnCalls[0].args).toEqual([
|
||||
'--yes',
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'update',
|
||||
'orca-cli',
|
||||
'orchestration',
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type SkillUpdateRun,
|
||||
type SkillUpdateStartResult
|
||||
} from '../../shared/skill-freshness'
|
||||
import { SKILLS_CLI_PACKAGE_SPEC } from '../../shared/skills-cli-version'
|
||||
import { resolveCliCommand } from '../codex-cli/command'
|
||||
import { killWithDescendantSweep } from '../pty-descendant-termination'
|
||||
import { getSpawnArgsForWindows } from '../win32-utils'
|
||||
@@ -47,7 +48,7 @@ function clampOutput(value: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `npx --yes skills update <names> --global -y` headlessly.
|
||||
* Runs `npx --yes skills@<pin> update <names> --global -y` headlessly.
|
||||
*
|
||||
* Both `--yes` flags are load-bearing and distinct: `npx --yes` skips the
|
||||
* install-this-package prompt, and `skills … -y` takes the CLI's own
|
||||
@@ -92,7 +93,14 @@ export class SkillUpdateRunner {
|
||||
const resolveCommand = this.deps.resolveCommand ?? ((name: string) => resolveCliCommand(name))
|
||||
const spawnProcess = this.deps.spawnProcess ?? spawn
|
||||
const npxCommand = resolveCommand('npx')
|
||||
const npxArgs = ['--yes', 'skills', 'update', ...canonicalNames, '--global', '-y']
|
||||
const npxArgs = [
|
||||
'--yes',
|
||||
SKILLS_CLI_PACKAGE_SPEC,
|
||||
'update',
|
||||
...canonicalNames,
|
||||
'--global',
|
||||
'-y'
|
||||
]
|
||||
|
||||
let spawnCmd: string
|
||||
let spawnArgs: string[]
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('onboarding feature setup runner', () => {
|
||||
|
||||
expect(text).toBe(ALL_SKILL_INSTALL_COMMAND)
|
||||
expect(text).toBe(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli --skill computer-use --skill orchestration --skill orca-linear --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --skill computer-use --skill orchestration --skill orca-linear --global'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
|
||||
import { TooltipProvider } from '../ui/tooltip'
|
||||
|
||||
const INSTALL_COMMAND = 'npx skills add https://github.com/stablyai/orca --skill orca-cli --global'
|
||||
const UPDATE_COMMAND = 'npx skills update orca-cli --global'
|
||||
const INSTALL_COMMAND =
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --global'
|
||||
const UPDATE_COMMAND = 'npx skills@1.5.21 update orca-cli --global'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
clipboardWrite: vi.fn(),
|
||||
|
||||
@@ -16,8 +16,8 @@ vi.mock('./AgentSkillSetupPanel', () => ({
|
||||
describe('BrowserUseSkillStep', () => {
|
||||
it('forwards a single-skill installed command even when setup installs a bundle', () => {
|
||||
const bundleInstallCommand =
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli --skill orchestration --global'
|
||||
const updateCommand = 'npx skills update orca-cli --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --skill orchestration --global'
|
||||
const updateCommand = 'npx skills@1.5.21 update orca-cli --global'
|
||||
|
||||
renderToStaticMarkup(
|
||||
<BrowserUseSkillStep
|
||||
|
||||
@@ -181,6 +181,21 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
|
||||
).toBe(`${windowsNpxPreflightPrefix}${windowsNpxGuidance}) else (${installCommand})"`)
|
||||
})
|
||||
|
||||
it('rewrites the version-pinned update command on the Windows host path too', () => {
|
||||
const installCommand = buildAgentFeatureSkillInstallCommand(['orchestration'])
|
||||
|
||||
expect(
|
||||
buildSkillCommandForRuntime(
|
||||
'npx skills@1.5.21 update orchestration --global',
|
||||
{
|
||||
runtime: 'host',
|
||||
label: 'Windows'
|
||||
},
|
||||
'win32'
|
||||
)
|
||||
).toBe(`${windowsNpxPreflightPrefix}${windowsNpxGuidance}) else (${installCommand})"`)
|
||||
})
|
||||
|
||||
it('treats missing runtime as a preflighted Windows host fallback for skill updates', () => {
|
||||
const installCommand = buildAgentFeatureSkillInstallCommand(['orca-cli'])
|
||||
|
||||
|
||||
@@ -115,7 +115,9 @@ function normalizeWindowsSkillUpdateCommand(
|
||||
}
|
||||
|
||||
const trimmedCommand = command.trim()
|
||||
const updateMatch = /^npx\s+skills\s+update\s+([A-Za-z0-9_-]+)\s+--global$/i.exec(trimmedCommand)
|
||||
const updateMatch = /^npx\s+skills(?:@\S+)?\s+update\s+([A-Za-z0-9_-]+)\s+--global$/i.exec(
|
||||
trimmedCommand
|
||||
)
|
||||
if (!updateMatch) {
|
||||
return command
|
||||
}
|
||||
@@ -136,7 +138,7 @@ function wrapWindowsSkillCommandWithNpxPrerequisite(
|
||||
// Why: skill setup terminals spawn on the focused runtime environment, so a
|
||||
// Windows client must not hand a cmd.exe command to a remote host.
|
||||
isRemoteRuntimeEnvironmentFocused() ||
|
||||
!/^npx\s+skills\s+(?:add|update)\b/i.test(trimmedCommand)
|
||||
!/^npx\s+skills(?:@\S+)?\s+(?:add|update)\b/i.test(trimmedCommand)
|
||||
) {
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { getLinearUsageExamples } from '@/lib/linear-usage-examples'
|
||||
import { LinearAgentSkillPane } from './LinearAgentSkillPane'
|
||||
|
||||
const UPDATE_COMMAND = 'npx skills update orca-linear --global'
|
||||
const UPDATE_COMMAND = 'npx skills@1.5.21 update orca-linear --global'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
panelProps: [] as Record<string, unknown>[],
|
||||
|
||||
@@ -8,10 +8,10 @@ import { getOrchestrationUsageExamples } from '@/lib/orchestration-usage-example
|
||||
import { OrchestrationPane } from './OrchestrationPane'
|
||||
|
||||
const INSTALL_COMMAND =
|
||||
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
const UPDATE_COMMAND = INSTALL_COMMAND
|
||||
const WINDOWS_INSTALL_COMMAND =
|
||||
'cmd.exe /d /s /c "where.exe npx >nul 2>nul & if errorlevel 1 (echo ERROR: npx was not found. Install Node.js LTS from https://nodejs.org/ to get npx. & echo Then close this terminal and start skill setup again - a new terminal picks up the updated PATH. & exit /b 1) else (npx skills add https://github.com/stablyai/orca --skill orchestration --global)"'
|
||||
'cmd.exe /d /s /c "where.exe npx >nul 2>nul & if errorlevel 1 (echo ERROR: npx was not found. Install Node.js LTS from https://nodejs.org/ to get npx. & echo Then close this terminal and start skill setup again - a new terminal picks up the updated PATH. & exit /b 1) else (npx skills@1.5.21 add https://github.com/stablyai/orca --skill orchestration --global)"'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
dialogProps: [] as Record<string, unknown>[],
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('LinearAgentSkillInstallCta', () => {
|
||||
'Full guided setup (connect + skill + visibility) is under Settings → Task Sources.'
|
||||
)
|
||||
expect(rendered.textContent).toContain(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-linear --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-linear --global'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('LinearAgentSkillInstallCta', () => {
|
||||
})
|
||||
|
||||
expect(mocks.clipboardWrite).toHaveBeenCalledWith(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-linear --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-linear --global'
|
||||
)
|
||||
expect(mocks.toastSuccess).toHaveBeenCalled()
|
||||
})
|
||||
@@ -144,7 +144,7 @@ describe('LinearAgentSkillInstallCta', () => {
|
||||
|
||||
expect(rendered.textContent).toContain('Installed')
|
||||
expect(rendered.textContent).toContain('Agent skill installed. To update it, run:')
|
||||
expect(rendered.textContent).toContain('npx skills update orca-linear --global')
|
||||
expect(rendered.textContent).toContain('npx skills@1.5.21 update orca-linear --global')
|
||||
expect(rendered.textContent).not.toContain('Not installed')
|
||||
})
|
||||
|
||||
@@ -160,7 +160,7 @@ describe('LinearAgentSkillInstallCta', () => {
|
||||
|
||||
const rendered = await renderCta()
|
||||
|
||||
expect(rendered.textContent).toContain('npx skills update linear-tickets --global')
|
||||
expect(rendered.textContent).toContain('npx skills@1.5.21 update linear-tickets --global')
|
||||
})
|
||||
|
||||
it('notes that remote agent environments need their own setup', async () => {
|
||||
|
||||
@@ -344,11 +344,11 @@ describe('LinearAgentSkillSetupPrompt', () => {
|
||||
setupButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(document.body.textContent).toContain("wsl.exe -d 'Fedora' -- bash -lc 'npx skills add")
|
||||
expect(document.body.textContent).toContain("-d 'Fedora' -- bash -lc 'npx skills@1.5.21 add")
|
||||
expect(mocks.panelProps.at(-1)).toEqual(
|
||||
expect.objectContaining({
|
||||
installedCommand:
|
||||
"wsl.exe -d 'Fedora' -- bash -lc 'npx skills update orca-linear --global'",
|
||||
"wsl.exe -d 'Fedora' -- bash -lc 'npx skills@1.5.21 update orca-linear --global'",
|
||||
terminalShellOverride: 'powershell.exe',
|
||||
getPrerequisiteStatus: expect.any(Function)
|
||||
})
|
||||
|
||||
+5
-3
@@ -156,7 +156,7 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
|
||||
await renderPrompt()
|
||||
|
||||
expect(mocks.panelProps.at(-1)).toEqual(
|
||||
expect.objectContaining({ installedCommand: 'npx skills update orca-linear --global' })
|
||||
expect.objectContaining({ installedCommand: 'npx skills@1.5.21 update orca-linear --global' })
|
||||
)
|
||||
})
|
||||
|
||||
@@ -166,7 +166,9 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
|
||||
await renderPrompt()
|
||||
|
||||
expect(mocks.panelProps.at(-1)).toEqual(
|
||||
expect.objectContaining({ installedCommand: 'npx skills update linear-tickets --global' })
|
||||
expect.objectContaining({
|
||||
installedCommand: 'npx skills@1.5.21 update linear-tickets --global'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -176,7 +178,7 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
|
||||
await renderPrompt()
|
||||
|
||||
expect(mocks.panelProps.at(-1)).toEqual(
|
||||
expect.objectContaining({ installedCommand: 'npx skills update orca-linear --global' })
|
||||
expect.objectContaining({ installedCommand: 'npx skills@1.5.21 update orca-linear --global' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -499,7 +499,7 @@ describe('SkillFreshnessUpdateDialog', () => {
|
||||
await openViaRequest()
|
||||
|
||||
expect(container?.textContent).toContain(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -530,7 +530,7 @@ describe('SkillFreshnessUpdateDialog', () => {
|
||||
|
||||
const row = container?.querySelector('[data-skill-row="orchestration"]')
|
||||
expect(row?.textContent).toContain(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
)
|
||||
expect(row?.textContent).not.toContain('This is a project skill, not a global one')
|
||||
// Still listed, though — ownership silences the explanation, never the location.
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('skippedReason', () => {
|
||||
const reason = skippedReason([row(null)], 'orchestration')
|
||||
expect(reason).toContain('reports the skill as already up to date')
|
||||
expect(reason).toContain(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orchestration --global'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('skippedReason', () => {
|
||||
// Why: reinstalling a newer copy rolls the user back to what this build ships.
|
||||
const reason = skippedReason([row('newer')], 'orchestration')
|
||||
expect(reason).toContain('later version')
|
||||
expect(reason).not.toContain('skills add')
|
||||
expect(reason).not.toContain('skills@1.5.21 add')
|
||||
})
|
||||
|
||||
it('keeps a placement blocker ahead of the record advice', () => {
|
||||
@@ -63,7 +63,7 @@ describe('skippedReason', () => {
|
||||
// skip swapped the one runnable command for advice about a copy Orca never judged.
|
||||
const reason = skippedReason([row(null), projectRow()], 'orchestration')
|
||||
expect(reason).toContain('reports the skill as already up to date')
|
||||
expect(reason).toContain('skills add')
|
||||
expect(reason).toContain('skills@1.5.21 add')
|
||||
expect(reason).not.toContain('This is a project skill')
|
||||
})
|
||||
|
||||
|
||||
@@ -17,22 +17,22 @@ import {
|
||||
describe('agent feature skill commands', () => {
|
||||
it('builds a global install command by default', () => {
|
||||
expect(buildAgentFeatureSkillInstallCommand(['orca-cli'])).toBe(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --global'
|
||||
)
|
||||
})
|
||||
|
||||
it('drops --global when installing locally', () => {
|
||||
expect(buildAgentFeatureSkillInstallCommand(['orca-cli'], { global: false })).toBe(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli'
|
||||
)
|
||||
})
|
||||
|
||||
it('repeats --skill per name for multi-skill installs', () => {
|
||||
expect(buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])).toBe(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli --skill orchestration --global'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --skill orchestration --global'
|
||||
)
|
||||
expect(buildAgentFeatureSkillInstallArgs(['orca-cli', 'orchestration'])).toEqual([
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'add',
|
||||
'https://github.com/stablyai/orca',
|
||||
'--skill',
|
||||
@@ -76,10 +76,10 @@ describe('agent feature skill commands', () => {
|
||||
expect(
|
||||
buildAgentFeatureSkillInstallCommand(['orca-cli'], { yes: true, agents: ['universal'] })
|
||||
).toBe(
|
||||
'npx skills add https://github.com/stablyai/orca --skill orca-cli --global --agent universal -y'
|
||||
'npx skills@1.5.21 add https://github.com/stablyai/orca --skill orca-cli --global --agent universal -y'
|
||||
)
|
||||
expect(buildAgentFeatureSkillUpdateCommand(['orca-cli'], { global: false, yes: true })).toBe(
|
||||
'npx skills update orca-cli --project -y'
|
||||
'npx skills@1.5.21 update orca-cli --project -y'
|
||||
)
|
||||
expect(
|
||||
buildAgentFeatureSkillInstallArgs(['orca-cli'], { yes: true, agents: ['universal'] }).at(-1)
|
||||
@@ -89,26 +89,26 @@ describe('agent feature skill commands', () => {
|
||||
|
||||
it('builds single-skill update commands', () => {
|
||||
expect(buildAgentFeatureSkillUpdateCommand('orchestration')).toBe(
|
||||
'npx skills update orchestration --global'
|
||||
'npx skills@1.5.21 update orchestration --global'
|
||||
)
|
||||
})
|
||||
|
||||
it('trims and rejects blank update skill names', () => {
|
||||
expect(buildAgentFeatureSkillUpdateCommand(' orca-cli ')).toBe(
|
||||
'npx skills update orca-cli --global'
|
||||
'npx skills@1.5.21 update orca-cli --global'
|
||||
)
|
||||
expect(() => buildAgentFeatureSkillUpdateCommand(' ')).toThrow('A skill name is required.')
|
||||
})
|
||||
|
||||
it('builds multi-skill update commands and selects project scope for --local', () => {
|
||||
expect(buildAgentFeatureSkillUpdateCommand(['orca-cli', 'orchestration'])).toBe(
|
||||
'npx skills update orca-cli orchestration --global'
|
||||
'npx skills@1.5.21 update orca-cli orchestration --global'
|
||||
)
|
||||
expect(buildAgentFeatureSkillUpdateCommand(['orca-cli'], { global: false })).toBe(
|
||||
'npx skills update orca-cli --project'
|
||||
'npx skills@1.5.21 update orca-cli --project'
|
||||
)
|
||||
expect(buildAgentFeatureSkillUpdateArgs(['orca-cli'], { global: false })).toEqual([
|
||||
'skills',
|
||||
'skills@1.5.21',
|
||||
'update',
|
||||
'orca-cli',
|
||||
'--project'
|
||||
@@ -117,16 +117,30 @@ describe('agent feature skill commands', () => {
|
||||
})
|
||||
|
||||
it('exports single-skill update constants without changing install bundles', () => {
|
||||
expect(ORCA_CLI_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-cli --global')
|
||||
expect(COMPUTER_USE_SKILL_UPDATE_COMMAND).toBe('npx skills update computer-use --global')
|
||||
expect(ORCHESTRATION_SKILL_UPDATE_COMMAND).toBe('npx skills update orchestration --global')
|
||||
expect(ORCA_CLI_SKILL_UPDATE_COMMAND).toBe('npx skills@1.5.21 update orca-cli --global')
|
||||
expect(COMPUTER_USE_SKILL_UPDATE_COMMAND).toBe('npx skills@1.5.21 update computer-use --global')
|
||||
expect(ORCHESTRATION_SKILL_UPDATE_COMMAND).toBe(
|
||||
'npx skills@1.5.21 update orchestration --global'
|
||||
)
|
||||
expect(EPHEMERAL_VMS_SKILL_UPDATE_COMMAND).toBe(
|
||||
'npx skills update orca-per-workspace-env --global'
|
||||
'npx skills@1.5.21 update orca-per-workspace-env --global'
|
||||
)
|
||||
expect(ORCA_LINEAR_SKILL_UPDATE_COMMAND).toBe('npx skills@1.5.21 update orca-linear --global')
|
||||
expect(LINEAR_TICKETS_SKILL_UPDATE_COMMAND).toBe(
|
||||
'npx skills@1.5.21 update linear-tickets --global'
|
||||
)
|
||||
expect(ORCA_LINEAR_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-linear --global')
|
||||
expect(LINEAR_TICKETS_SKILL_UPDATE_COMMAND).toBe('npx skills update linear-tickets --global')
|
||||
expect(ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND).toBe(
|
||||
buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])
|
||||
)
|
||||
})
|
||||
|
||||
it('pins the skills CLI to one exact version in every invocation', () => {
|
||||
// Why: an unpinned `skills` floats to npm latest, whose parser silently
|
||||
// drops unknown flags — an upstream --agent rename would revive the
|
||||
// ~75-agent fan-out install with exit 0 (#11593). Exact only, never a
|
||||
// range: cmd.exe treats ^ as its escape character in pasteable commands.
|
||||
const exactPin = /^skills@\d+\.\d+\.\d+$/
|
||||
expect(buildAgentFeatureSkillInstallArgs(['orca-cli'])[0]).toMatch(exactPin)
|
||||
expect(buildAgentFeatureSkillUpdateArgs('orca-cli')[0]).toMatch(exactPin)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isSkillsCliAgentKeyShaped } from './skills-cli-agent-keys'
|
||||
import { SKILLS_CLI_PACKAGE_SPEC } from './skills-cli-version'
|
||||
|
||||
export const ORCA_SKILLS_REPOSITORY_URL = 'https://github.com/stablyai/orca'
|
||||
|
||||
@@ -42,7 +43,7 @@ export function buildAgentFeatureSkillInstallArgs(
|
||||
// Why: one flag per name remains compatible with both single-value and variadic parsers.
|
||||
const skillArgs = skillNames.flatMap((name) => ['--skill', name])
|
||||
return [
|
||||
'skills',
|
||||
SKILLS_CLI_PACKAGE_SPEC,
|
||||
'add',
|
||||
ORCA_SKILLS_REPOSITORY_URL,
|
||||
...skillArgs,
|
||||
@@ -75,7 +76,7 @@ export function buildAgentFeatureSkillUpdateArgs(
|
||||
}
|
||||
const global = options.global ?? true
|
||||
return [
|
||||
'skills',
|
||||
SKILLS_CLI_PACKAGE_SPEC,
|
||||
'update',
|
||||
...names,
|
||||
global ? '--global' : '--project',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SkillProvider, SkillSourceKind } from './skills'
|
||||
import { SKILLS_CLI_PACKAGE_SPEC } from './skills-cli-version'
|
||||
|
||||
export type SkillBundleFileIdentity = {
|
||||
path: string
|
||||
@@ -209,7 +210,9 @@ export function canonicalizeSkillUpdateNames(names: readonly string[]): string[]
|
||||
|
||||
export function buildTargetedSkillUpdateCommand(names: readonly string[]): string | null {
|
||||
const canonicalNames = canonicalizeSkillUpdateNames(names)
|
||||
return canonicalNames ? `npx skills update ${canonicalNames.join(' ')} --global` : null
|
||||
return canonicalNames
|
||||
? `npx ${SKILLS_CLI_PACKAGE_SPEC} update ${canonicalNames.join(' ')} --global`
|
||||
: null
|
||||
}
|
||||
|
||||
// Why: `skills update` has no --json (that flag only exists on `list`), so the
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Why: `skills` floats to npm latest at spawn time, and its parser silently
|
||||
// drops flags it does not recognize — an upstream --agent rename would revive
|
||||
// the ~75-agent fan-out install with exit 0 (#11593). Pinning freezes the argv
|
||||
// contract (published npm versions are immutable), and CI re-proves --agent
|
||||
// scoping against this exact version, so a bump that breaks scoping goes red.
|
||||
export const SKILLS_CLI_VERSION = '1.5.21'
|
||||
|
||||
// Why: an exact spec, never a range — `^` is cmd.exe's escape character, so a
|
||||
// caret range inside a pasteable Settings command would be silently mangled.
|
||||
export const SKILLS_CLI_PACKAGE_SPEC = `skills@${SKILLS_CLI_VERSION}`
|
||||
Reference in New Issue
Block a user