fix(cli): surface install lifecycle warnings

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-01 20:22:18 +08:00
parent 130e27f3f6
commit b9be415d34
5 changed files with 74 additions and 4 deletions

View file

@ -115,7 +115,16 @@ export async function installCommand(
})
if (options.json) {
return JSON.stringify({ ok: true, namespace, slug, installed: result.installed })
return JSON.stringify({
ok: true,
namespace,
slug,
installed: result.installed,
...(result.warnings?.length ? { warnings: result.warnings } : {})
})
}
return result.installed.map(i => `Installed ${namespace}/${slug} -> ${i.dir} (${i.agent})`).join('\n')
return [
...result.installed.map(i => `Installed ${namespace}/${slug} -> ${i.dir} (${i.agent})`),
...(result.warnings ?? []).map(warning => `Warning: ${warning}`)
].join('\n')
}

View file

@ -145,6 +145,7 @@ function renderPullResult(result: PullResult, json: boolean, check: boolean): st
...result.entries
.filter(entry => !result.actions.some(action => action.slug === entry.slug))
.map(entry => `${entry.status.padEnd(16)} ${entry.slug}`),
...result.warnings.map(item => `warning ${item.slug}: ${item.message}`),
...result.failures.map(item => `failed ${item.slug}: ${item.message}`)
]
return lines.join('\n')

View file

@ -36,6 +36,7 @@ export interface PullResult {
entries: SyncStatusEntry[]
actions: Array<{ slug: string; action: 'installed' | 'updated' | 'pruned' }>
failures: Array<{ slug: string; message: string }>
warnings: Array<{ slug: string; message: string }>
}
export interface PushResultItem {
@ -133,6 +134,7 @@ export async function pullNamespace(options: {
check: boolean
prune: boolean
force: boolean
installSkillFn?: typeof installSkill
}): Promise<PullResult> {
const inspected = await inspectNamespaceWorkspace(options)
const result: PullResult = {
@ -140,7 +142,8 @@ export async function pullNamespace(options: {
rootDir: options.rootDir,
entries: inspected.entries,
actions: [],
failures: []
failures: [],
warnings: []
}
if (options.check) return result
@ -154,7 +157,7 @@ export async function pullNamespace(options: {
const remote = remoteBySlug.get(entry.slug)
if (!remote) continue
try {
await installSkill({
const installed = await (options.installSkillFn ?? installSkill)({
registry: options.registry,
token: options.token,
namespace: options.namespace,
@ -172,6 +175,7 @@ export async function pullNamespace(options: {
force: entry.status !== 'not-installed' || options.force
})
result.actions.push({ slug: entry.slug, action: entry.status === 'not-installed' ? 'installed' : 'updated' })
result.warnings.push(...(installed.warnings ?? []).map(message => ({ slug: entry.slug, message })))
} catch (error) {
result.failures.push({ slug: entry.slug, message: error instanceof Error ? error.message : 'install failed' })
}

View file

@ -6,6 +6,8 @@ import { describe, expect, test } from 'bun:test'
import { startFakeRegistry, type FakeSkill } from '../helpers/fake-registry'
import { runCli } from '../helpers/run-cli'
import { createTempHome } from '../helpers/temp-env'
import { SkillHubClient } from '../../src/clients/skillhub-client'
import { pullNamespace } from '../../src/services/sync-service'
function makeSkill(body: string): { zipBytes: Uint8Array; fingerprint: string } {
const content = strToU8(body)
@ -15,6 +17,41 @@ function makeSkill(body: string): { zipBytes: Uint8Array; fingerprint: string }
}
describe('sync command', () => {
test('pull propagates committed install warnings', async () => {
const env = await createTempHome()
const skillsDir = join(env.cwd, 'team-skills')
const fixture = makeSkill('---\nname: demo\ndescription: Demo\nversion: 1.0.0\n---\n')
const registry = await startFakeRegistry({
token: 'token',
skills: [{ namespace: 'team-a', slug: 'demo', ...fixture }]
})
try {
const result = await pullNamespace({
client: new SkillHubClient(registry.url, 'token'),
registry: registry.url,
token: 'token',
namespace: 'team-a',
rootDir: skillsDir,
check: false,
prune: false,
force: false,
installSkillFn: async () => ({
installed: [{ agent: 'workspace', dir: join(skillsDir, 'demo') }],
warnings: ['target lock cleanup failed: simulated release failure']
})
})
expect(result.actions).toEqual([{ slug: 'demo', action: 'installed' }])
expect(result.warnings).toEqual([{
slug: 'demo',
message: 'target lock cleanup failed: simulated release failure'
}])
} finally {
registry.stop()
}
})
test('pull installs a namespace incrementally and writes workspace metadata', async () => {
const env = await createTempHome()
const skillsDir = join(env.cwd, 'team-skills')

View file

@ -146,6 +146,25 @@ describe('installCommand dependency injection', () => {
}] as AgentCandidate[]
}
test('renders post-commit warnings in human and JSON output', async () => {
const deps: InstallCommandDeps = {
isTTY: () => false,
resolveInstallTargets: fakeResolveInstallTargets(),
installSkill: async () => ({
installed: [{ agent: 'codex', dir: '/home/u/.codex/skills/foo' }],
warnings: ['target lock cleanup failed: simulated release failure']
})
}
const human = await installCommand('@global/foo', { registry: 'http://localhost' }, deps)
expect(human).toContain('Warning: target lock cleanup failed')
const json = JSON.parse(await installCommand('@global/foo', {
registry: 'http://localhost',
json: true
}, deps))
expect(json.warnings).toEqual(['target lock cleanup failed: simulated release failure'])
})
test('passes a namespaced coordinate to installSkill', async () => {
let received: Parameters<NonNullable<InstallCommandDeps['installSkill']>>[0] | undefined
const deps: InstallCommandDeps = {