Revert "fix(linear): migrate legacy skill installs"

This reverts commit 3ff45f9245.
This commit is contained in:
Brennan Benson
2026-07-17 15:03:45 -07:00
parent 3ff45f9245
commit d220db167d
11 changed files with 145 additions and 53 deletions
@@ -16,7 +16,8 @@ const UPDATE_COMMAND = 'npx skills update orca-linear --global'
const mocks = vi.hoisted(() => ({
panelProps: [] as Record<string, unknown>[],
runtime: 'native' as 'native' | 'wsl',
skillInstalled: true
skillInstalled: true,
updateSkillName: 'orca-linear'
}))
vi.mock('./AgentSkillSetupPanel', () => ({
@@ -40,6 +41,13 @@ vi.mock('./CliSkillRuntimeSetup', () => ({
getWslCliDistroRequest: () => undefined
}))
vi.mock('@/lib/linear-agent-skill-update-command', () => ({
getLinearAgentSkillUpdateTarget: () => ({
command: UPDATE_COMMAND,
skillName: mocks.updateSkillName
})
}))
vi.mock('@/hooks/useInstalledAgentSkills', () => ({
GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'],
useInstalledAgentSkillNames: () => ({
@@ -86,6 +94,7 @@ describe('LinearAgentSkillPane', () => {
mocks.panelProps.length = 0
mocks.runtime = 'native'
mocks.skillInstalled = true
mocks.updateSkillName = 'orca-linear'
})
it('renders the Linear skill card and the usage examples', () => {
@@ -121,16 +130,10 @@ describe('LinearAgentSkillPane', () => {
expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBeUndefined()
})
it('offers the canonical install when the accepted skill is missing', async () => {
mocks.skillInstalled = false
it('checks freshness under the legacy name when that is the installed update target', async () => {
mocks.updateSkillName = 'linear-tickets'
await renderPane()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({
installed: false,
command: ORCA_LINEAR_SKILL_INSTALL_COMMAND,
freshnessSkillName: ORCA_LINEAR_SKILL_NAME
})
)
expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBe('linear-tickets')
})
})
@@ -5,13 +5,13 @@ import { LinearIcon } from '@/components/icons/LinearIcon'
import {
LINEAR_AGENT_SKILL_NAMES,
ORCA_LINEAR_SKILL_INSTALL_COMMAND,
ORCA_LINEAR_SKILL_NAME,
ORCA_LINEAR_SKILL_UPDATE_COMMAND
ORCA_LINEAR_SKILL_NAME
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
} from '@/lib/agent-skill-cli-prerequisite'
import { getLinearAgentSkillUpdateTarget } from '@/lib/linear-agent-skill-update-command'
import { getLinearUsageExamples } from '@/lib/linear-usage-examples'
import type { SkillUsageExample } from '@/lib/skill-usage-example'
import {
@@ -52,6 +52,7 @@ export function LinearAgentSkillPane(): React.JSX.Element {
installed: linearSkillInstalled,
loading: linearSkillLoading,
error: linearSkillError,
skills: linearSkills,
refresh: refreshLinearSkill
} = useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, {
discoveryTarget: activeSkillRuntime.discoveryTarget,
@@ -68,14 +69,20 @@ export function LinearAgentSkillPane(): React.JSX.Element {
),
[activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason]
)
const updateTarget = useMemo(
() => getLinearAgentSkillUpdateTarget(linearSkills, linearSkillInstalled),
[linearSkillInstalled, linearSkills]
)
const updateCommand = useMemo(() => {
const command = updateTarget.command
return activeSkillRuntime.installDisabledReason
? ORCA_LINEAR_SKILL_UPDATE_COMMAND
: buildSkillCommandForRuntime(
ORCA_LINEAR_SKILL_UPDATE_COMMAND,
activeSkillRuntime.agentRuntime
)
}, [activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason])
? command
: buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime)
}, [
activeSkillRuntime.agentRuntime,
activeSkillRuntime.installDisabledReason,
updateTarget.command
])
return (
<SearchableSetting
@@ -130,7 +137,7 @@ export function LinearAgentSkillPane(): React.JSX.Element {
// Why: the local-host-only freshness scan cannot vouch for a WSL runtime,
// so fall back to the presence-only pill there (mirrors the other skills).
freshnessSkillName={
activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : ORCA_LINEAR_SKILL_NAME
activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : updateTarget.skillName
}
/>
@@ -24,10 +24,10 @@ const updateCapableCallers = new Map<string, readonly string[]>([
['COMPUTER_USE_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
// Why: the Linear settings section must update only the canonical skill;
// legacy-only discovery is treated as missing and follows the install path.
// Why: the Linear settings section shares the update-target resolver so
// legacy-only installs keep the command and freshness identity aligned.
'src/renderer/src/components/settings/LinearAgentSkillPane.tsx',
['ORCA_LINEAR_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
['getLinearAgentSkillUpdateTarget', 'installedCommand={updateCommand}']
],
[
'src/renderer/src/components/settings/EphemeralVmsPane.tsx',
@@ -50,8 +50,10 @@ const updateCapableCallers = new Map<string, readonly string[]>([
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
// Why: the single-skill update command selection moved into
// getLinearAgentSkillUpdateCommand so the settings install CTA shares it.
'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx',
['ORCA_LINEAR_SKILL_UPDATE_COMMAND', 'installedCommand={installedCommand}']
['getLinearAgentSkillUpdateCommand', 'installedCommand={installedCommand}']
],
[
'src/renderer/src/components/sidebar/LinearAgentSkillSetupDialog.tsx',
@@ -146,8 +146,8 @@ describe('LinearAgentSkillInstallCta', () => {
expect(rendered.textContent).not.toContain('Not installed')
})
it('migrates a legacy-only install through the canonical install command', async () => {
mocks.skillState.installed = false
it('updates through the legacy skill name when only linear-tickets is installed', async () => {
mocks.skillState.installed = true
mocks.skillState.skills = [
discoveredSkill({
name: 'linear-tickets',
@@ -158,11 +158,7 @@ describe('LinearAgentSkillInstallCta', () => {
const rendered = await renderCta()
expect(rendered.textContent).toContain(
'npx skills add https://github.com/stablyai/orca --skill orca-linear --global'
)
expect(rendered.textContent).toContain('Not installed')
expect(rendered.textContent).not.toContain('npx skills update linear-tickets --global')
expect(rendered.textContent).toContain('npx skills update linear-tickets --global')
})
it('notes that remote agent environments need their own setup', async () => {
@@ -11,9 +11,9 @@ import {
import {
LINEAR_AGENT_SKILL_NAMES,
ORCA_LINEAR_SKILL_INSTALL_COMMAND,
ORCA_LINEAR_SKILL_NAME,
ORCA_LINEAR_SKILL_UPDATE_COMMAND
ORCA_LINEAR_SKILL_NAME
} from '@/lib/agent-feature-install-commands'
import { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command'
import { cn } from '@/lib/utils'
import { getLinearAgentSkillSetupInlineRuntimeCopy } from '../sidebar/linear-agent-skill-setup-copy'
import {
@@ -57,10 +57,12 @@ export function LinearAgentSkillInstallCta({
const command = useMemo(
() =>
buildSkillCommandForRuntime(
skill.installed ? ORCA_LINEAR_SKILL_UPDATE_COMMAND : ORCA_LINEAR_SKILL_INSTALL_COMMAND,
skill.installed
? getLinearAgentSkillUpdateCommand(skill.skills, skill.installed)
: ORCA_LINEAR_SKILL_INSTALL_COMMAND,
agentRuntime
),
[agentRuntime, skill.installed]
[agentRuntime, skill.installed, skill.skills]
)
const subordinateRowClass = useIntegrationSubordinateRowClass('space-y-1.5')
const commandRowClass = useIntegrationCommandRowClass()
@@ -9,9 +9,9 @@ import {
} from '@/hooks/useInstalledAgentSkills'
import {
LINEAR_AGENT_SKILL_NAMES,
ORCA_LINEAR_SKILL_INSTALL_COMMAND,
ORCA_LINEAR_SKILL_UPDATE_COMMAND
ORCA_LINEAR_SKILL_INSTALL_COMMAND
} from '@/lib/agent-feature-install-commands'
import { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command'
import {
ensureOrcaCliAvailableForAgentSkillTerminal,
isOrcaCliAvailableOnPath
@@ -122,8 +122,12 @@ export function LinearAgentSkillSetupPrompt({
[agentRuntime]
)
const installedCommand = useMemo(
() => buildSkillCommandForRuntime(ORCA_LINEAR_SKILL_UPDATE_COMMAND, agentRuntime),
[agentRuntime]
() =>
buildSkillCommandForRuntime(
getLinearAgentSkillUpdateCommand(skill.skills, skill.installed),
agentRuntime
),
[agentRuntime, skill.installed, skill.skills]
)
const terminalShellOverride = getLinearPromptTerminalShellOverride(
currentPlatform,
@@ -160,18 +160,13 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
)
})
it('migrates a legacy-only install through the canonical install command', async () => {
mocks.skillState.installed = false
it('uses the legacy update command when only the legacy Linear skill is installed', async () => {
mocks.skillState.skills = [legacyLinearSkillPath()]
await renderPrompt()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({
installed: false,
command: 'npx skills add https://github.com/stablyai/orca --skill orca-linear --global',
installedCommand: 'npx skills update orca-linear --global'
})
expect.objectContaining({ installedCommand: 'npx skills update linear-tickets --global' })
)
})
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import type { DiscoveredSkill } from '../../../shared/skills'
import {
LINEAR_TICKETS_SKILL_UPDATE_COMMAND,
ORCA_LINEAR_SKILL_UPDATE_COMMAND
} from './agent-feature-install-commands'
import { getLinearAgentSkillUpdateTarget } from './linear-agent-skill-update-command'
function skill(name: string): DiscoveredSkill {
return {
id: name,
name,
description: null,
providers: ['agent-skills'],
sourceKind: 'home',
sourceLabel: 'Agent skills home',
rootPath: '/home/test/.agents/skills',
directoryPath: `/home/test/.agents/skills/${name}`,
skillFilePath: `/home/test/.agents/skills/${name}/SKILL.md`,
installed: true,
fileCount: 1,
updatedAt: null
}
}
describe('getLinearAgentSkillUpdateTarget', () => {
it('targets the canonical skill for missing, canonical, and dual-name installs', () => {
expect(getLinearAgentSkillUpdateTarget([], false)).toEqual({
skillName: 'orca-linear',
command: ORCA_LINEAR_SKILL_UPDATE_COMMAND
})
expect(getLinearAgentSkillUpdateTarget([skill('orca-linear')], true)).toEqual({
skillName: 'orca-linear',
command: ORCA_LINEAR_SKILL_UPDATE_COMMAND
})
expect(
getLinearAgentSkillUpdateTarget([skill('orca-linear'), skill('linear-tickets')], true)
).toEqual({
skillName: 'orca-linear',
command: ORCA_LINEAR_SKILL_UPDATE_COMMAND
})
})
it('keeps the update and freshness identity on a legacy-only install', () => {
expect(getLinearAgentSkillUpdateTarget([skill('linear-tickets')], true)).toEqual({
skillName: 'linear-tickets',
command: LINEAR_TICKETS_SKILL_UPDATE_COMMAND
})
})
})
@@ -0,0 +1,40 @@
import type { DiscoveredSkill } from '../../../shared/skills'
import {
GLOBAL_AGENT_SKILL_SOURCE_KINDS,
hasInstalledAgentSkill
} from '@/hooks/useInstalledAgentSkills'
import {
LINEAR_TICKETS_SKILL_NAME,
LINEAR_TICKETS_SKILL_UPDATE_COMMAND,
ORCA_LINEAR_SKILL_NAME,
ORCA_LINEAR_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
export type LinearAgentSkillUpdateTarget = {
skillName: typeof ORCA_LINEAR_SKILL_NAME | typeof LINEAR_TICKETS_SKILL_NAME
command: string
}
// Why: legacy-only installs must update and report freshness for the installed
// legacy skill, while fresh/canonical/both-name states use the canonical name.
export function getLinearAgentSkillUpdateTarget(
skills: readonly DiscoveredSkill[],
installed: boolean
): LinearAgentSkillUpdateTarget {
const canonicalSkillInstalled = hasInstalledAgentSkill(skills, ORCA_LINEAR_SKILL_NAME, {
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
const legacySkillInstalled = hasInstalledAgentSkill(skills, LINEAR_TICKETS_SKILL_NAME, {
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
return !installed || canonicalSkillInstalled || !legacySkillInstalled
? { skillName: ORCA_LINEAR_SKILL_NAME, command: ORCA_LINEAR_SKILL_UPDATE_COMMAND }
: { skillName: LINEAR_TICKETS_SKILL_NAME, command: LINEAR_TICKETS_SKILL_UPDATE_COMMAND }
}
export function getLinearAgentSkillUpdateCommand(
skills: readonly DiscoveredSkill[],
installed: boolean
): string {
return getLinearAgentSkillUpdateTarget(skills, installed).command
}
@@ -4,7 +4,6 @@ import {
buildAgentFeatureSkillUpdateCommand,
COMPUTER_USE_SKILL_UPDATE_COMMAND,
EPHEMERAL_VMS_SKILL_UPDATE_COMMAND,
LINEAR_AGENT_SKILL_NAMES,
LINEAR_TICKETS_SKILL_UPDATE_COMMAND,
ORCA_LINEAR_SKILL_UPDATE_COMMAND,
ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND,
@@ -39,8 +38,4 @@ describe('agent feature skill commands', () => {
buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])
)
})
it('requires the canonical Linear skill so legacy installs migrate', () => {
expect(LINEAR_AGENT_SKILL_NAMES).toEqual(['orca-linear'])
})
})
+1 -3
View File
@@ -6,9 +6,7 @@ export const ORCHESTRATION_SKILL_NAME = 'orchestration'
export const EPHEMERAL_VMS_SKILL_NAME = 'orca-per-workspace-env'
export const ORCA_LINEAR_SKILL_NAME = 'orca-linear'
export const LINEAR_TICKETS_SKILL_NAME = 'linear-tickets'
// Why: linear-tickets is retired and must not satisfy setup readiness; legacy-only
// users should be offered the canonical orca-linear install path.
export const LINEAR_AGENT_SKILL_NAMES = [ORCA_LINEAR_SKILL_NAME] as const
export const LINEAR_AGENT_SKILL_NAMES = [ORCA_LINEAR_SKILL_NAME, LINEAR_TICKETS_SKILL_NAME] as const
export function buildAgentFeatureSkillInstallCommand(skillNames: readonly string[]): string {
if (skillNames.length === 0) {