feat(skills): install bundles on another machine

This commit is contained in:
Jinwoo-H
2026-08-12 08:21:07 -04:00
parent ed4e2d8d76
commit f548827c1e
6 changed files with 55 additions and 8 deletions
@@ -889,9 +889,8 @@ does not mean the surrounding phase is complete.
- [x] Show whether a package came from Orca Cloud and its accessible version history.
- [x] Refresh discovery and installation state after local or remote actions.
- [x] Keep Cloud deletion, share revocation, and local removal clearly separate.
- [ ] Add installed-bundle update, rollback, install-on-another-machine, inspect, and remove actions.
Grouped inspection, selective update/rollback, and one-surface safe removal are implemented;
install-on-another-machine remains.
- [x] Add installed-bundle update, rollback, install-on-another-machine, inspect, and remove actions.
An active unlisted link opens the existing destination-aware installer for another machine.
- [x] Add shared-bundle copy-link, access, revoke, publish-version, and Cloud-delete actions. Exact
full-bundle version publishing reuses its package; ambiguous or partial managed-bundle
selections create a new package rather than mutating the wrong history.
@@ -165,7 +165,8 @@ beforeEach(() => {
useAppStore.setState({
runtimeEnvironments: [],
sshConnectionStates: new Map(),
sshTargetLabels: new Map()
sshTargetLabels: new Map(),
pendingSkillShareId: null
})
})
@@ -344,6 +345,33 @@ describe('SkillInstallManagementDialog', () => {
expect(skills.installPackageVersion).not.toHaveBeenCalled()
})
it('opens an active bundle link in the existing machine installer', async () => {
const installed = [bundleInstall('alpha-skill'), bundleInstall('beta-skill')]
const versions = [bundleVersion('ver_1', ['alpha-skill', 'beta-skill'])]
const details = {
...packageDetails(versions),
management: {
shares: [
{
id: 'share_bundle',
url: 'https://app.orca.dev/skills/share/share_bundle',
createdAt: '2026-08-12T00:00:00.000Z'
}
]
}
}
const skills = skillsApi(installed, versions, details)
const onOpenChange = vi.fn()
Object.defineProperty(window, 'api', { configurable: true, value: { skills } })
render(<SkillInstallManagementDialog open onOpenChange={onOpenChange} />)
fireEvent.click(await screen.findByRole('button', { name: /2 skill bundle.*ver_1/ }))
fireEvent.click(await screen.findByRole('button', { name: 'Install on another machine' }))
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(useAppStore.getState().pendingSkillShareId).toBe('share_bundle')
})
it('removes a bundle with one confirmation and preserves modified skills', async () => {
const installed = [bundleInstall('alpha-skill'), bundleInstall('beta-skill', 'modified')]
const skills = skillsApi(installed, [bundleVersion('ver_1', ['alpha-skill', 'beta-skill'])])
@@ -310,8 +310,6 @@ export function SkillInstallManagementDialog({
setError(null)
setNotice(null)
setResult(null)
setBundleResult(null)
setConfirmRemove(false)
onOpenChange(false)
}
@@ -377,6 +375,10 @@ export function SkillInstallManagementDialog({
onVersionChange={setVersionId}
onInstall={(discardLocal) => void installVersion(discardLocal)}
onCancelInstall={() => void cancelInstall()}
onInstallAnotherMachine={(shareId) => {
close()
useAppStore.getState().openSkillShare(shareId)
}}
onRemove={(discardLocal) => void remove(discardLocal)}
onCloudChanged={refreshPackageDetails}
onPackageDeleted={packageDeleted}
@@ -1,4 +1,4 @@
import { AlertTriangle, Loader2, RotateCcw, Trash2 } from 'lucide-react'
import { AlertTriangle, Loader2, MonitorUp, RotateCcw, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Select,
@@ -30,6 +30,7 @@ export function SkillManagedInstallDetails({
onVersionChange,
onInstall,
onCancelInstall,
onInstallAnotherMachine,
onRemove,
onCloudChanged,
onPackageDeleted
@@ -47,6 +48,7 @@ export function SkillManagedInstallDetails({
onVersionChange: (versionId: string) => void
onInstall: (discardLocal?: boolean) => void
onCancelInstall: () => void
onInstallAnotherMachine: (shareId: string) => void
onRemove: (discardLocal?: boolean) => void
onCloudChanged: () => Promise<void>
onPackageDeleted: () => void
@@ -56,6 +58,7 @@ export function SkillManagedInstallDetails({
selectedVersion?.manifest && 'skills' in selectedVersion.manifest
? selectedVersion.manifest
: null
const activeShare = details.management?.shares[0]
return (
<section className="space-y-3 rounded-md border border-border p-3">
<div>
@@ -125,6 +128,16 @@ export function SkillManagedInstallDetails({
{copy.cancelInstall}
</Button>
) : null}
{activeShare ? (
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => onInstallAnotherMachine(activeShare.id)}
>
<MonitorUp className="size-4" /> {copy.installAnotherMachine}
</Button>
) : null}
<Button
variant={confirmRemove ? 'destructive' : 'outline'}
size="sm"
@@ -55,6 +55,10 @@ export function skillInstallManagementCopy() {
'auto.components.skills.SkillInstallManagementDialog.561e49ccd1',
'Install selected version'
),
installAnotherMachine: translate(
'auto.components.skills.SkillInstallManagementDialog.installAnotherMachine',
'Install on another machine'
),
cancelInstall: translate(
'auto.components.skills.SkillInstallManagementDialog.c1d03ee50d',
'Cancel installation'
+2 -1
View File
@@ -4208,7 +4208,8 @@
"34c2ef9e71": "Install {{count}} skills",
"a0cab67c2f": "Remove {{count}} skills",
"f970d8088d": "Confirm remove {{count}} skills",
"dab29e4b54": "{{installed}} installed · {{updated}} updated · {{keptLocal}} kept local"
"dab29e4b54": "{{installed}} installed · {{updated}} updated · {{keptLocal}} kept local",
"installAnotherMachine": "Install on another machine"
},
"SkillInstallReviewContent": {
"89e2601162": "Discard and replace",